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
This commit is contained in:
7
auth.py
7
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) )
|
||||
|
||||
@@ -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
|
||||
|
||||
36
sync.py
36
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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user