From 383e2078fd963972815542ed3f7ebea1e79bb200 Mon Sep 17 00:00:00 2001 From: Timothy Rae Date: Sat, 10 Dec 2022 13:55:30 +1300 Subject: [PATCH] Update to tidalapi v0.7 + other minor improvements * Speed improvements erasing playlists * Fix progress bar when erasing playlists * Potentially more useful error logs from repeat_on_exception --- auth.py | 7 ++-- requirements.txt | 3 +- sync.py | 36 +++++++++--------- tidalapi_patch.py | 93 +++++++++++------------------------------------ 4 files changed, 45 insertions(+), 94 deletions(-) diff --git a/auth.py b/auth.py index 178e9a9..bda3eb1 100644 --- a/auth.py +++ b/auth.py @@ -32,10 +32,9 @@ def open_tidal_session(config = None): session = tidalapi.Session() if previous_session: try: - if session.load_oauth_session(previous_session['session_id'], - previous_session['token_type'], - previous_session['access_token'], - previous_session['refresh_token'] ): + if session.load_oauth_session(token_type= previous_session['token_type'], + access_token=previous_session['access_token'], + refresh_token=previous_session['refresh_token'] ): return session except Exception as e: print("Error loading previous Tidal Session: \n" + str(e) ) diff --git a/requirements.txt b/requirements.txt index 4deec3a..db6a31b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ spotipy==2.19.0 -tidalapi==0.6.10 +requests>=2.28.1 # for tidalapi +tidalapi==0.7.0 pyyaml==5.3.1 tqdm==4.45.0 diff --git a/sync.py b/sync.py index ed7c497..9820098 100755 --- a/sync.py +++ b/sync.py @@ -7,11 +7,11 @@ from multiprocessing import Pool import sys import spotipy import tidalapi -from tidalapi_patch import create_tidal_playlist, set_tidal_playlist +from tidalapi_patch import set_tidal_playlist import time from tqdm import tqdm +import traceback import unicodedata -import webbrowser import yaml def normalize(s): @@ -83,21 +83,21 @@ def tidal_search(spotify_track_and_cache, tidal_session): if cached_tidal_track: return cached_tidal_track # search for album name and first album artist if 'album' in spotify_track and 'artists' in spotify_track['album'] and len(spotify_track['album']['artists']): - album_result = tidal_session.search('album', simple(spotify_track['album']['name']) + " " + simple(spotify_track['album']['artists'][0]['name'])) - for album in album_result.albums: - album_tracks = tidal_session.get_album_tracks(album.id) + album_result = tidal_session.search(simple(spotify_track['album']['name']) + " " + simple(spotify_track['album']['artists'][0]['name']), models=[tidalapi.album.Album]) + for album in album_result['albums']: + album_tracks = tidal_session.album(album.id).tracks() if len(album_tracks) >= spotify_track['track_number']: track = album_tracks[spotify_track['track_number'] - 1] if match(track, spotify_track): return track # if that fails then search for track name and first artist - for track in tidal_session.search('track', simple(spotify_track['name']) + ' ' + simple(spotify_track['artists'][0]['name'])).tracks: + for track in tidal_session.search(simple(spotify_track['name']) + ' ' + simple(spotify_track['artists'][0]['name']), models=[tidalapi.media.Track])['tracks']: if match(track, spotify_track): return track def get_tidal_playlists_dict(tidal_session): # a dictionary of name --> playlist - tidal_playlists = tidal_session.get_user_playlists(tidal_session.user.id) + tidal_playlists = tidal_session.user.playlists() output = {} for playlist in tidal_playlists: output[playlist.name] = playlist @@ -107,13 +107,13 @@ def repeat_on_exception(function, *args, remaining=5, **kwargs): # utility to repeat calling the function up to 5 times if an exception is thrown try: return function(*args, **kwargs) - except: + except Exception as e: if remaining: - print("Error, retrying {} more times".format(remaining)) + print(f"{type(e).__name__} occurred. Retrying {remaining} more times.\n\n{traceback.format_exc()}") else: - print("Repeated error calling the function '{}' with the following arguments:".format(function.__name__)) + print(f"Repeated error {type(e).__name__} occurred and could not be recovered\n\n The following arguments were provided:") print(args) - raise + sys.exit(1) time.sleep(5) return repeat_on_exception(function, *args, remaining=remaining-1, **kwargs) @@ -143,7 +143,7 @@ def get_tracks_from_spotify_playlist(spotify_session, spotify_playlist): class TidalPlaylistCache: def __init__(self, playlist, tidal_session): - self._data = tidal_session.get_playlist_tracks(playlist.id) + self._data = playlist.tracks() def _search(self, spotify_track): ''' check if the given spotify track was already in the tidal playlist.''' @@ -169,7 +169,7 @@ class TidalPlaylistCache: return (results, cache_hits) def tidal_playlist_is_dirty(tidal_session, playlist_id, new_track_ids): - old_tracks = tidal_session.get_playlist_tracks(playlist_id) + old_tracks = tidal_session.playlist(playlist_id).tracks() if len(old_tracks) != len(new_track_ids): return True for i in range(len(old_tracks)): @@ -188,14 +188,15 @@ def sync_playlist(spotify_session, tidal_session, spotify_id, tidal_id, config): if tidal_id: # if a Tidal playlist was specified then look it up try: - tidal_playlist = tidal_session.get_playlist(tidal_id) - except exception: + tidal_playlist = tidal_session.playlist(tidal_id) + except Exception as e: print("Error getting Tidal playlist " + tidal_id) print(e) return else: # create a new Tidal playlist if required - tidal_playlist = create_tidal_playlist(tidal_session, spotify_playlist['name']) + print(f"No playlist found on Tidal corresponding to Spotify playlist: '{spotify_playlist['name']}', creating new playlist") + tidal_playlist = tidal_session.user.create_playlist(spotify_playlist['name'], spotify_playlist['description']) tidal_track_ids = [] spotify_tracks, cache_hits = TidalPlaylistCache(tidal_playlist, tidal_session).search(spotify_session, spotify_playlist) if cache_hits == len(spotify_tracks): @@ -213,7 +214,7 @@ def sync_playlist(spotify_session, tidal_session, spotify_id, tidal_id, config): print(color[0] + "Could not find track {}: {} - {}".format(spotify_track['id'], ",".join([a['name'] for a in spotify_track['artists']]), spotify_track['name']) + color[1]) if tidal_playlist_is_dirty(tidal_session, tidal_playlist.id, tidal_track_ids): - set_tidal_playlist(tidal_session, tidal_playlist.id, tidal_track_ids) + set_tidal_playlist(tidal_playlist, tidal_track_ids) else: print("No changes to write to Tidal playlist") @@ -231,7 +232,6 @@ def pick_tidal_playlist_for_spotify_playlist(spotify_playlist, tidal_playlists): tidal_playlist = tidal_playlists[spotify_playlist['name']] return (spotify_playlist['id'], tidal_playlist.id) else: - print(f"No playlist found on Tidal corresponding to Spotify playlist: '{spotify_playlist['name']}', creating new playlist") return (spotify_playlist['id'], None) diff --git a/tidalapi_patch.py b/tidalapi_patch.py index 68079bc..24712cd 100644 --- a/tidalapi_patch.py +++ b/tidalapi_patch.py @@ -1,76 +1,27 @@ -import tidalapi from tqdm import tqdm -tidalapi_parse_album = tidalapi._parse_album +def _remove_indices_from_playlist(playlist, indices): + headers = {'If-None-Match': playlist._etag} + index_string = ",".join(map(str, indices)) + playlist.requests.request('DELETE', (playlist._base_url + '/items/%s') % (playlist.id, index_string), headers=headers) + playlist._reparse() - -def patch(): - tidalapi._parse_album = _parse_album - tidalapi.models.Album.picture = picture - - -def _parse_album(json_obj, artist=None, artists=None): - obj = tidalapi_parse_album(json_obj, artist, artists) - image_id = "" - if json_obj.get("cover"): - image_id = json_obj.get("cover") - - obj.__dict__.update(image_id=image_id) - return obj - - -def picture(obj, width, height): - return "https://resources.tidal.com/images/{image_id}/{width}x{height}.jpg".format( - image_id=obj.image_id.replace("-", "/"), width=width, height=height - ) - -def set_tidal_playlist(session, playlist_id, track_ids): - # erases any items in the given playlist, then adds all of the tracks given in track_ids - # had to hack this together because the API doesn't include it - - chunk_size = 20 # add/delete tracks in chunks of no more than this many tracks - request_params = { - 'sessionId': session.session_id, - 'countryCode': session.country_code, - 'limit': '999', - } - def get_headers(): - etag = session.request('GET','playlists/%s/tracks' % playlist_id).headers['ETag'] - return {'if-none-match' : etag} - - # clear all old items from playlist - playlist = session.get_playlist(playlist_id) - progress = tqdm(desc="Erasing existing tracks from Tidal playlist", total=playlist.num_tracks) - while True: - if not playlist.num_tracks: - break - track_index_string = ",".join([str(x) for x in range(min(chunk_size, playlist.num_tracks))]) - result = session.request('DELETE', 'playlists/{}/tracks/{}'.format(playlist.id, track_index_string), params=request_params, headers=get_headers()) - result.raise_for_status() - progress.update(min(chunk_size, playlist.num_tracks)) - playlist = session.get_playlist(playlist_id) - progress.close() - - # add all new items to the playlist +def clear_tidal_playlist(playlist, chunk_size=20): + with tqdm(desc="Erasing existing tracks from Tidal playlist", total=playlist.num_tracks) as progress: + while playlist.num_tracks: + indices = range(min(playlist.num_tracks, chunk_size)) + _remove_indices_from_playlist(playlist, indices) + progress.update(len(indices)) + +def add_multiple_tracks_to_playlist(playlist, track_ids, chunk_size=20): offset = 0 - progress = tqdm(desc="Adding new tracks to Tidal playlist", total=len(track_ids)) - while offset < len(track_ids): - count = min(chunk_size, len(track_ids) - offset) - data = { - 'trackIds' : ",".join([str(x) for x in track_ids[offset:offset+chunk_size]]), - 'toIndex' : offset - } - offset += count - result = session.request('POST', 'playlists/{}/tracks'.format(playlist.id), params=request_params, data=data, headers=get_headers()) - result.raise_for_status() - progress.update(count) - progress.close() + with tqdm(desc="Adding new tracks to Tidal playlist", total=len(track_ids)) as progress: + while offset < len(track_ids): + count = min(chunk_size, len(track_ids) - offset) + playlist.add(track_ids[offset:offset+chunk_size]) + offset += count + progress.update(count) -def create_tidal_playlist(session, name): - result = session.request('POST','users/%s/playlists' % session.user.id ,data={'title': name}) - return session.get_playlist(result.json()['uuid']) - -def delete_tidal_playlist(session, playlist): - etag = session.request('GET','playlists/%s' % playlist.id).headers['ETag'] - headers = {'if-none-match' : etag} - session.request('DELETE','playlists/%s' % playlist.id, headers=headers) +def set_tidal_playlist(playlist, track_ids): + clear_tidal_playlist(playlist) + add_multiple_tracks_to_playlist(playlist, track_ids)