|
|
|
|
@@ -2,6 +2,7 @@
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
from .cache import failure_cache, track_match_cache
|
|
|
|
|
import datetime
|
|
|
|
|
from functools import partial
|
|
|
|
|
from typing import List, Sequence, Set, Mapping
|
|
|
|
|
import math
|
|
|
|
|
@@ -85,6 +86,7 @@ def artist_match(tidal_track: tidalapi.Track, spotify_track) -> bool:
|
|
|
|
|
return get_tidal_artists(tidal_track, True).intersection(get_spotify_artists(spotify_track, True)) != set()
|
|
|
|
|
|
|
|
|
|
def match(tidal_track, spotify_track) -> bool:
|
|
|
|
|
if not spotify_track['id']: return False
|
|
|
|
|
return isrc_match(tidal_track, spotify_track) or (
|
|
|
|
|
duration_match(tidal_track, spotify_track)
|
|
|
|
|
and name_match(tidal_track, spotify_track)
|
|
|
|
|
@@ -154,7 +156,7 @@ async def repeat_on_request_error(function, *args, remaining=5, **kwargs):
|
|
|
|
|
time.sleep(sleep_schedule.get(remaining, 1))
|
|
|
|
|
return await repeat_on_request_error(function, *args, remaining=remaining-1, **kwargs)
|
|
|
|
|
|
|
|
|
|
async def get_tracks_from_spotify_playlist(spotify_session: spotipy.Spotify, spotify_playlist: t_spotify.SpotifyPlaylist):
|
|
|
|
|
async def get_tracks_from_spotify_playlist(spotify_session: spotipy.Spotify, spotify_playlist):
|
|
|
|
|
def _get_tracks_from_spotify_playlist(offset: int, spotify_session: spotipy.Spotify, playlist_id: str):
|
|
|
|
|
fields="next,total,limit,items(track(name,album(name,artists),artists,track_number,duration_ms,id,external_ids(isrc)))"
|
|
|
|
|
return spotify_session.playlist_tracks(playlist_id, fields, offset=offset)
|
|
|
|
|
@@ -214,6 +216,7 @@ def get_tracks_for_new_tidal_playlist(spotify_tracks: Sequence[t_spotify.Spotify
|
|
|
|
|
output = []
|
|
|
|
|
seen_tracks = set()
|
|
|
|
|
for spotify_track in spotify_tracks:
|
|
|
|
|
if not spotify_track['id']: continue
|
|
|
|
|
tidal_id = track_match_cache.get(spotify_track['id'])
|
|
|
|
|
if tidal_id and not tidal_id in seen_tracks:
|
|
|
|
|
output.append(tidal_id)
|
|
|
|
|
@@ -222,10 +225,16 @@ def get_tracks_for_new_tidal_playlist(spotify_tracks: Sequence[t_spotify.Spotify
|
|
|
|
|
|
|
|
|
|
async def sync_playlist(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, spotify_playlist, tidal_playlist: tidalapi.Playlist | None, config):
|
|
|
|
|
async def _run_rate_limiter(semaphore):
|
|
|
|
|
''' Leaky bucket algorithm for rate limiting. Periodically releases an item from semaphore at rate_limit'''
|
|
|
|
|
''' Leaky bucket algorithm for rate limiting. Periodically releases items from semaphore at rate_limit'''
|
|
|
|
|
_sleep_time = config.get('max_concurrency', 10)/config.get('rate_limit', 10)/4 # aim to sleep approx time to drain 1/4 of 'bucket'
|
|
|
|
|
t0 = datetime.datetime.now()
|
|
|
|
|
while True:
|
|
|
|
|
await asyncio.sleep(1/config.get('rate_limit', 12)) # sleep for min time between new function executions
|
|
|
|
|
semaphore.release() # leak one item from the 'bucket'
|
|
|
|
|
await asyncio.sleep(_sleep_time)
|
|
|
|
|
t = datetime.datetime.now()
|
|
|
|
|
dt = (t - t0).total_seconds()
|
|
|
|
|
new_items = round(config.get('rate_limit', 10)*dt)
|
|
|
|
|
t0 = t
|
|
|
|
|
[semaphore.release() for i in range(new_items)] # leak new_items from the 'bucket'
|
|
|
|
|
|
|
|
|
|
# Create a new Tidal playlist if required
|
|
|
|
|
if not tidal_playlist:
|
|
|
|
|
@@ -294,6 +303,7 @@ async def get_playlists_from_spotify(spotify_session: spotipy.Spotify, config):
|
|
|
|
|
print("Loading Spotify playlists")
|
|
|
|
|
results = spotify_session.user_playlists(config['spotify']['username'])
|
|
|
|
|
exclude_list = set([x.split(':')[-1] for x in config.get('excluded_playlists', [])])
|
|
|
|
|
playlists.extend([p for p in results['items'] if p['owner']['id'] == config['spotify']['username'] and not p['id'] in exclude_list])
|
|
|
|
|
|
|
|
|
|
# get all the remaining playlists in parallel
|
|
|
|
|
if results['next']:
|
|
|
|
|
|