Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
Loading items

Target

Select target project
  • deadlocker8/SpotifyBackup
1 result
Select Git revision
Loading items
Show changes
Commits on Source (4)
......@@ -6,10 +6,10 @@ import pyaudiowpatch as pyaudio
from TheCodeLabs_BaseUtils.DefaultLogger import DefaultLogger
LOG_FORMAT = '[%(levelname)-7s] - %(asctime)s - %(message)s'
LOGGER = DefaultLogger().create_logger_if_not_exists('SpotifyAudioRecorder', logFormat=LOG_FORMAT)
LOGGER = DefaultLogger().create_logger_if_not_exists('AudioRecorder', logFormat=LOG_FORMAT)
class SpotifyAudioRecorder:
class AudioRecorder:
_CHUNK_SIZE = 512
def __init__(self, deviceName: str, destinationFilePath: str):
......@@ -59,7 +59,7 @@ class SpotifyAudioRecorder:
if __name__ == '__main__':
recorder = SpotifyAudioRecorder(
recorder = AudioRecorder(
deviceName='3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]',
destinationFilePath='C:/Users/RobertG/Desktop/output.wav'
)
......
import re
import subprocess
from TheCodeLabs_BaseUtils.DefaultLogger import DefaultLogger
from mutagen.easyid3 import EasyID3
LOG_FORMAT = '[%(levelname)-7s] - %(asctime)s - %(message)s'
LOGGER = DefaultLogger().create_logger_if_not_exists('Mp3Converter', logFormat=LOG_FORMAT)
class Mp3Converter:
_PATTERN_MAX_VOLUME = re.compile(r'max_volume: ([-.\d+]*)\s')
@staticmethod
def convert_to_mp3(inputFilePath: str, outputFilePath: str) -> None:
output = subprocess.check_output(['ffmpeg',
'-i', inputFilePath,
'-af', 'volumedetect',
'-vn', # ignore video streams
'-sn', # ignore subtitle streams
'-sn', # ignore data streams
'-hide_banner', # suppress unnecessary log messages
'-f', 'null',
'NULL'],
stderr=subprocess.STDOUT)
output = output.decode('utf-8')
matches = re.findall(Mp3Converter._PATTERN_MAX_VOLUME, output)
detectedMaxVolume = None
if matches:
detectedMaxVolume = float(matches[0])
LOGGER.debug(f'\t\tDetected max volume: {detectedMaxVolume}dB')
arguments = ['ffmpeg',
'-i', inputFilePath,
'-y', # overwrite if already exists
'-hide_banner', # suppress unnecessary log messages
'-loglevel', 'error',
'-vn', # ignore video streams
'-sn', # ignore subtitle streams
'-sn', # ignore data streams
'-ac', '2', # 2 channels
'-b:a', '320k'] # 320k bitrate
if detectedMaxVolume is not None and detectedMaxVolume < 0:
arguments.extend(['-filter:a', f'volume={abs(detectedMaxVolume)}dB']) # normalizes volume
arguments.append(outputFilePath)
LOGGER.info(f'\t\tRunning {" ".join(arguments)}')
subprocess.check_call(arguments)
@staticmethod
def set_mp3_tags(filePath: str, title: str, artist: str, album: str) -> None:
LOGGER.info(f'\t\tSetting mp3 tags for "{filePath}"')
audio = EasyID3(filePath)
audio['title'] = title
audio['artist'] = artist
audio['album'] = album
audio.save()
if __name__ == '__main__':
Mp3Converter.convert_to_mp3('C:/Programmierung/SpotifyBackup/recorder/1 - Bignic - tetrapod.wav',
'C:/Programmierung/SpotifyBackup/recorder/a.mp3')
Mp3Converter.set_mp3_tags('C:/Programmierung/SpotifyBackup/recorder/a.mp3',
title='Fancy Song',
artist='Various Artists',
album='Album #1')
......@@ -7,7 +7,8 @@ import spotipy
from TheCodeLabs_BaseUtils.DefaultLogger import DefaultLogger
from spotipy import SpotifyOAuth, CacheFileHandler
from SpotifyAudioRecorder import SpotifyAudioRecorder
from AudioRecorder import AudioRecorder
from Mp3Converter import Mp3Converter
LOG_FORMAT = '[%(levelname)-7s] - %(asctime)s - %(message)s'
LOGGER = DefaultLogger().create_logger_if_not_exists('SpotifyRecorder', logFormat=LOG_FORMAT)
......@@ -106,23 +107,39 @@ class SpotifyRecorder:
skippedTrackNumbers.append(indexInPlaylist)
continue
LOGGER.info(
f'>>> Recording track {index + 1}/{len(tracks)}: #{indexInPlaylist} "{track["track"]["name"]}"...')
LOGGER.info(f'>>> Recording track {index + 1}/{len(tracks)}: '
f'#{indexInPlaylist} "{track["track"]["name"]}"...')
try:
self.__stop_playback_if_playing(deviceId)
filePath = self.__determine_file_path(indexInPlaylist + 1, track)
recorder = SpotifyAudioRecorder(self._audioDeviceName, filePath)
fileName = self.__determine_file_name(indexInPlaylist, track)
filePathWav = f'{fileName}.wav'
filePathMp3 = f'{fileName}.mp3'
recorder = AudioRecorder(self._audioDeviceName, filePathWav)
with recorder.record():
self.__play_track(deviceId, track['track']['uri'])
timeWaitedForPlaying = self.__wait_for_track_playing(track['track']['id'])
self.__wait_for_track_end(track, timeWaitedForPlaying)
Mp3Converter.convert_to_mp3(filePathWav, filePathMp3)
Mp3Converter.set_mp3_tags(filePath=filePathMp3,
title=track['track']['name'],
artist=self.__join_artists(track),
album=track['track']['album']['name'])
recordedTrackNumbers.append(indexInPlaylist)
except Exception as e:
LOGGER.error(f'An error occurred while recording track "{track["track"]["name"]}"', exc_info=e)
errorTrackNumbers.append(indexInPlaylist)
self.__print_end_statistics(errorTrackNumbers, recordedTrackNumbers, skippedTrackNumbers, len(tracks))
def __join_artists(self, track) -> str:
artists = track['track']['artists']
return ' & '.join(artist['name'] for artist in artists)
def __print_end_statistics(self, errorTrackNumbers: list[int], recordedTrackNumbers: list[int],
skippedTrackNumbers: list[int], numberOfTracks: int):
LOGGER.info('### DONE ###')
LOGGER.info('>>> Skipped <<<')
......@@ -133,13 +150,12 @@ class SpotifyRecorder:
for number in errorTrackNumbers:
LOGGER.info(f'Error #{number}')
LOGGER.info(
f'### {len(tracks)} tracks, {len(recordedTrackNumbers)} recorded, {len(skippedTrackNumbers)} skipped, {len(errorTrackNumbers)} errors ###')
LOGGER.info(f'### {numberOfTracks} tracks, {len(recordedTrackNumbers)} recorded, '
f'{len(skippedTrackNumbers)} skipped, {len(errorTrackNumbers)} errors ###')
def __determine_file_path(self, index: int, track) -> str:
artists = track['track']['artists']
artists = ' & '.join(artist['name'] for artist in artists)
fileName = f'{index} - {artists} - {track["track"]["name"]}.wav'
def __determine_file_name(self, index: int, track) -> str:
artists = self.__join_artists(track)
fileName = f'{index} - {artists} - {track["track"]["name"]}'
return os.path.join(self._destinationFolder, fileName)
def __wait_for_track_end(self, track, timeWaitedForPlaying: float) -> None:
......@@ -197,7 +213,11 @@ class SpotifyRecorder:
self._spotify.start_playback(device_id=deviceId, uris=[trackUri])
def __stop_playback_if_playing(self, deviceId: str) -> None:
if self._spotify.current_playback()['is_playing']:
currentPlayback = self._spotify.current_playback()
if currentPlayback is None:
return
if currentPlayback['is_playing']:
self._spotify.pause_playback(device_id=deviceId)
def __wait_for_track_playing(self, expectedTrackId: str) -> float:
......@@ -212,7 +232,8 @@ class SpotifyRecorder:
LOGGER.debug(f'\t\tTrack started playing after {duration:.1f}s')
break
else:
raise RuntimeError(f'Wrong track started playing (actual: {currentPlayback["item"]["id"]}, expected: {expectedTrackId})')
raise RuntimeError(
f'Wrong track started playing (actual: {currentPlayback["item"]["id"]}, expected: {expectedTrackId})')
time.sleep(1)
......
......@@ -132,6 +132,17 @@ files = [
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "mutagen"
version = "1.47.0"
description = "read and write audio tags for many formats"
optional = false
python-versions = ">=3.7"
files = [
{file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"},
{file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"},
]
[[package]]
name = "psutil"
version = "5.9.7"
......@@ -341,4 +352,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "a10c10003642c988271028a8a2a44227cae6123d1e05e4a66f3cb2dbab5a655a"
content-hash = "bb77de02b25207bf10249bf1370a69682b27ad88452311d42abc7d9830eb5494"
......@@ -14,6 +14,7 @@ python = "^3.11"
thecodelabs-baseutils = {version = "*", source = "TheCodeLabs" }
spotipy = "2.22.0"
PyAudioWPatch = "0.2.12.6"
mutagen = "1.47.0"
[tool.poetry.dev-dependencies]
......