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
  • master
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
8 results
Show changes
...@@ -26,7 +26,9 @@ class SpotifyRecorder: ...@@ -26,7 +26,9 @@ class SpotifyRecorder:
playlist: Dict[str, str], playlist: Dict[str, str],
spotifyDeviceName: str, spotifyDeviceName: str,
audioDeviceName: str, audioDeviceName: str,
destinationFolder: str): destinationFolder: str,
startNumber: int = 0,
limit: int = -1):
self._clientID = clientID self._clientID = clientID
self._clientSecret = clientSecret self._clientSecret = clientSecret
self._redirectUrl = redirectUrl self._redirectUrl = redirectUrl
...@@ -36,11 +38,11 @@ class SpotifyRecorder: ...@@ -36,11 +38,11 @@ class SpotifyRecorder:
self._spotifyDeviceName = spotifyDeviceName self._spotifyDeviceName = spotifyDeviceName
self._audioDeviceName = audioDeviceName self._audioDeviceName = audioDeviceName
self._destinationFolder = destinationFolder self._destinationFolder = destinationFolder
self._startNumber = startNumber
self._limit = limit
os.makedirs(self._destinationFolder, exist_ok=True) os.makedirs(self._destinationFolder, exist_ok=True)
# TODO: options specify range (start / stop track) or only offset index + limit
self._spotify = self.login() self._spotify = self.login()
def login(self) -> spotipy.Spotify: def login(self) -> spotipy.Spotify:
...@@ -53,16 +55,14 @@ class SpotifyRecorder: ...@@ -53,16 +55,14 @@ class SpotifyRecorder:
return spotipy.Spotify(client_credentials_manager=client_credentials_manager) return spotipy.Spotify(client_credentials_manager=client_credentials_manager)
def run(self): def run(self):
LOGGER.info(f'>>> Fetching all tracks for playlist {self._playlist["name"]}...') LOGGER.info(f'Fetching all tracks for playlist {self._playlist["name"]}...')
allTracks = []
playlist = self.__get_playlist(self._playlist['user'], self._playlist['id']) playlist = self.__get_playlist(self._playlist['user'], self._playlist['id'])
allTracks.extend(self.__get_tracks(playlist)) tracks = self.__get_tracks(playlist)
LOGGER.info(f'>>> Found {len(allTracks)} tracks')
self.__record_tracks(allTracks) self.__record_tracks(tracks)
def __get_playlist(self, username: str, playlistID: str) -> Dict: def __get_playlist(self, username: str, playlistID: str) -> Dict:
LOGGER.info(f'>>> Fetching playlist with ID: {playlistID} by {username}...') LOGGER.info(f'Fetching playlist with ID: {playlistID} by {username}...')
identifier = f'spotify:user:{username}:playlist:{playlistID}' identifier = f'spotify:user:{username}:playlist:{playlistID}'
playlist = self._spotify.playlist(identifier) playlist = self._spotify.playlist(identifier)
LOGGER.info(f'Found playlist "{playlist["name"]}"') LOGGER.info(f'Found playlist "{playlist["name"]}"')
...@@ -76,7 +76,7 @@ class SpotifyRecorder: ...@@ -76,7 +76,7 @@ class SpotifyRecorder:
tracks = self._spotify.next(tracks) tracks = self._spotify.next(tracks)
results.extend(tracks['items']) results.extend(tracks['items'])
LOGGER.info(f'Fetched {len(results)} tracks') LOGGER.info(f'Found {len(tracks)} tracks in playlist')
return results return results
def __extract_track_uris(self, tracks: List) -> List[str]: def __extract_track_uris(self, tracks: List) -> List[str]:
...@@ -85,34 +85,56 @@ class SpotifyRecorder: ...@@ -85,34 +85,56 @@ class SpotifyRecorder:
def __record_tracks(self, tracks: list): def __record_tracks(self, tracks: list):
deviceId = self.__get_device_id_by_name(self._spotifyDeviceName) deviceId = self.__get_device_id_by_name(self._spotifyDeviceName)
recordedTracks = [] recordedTrackNumbers = []
skippedTracks = [] skippedTrackNumbers = []
errorTracks = [] errorTrackNumbers = []
for index, track in enumerate(tracks[:2]):
if self._limit == -1:
LOGGER.info(f'Recording track #{self._startNumber} to all')
tracks = tracks[self._startNumber - 1:]
else:
LOGGER.info(f'Recording track #{self._startNumber} to (including) #{self._startNumber + self._limit - 1}')
tracks = tracks[self._startNumber - 1:self._startNumber - 1 + self._limit]
for index, track in enumerate(tracks):
indexInPlaylist = self._startNumber + index
if track['is_local']: if track['is_local']:
# TODO:
# It's not possible to add a local track to a playlist using the web API. # It's not possible to add a local track to a playlist using the web API.
# https://github.com/plamere/spotipy/issues/793#issuecomment-1082421408 # https://github.com/plamere/spotipy/issues/793#issuecomment-1082421408
LOGGER.info(f'Skipping local track "{track["track"]["name"]}"') LOGGER.info(f'Skipping local track "{track["track"]["name"]}"')
skippedTracks.append(track['track']['name']) skippedTrackNumbers.append(indexInPlaylist)
continue continue
LOGGER.info(f'Recording track {index + 1}/{len(tracks)}: "{track["track"]["name"]}"...') LOGGER.info(
f'>>> Recording track {index + 1}/{len(tracks)}: #{indexInPlaylist} "{track["track"]["name"]}"...')
try: try:
filePath = self.__determine_file_path(index + 1, track) self.__stop_playback_if_playing(deviceId)
filePath = self.__determine_file_path(indexInPlaylist + 1, track)
recorder = SpotifyAudioRecorder(self._audioDeviceName, filePath) recorder = SpotifyAudioRecorder(self._audioDeviceName, filePath)
with recorder.record(): with recorder.record():
self.__play_track(deviceId, track['track']['uri']) self.__play_track(deviceId, track['track']['uri'])
self.__wait_for_track_playing() timeWaitedForPlaying = self.__wait_for_track_playing(track['track']['id'])
self.__wait_for_track_end(track) self.__wait_for_track_end(track, timeWaitedForPlaying)
recordedTracks.append(track['track']['name']) recordedTrackNumbers.append(indexInPlaylist)
except Exception as e: except Exception as e:
LOGGER.error(f'An error occurred while recording track "{track["track"]["name"]}"', exc_info=e) LOGGER.error(f'An error occurred while recording track "{track["track"]["name"]}"', exc_info=e)
errorTracks.append(track['track']['name']) errorTrackNumbers.append(indexInPlaylist)
LOGGER.info('### DONE ###') LOGGER.info('### DONE ###')
LOGGER.info(f'{len(tracks)} tracks, {len(recordedTracks)} recorded, {len(skippedTracks)} skipped, {len(errorTracks)} errors')
LOGGER.info('>>> Skipped <<<')
for number in skippedTrackNumbers:
LOGGER.info(f'Skipped #{number}')
LOGGER.info('>>> Errors <<<')
for number in errorTrackNumbers:
LOGGER.info(f'Error #{number}')
LOGGER.info(
f'### {len(tracks)} tracks, {len(recordedTrackNumbers)} recorded, {len(skippedTrackNumbers)} skipped, {len(errorTrackNumbers)} errors ###')
def __determine_file_path(self, index: int, track) -> str: def __determine_file_path(self, index: int, track) -> str:
artists = track['track']['artists'] artists = track['track']['artists']
...@@ -120,10 +142,10 @@ class SpotifyRecorder: ...@@ -120,10 +142,10 @@ class SpotifyRecorder:
fileName = f'{index} - {artists} - {track["track"]["name"]}.wav' fileName = f'{index} - {artists} - {track["track"]["name"]}.wav'
return os.path.join(self._destinationFolder, fileName) return os.path.join(self._destinationFolder, fileName)
def __wait_for_track_end(self, track): def __wait_for_track_end(self, track, timeWaitedForPlaying: float) -> None:
trackDurationInMs = track['track']['duration_ms'] trackDurationInMs = track['track']['duration_ms']
trackDurationInSeconds = trackDurationInMs // 1000 trackDurationInSeconds = trackDurationInMs // 1000
LOGGER.info(f'Track duration: {self.__convert_seconds_to_duration(trackDurationInSeconds)}') LOGGER.info(f'\tTrack duration: {self.__convert_seconds_to_duration(trackDurationInSeconds)}')
startTime = time.time() startTime = time.time()
...@@ -138,21 +160,22 @@ class SpotifyRecorder: ...@@ -138,21 +160,22 @@ class SpotifyRecorder:
if remainingTimeInMs > self._THRESHOLD_TRACK_END_IN_MS: if remainingTimeInMs > self._THRESHOLD_TRACK_END_IN_MS:
sleepTime = remainingTimeInSeconds / 2 sleepTime = remainingTimeInSeconds / 2
LOGGER.debug(f'Waiting for track to end (remaining: ' LOGGER.debug(f'\t\tWaiting for track to end (remaining: '
f'{self.__convert_seconds_to_duration(remainingTimeInSeconds)}, ' f'{self.__convert_seconds_to_duration(remainingTimeInSeconds)}, '
f'sleep: {self.__convert_seconds_to_duration(sleepTime)}s)...') f'sleep: {self.__convert_seconds_to_duration(int(sleepTime))}s)...')
time.sleep(sleepTime) time.sleep(sleepTime)
else: else:
waitDuration = int(time.time() - startTime) waitDuration = int(time.time() - startTime)
waitDuration += timeWaitedForPlaying
if waitDuration < trackDurationInSeconds: if waitDuration < trackDurationInSeconds - self._WAIT_TIME_TRACK_PLAYING_SHORT_IN_S:
raise RuntimeError( raise RuntimeError(
f'Track finished too early (waited: {waitDuration}s, expected: {trackDurationInSeconds}s)') f'Track finished too early (waited: {waitDuration}s, expected: {trackDurationInSeconds}s)')
if waitDuration > trackDurationInSeconds + self._WAIT_TIME_TRACK_PLAYING_SHORT_IN_S * 3: if waitDuration > trackDurationInSeconds + self._WAIT_TIME_TRACK_PLAYING_SHORT_IN_S * 3:
raise RuntimeError( raise RuntimeError(
f'Track finished too late (waited: {waitDuration}s, expected: {trackDurationInSeconds})') f'Track finished too late (waited: {waitDuration}s, expected: {trackDurationInSeconds})')
LOGGER.debug(f'Track finished. Waited {waitDuration}s, expected {trackDurationInSeconds}s, OK') LOGGER.info(f'Track finished. Waited {waitDuration}s, expected {trackDurationInSeconds}s, OK')
break break
@staticmethod @staticmethod
...@@ -170,19 +193,31 @@ class SpotifyRecorder: ...@@ -170,19 +193,31 @@ class SpotifyRecorder:
raise RuntimeError('Device not found') raise RuntimeError('Device not found')
def __play_track(self, deviceId: str, trackUri: str): def __play_track(self, deviceId: str, trackUri: str) -> None:
self._spotify.start_playback(device_id=deviceId, uris=[trackUri]) self._spotify.start_playback(device_id=deviceId, uris=[trackUri])
def __wait_for_track_playing(self) -> None: def __stop_playback_if_playing(self, deviceId: str) -> None:
LOGGER.debug(f'Wait for track to start playing...') if self._spotify.current_playback()['is_playing']:
self._spotify.pause_playback(device_id=deviceId)
def __wait_for_track_playing(self, expectedTrackId: str) -> float:
LOGGER.debug(f'\t\tWait for track to start playing...')
startTime = time.time() startTime = time.time()
duration = 0
while time.time() - startTime < self._MAX_WAIT_TIME_TRACK_STARTING_IN_S: while time.time() - startTime < self._MAX_WAIT_TIME_TRACK_STARTING_IN_S:
if self._spotify.current_playback()['is_playing']: currentPlayback = self._spotify.current_playback()
LOGGER.debug(f'Track started playing after {time.time() - startTime:.1f}s') if currentPlayback['is_playing']:
duration = time.time() - startTime
if currentPlayback['item']['id'] == expectedTrackId:
LOGGER.debug(f'\t\tTrack started playing after {duration:.1f}s')
break break
else:
raise RuntimeError(f'Wrong track started playing (actual: {currentPlayback["item"]["id"]}, expected: {expectedTrackId})')
time.sleep(1) time.sleep(1)
return duration
if __name__ == '__main__': if __name__ == '__main__':
with open('config/settings-recorder.json', 'r', encoding='utf-8') as f: with open('config/settings-recorder.json', 'r', encoding='utf-8') as f:
...@@ -196,7 +231,9 @@ if __name__ == '__main__': ...@@ -196,7 +231,9 @@ if __name__ == '__main__':
SETTINGS['playlist'], SETTINGS['playlist'],
SETTINGS['spotifyDeviceName'], SETTINGS['spotifyDeviceName'],
SETTINGS['audioDeviceName'], SETTINGS['audioDeviceName'],
SETTINGS['destinationFolder']) SETTINGS['destinationFolder'],
SETTINGS['startNumber'],
SETTINGS['limit'], )
spotifyBackup.run() spotifyBackup.run()
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
"spotifyDeviceName": "MYDEVICE", "spotifyDeviceName": "MYDEVICE",
"audioDeviceName": "3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]", "audioDeviceName": "3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]",
"destinationFolder": "", "destinationFolder": "",
"startNumber": 1,
"limit": -1,
"redirectUrl": "http://localhost:8080", "redirectUrl": "http://localhost:8080",
"openBrowser": false, "openBrowser": false,
"cacheFilePath": ".cache" "cacheFilePath": ".cache"
......