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
  • master
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
8 results

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:
playlist: Dict[str, str],
spotifyDeviceName: str,
audioDeviceName: str,
destinationFolder: str):
destinationFolder: str,
startNumber: int = 0,
limit: int = -1):
self._clientID = clientID
self._clientSecret = clientSecret
self._redirectUrl = redirectUrl
......@@ -36,11 +38,11 @@ class SpotifyRecorder:
self._spotifyDeviceName = spotifyDeviceName
self._audioDeviceName = audioDeviceName
self._destinationFolder = destinationFolder
self._startNumber = startNumber
self._limit = limit
os.makedirs(self._destinationFolder, exist_ok=True)
# TODO: options specify range (start / stop track) or only offset index + limit
self._spotify = self.login()
def login(self) -> spotipy.Spotify:
......@@ -53,16 +55,14 @@ class SpotifyRecorder:
return spotipy.Spotify(client_credentials_manager=client_credentials_manager)
def run(self):
LOGGER.info(f'>>> Fetching all tracks for playlist {self._playlist["name"]}...')
allTracks = []
LOGGER.info(f'Fetching all tracks for playlist {self._playlist["name"]}...')
playlist = self.__get_playlist(self._playlist['user'], self._playlist['id'])
allTracks.extend(self.__get_tracks(playlist))
LOGGER.info(f'>>> Found {len(allTracks)} tracks')
tracks = self.__get_tracks(playlist)
self.__record_tracks(allTracks)
self.__record_tracks(tracks)
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}'
playlist = self._spotify.playlist(identifier)
LOGGER.info(f'Found playlist "{playlist["name"]}"')
......@@ -76,7 +76,7 @@ class SpotifyRecorder:
tracks = self._spotify.next(tracks)
results.extend(tracks['items'])
LOGGER.info(f'Fetched {len(results)} tracks')
LOGGER.info(f'Found {len(tracks)} tracks in playlist')
return results
def __extract_track_uris(self, tracks: List) -> List[str]:
......@@ -85,34 +85,56 @@ class SpotifyRecorder:
def __record_tracks(self, tracks: list):
deviceId = self.__get_device_id_by_name(self._spotifyDeviceName)
recordedTracks = []
skippedTracks = []
errorTracks = []
for index, track in enumerate(tracks[:2]):
recordedTrackNumbers = []
skippedTrackNumbers = []
errorTrackNumbers = []
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']:
# TODO:
# 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
LOGGER.info(f'Skipping local track "{track["track"]["name"]}"')
skippedTracks.append(track['track']['name'])
skippedTrackNumbers.append(indexInPlaylist)
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:
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)
with recorder.record():
self.__play_track(deviceId, track['track']['uri'])
self.__wait_for_track_playing()
self.__wait_for_track_end(track)
timeWaitedForPlaying = self.__wait_for_track_playing(track['track']['id'])
self.__wait_for_track_end(track, timeWaitedForPlaying)
recordedTracks.append(track['track']['name'])
recordedTrackNumbers.append(indexInPlaylist)
except Exception as 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(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:
artists = track['track']['artists']
......@@ -120,10 +142,10 @@ class SpotifyRecorder:
fileName = f'{index} - {artists} - {track["track"]["name"]}.wav'
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']
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()
......@@ -138,21 +160,22 @@ class SpotifyRecorder:
if remainingTimeInMs > self._THRESHOLD_TRACK_END_IN_MS:
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'sleep: {self.__convert_seconds_to_duration(sleepTime)}s)...')
f'sleep: {self.__convert_seconds_to_duration(int(sleepTime))}s)...')
time.sleep(sleepTime)
else:
waitDuration = int(time.time() - startTime)
waitDuration += timeWaitedForPlaying
if waitDuration < trackDurationInSeconds:
if waitDuration < trackDurationInSeconds - self._WAIT_TIME_TRACK_PLAYING_SHORT_IN_S:
raise RuntimeError(
f'Track finished too early (waited: {waitDuration}s, expected: {trackDurationInSeconds}s)')
if waitDuration > trackDurationInSeconds + self._WAIT_TIME_TRACK_PLAYING_SHORT_IN_S * 3:
raise RuntimeError(
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
@staticmethod
......@@ -170,19 +193,31 @@ class SpotifyRecorder:
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])
def __wait_for_track_playing(self) -> None:
LOGGER.debug(f'Wait for track to start playing...')
def __stop_playback_if_playing(self, deviceId: str) -> None:
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()
duration = 0
while time.time() - startTime < self._MAX_WAIT_TIME_TRACK_STARTING_IN_S:
if self._spotify.current_playback()['is_playing']:
LOGGER.debug(f'Track started playing after {time.time() - startTime:.1f}s')
currentPlayback = self._spotify.current_playback()
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
else:
raise RuntimeError(f'Wrong track started playing (actual: {currentPlayback["item"]["id"]}, expected: {expectedTrackId})')
time.sleep(1)
return duration
if __name__ == '__main__':
with open('config/settings-recorder.json', 'r', encoding='utf-8') as f:
......@@ -196,7 +231,9 @@ if __name__ == '__main__':
SETTINGS['playlist'],
SETTINGS['spotifyDeviceName'],
SETTINGS['audioDeviceName'],
SETTINGS['destinationFolder'])
SETTINGS['destinationFolder'],
SETTINGS['startNumber'],
SETTINGS['limit'], )
spotifyBackup.run()
......
......@@ -10,6 +10,8 @@
"spotifyDeviceName": "MYDEVICE",
"audioDeviceName": "3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]",
"destinationFolder": "",
"startNumber": 1,
"limit": -1,
"redirectUrl": "http://localhost:8080",
"openBrowser": false,
"cacheFilePath": ".cache"
......