6 Commits
1.0.0 ... 1.0.3

Author SHA1 Message Date
Tim Rae
457da1724f Bump version to 1.0.3 2024-10-14 01:25:58 +02:00
Tim Rae
4d7c3b0ef0 Filter out tracks which don't have valid album metadata
Apply a final sanity filter to tracklist to validate assumption in matching
algorithm that the album has certain fields available.

In most situations this filter is not necessary, but occasionally we do
seem to encounter tracks that have no album metadata
2024-10-14 01:25:48 +02:00
Tim Rae
4fb702d008 Filter out podcast episodes (#83)
* Filter out podcast episodes from playlists
* Don't create empty playlists
* Bump version 1.0.2

Fixes #30
2024-09-28 08:34:58 +02:00
Tim Rae
693dcd110f Update version to 1.0.1 2024-09-12 16:59:45 +02:00
Tim Rae
17d60c019d Use user id from spotify session instead of config file
Fixes #80
2024-09-11 19:18:13 +02:00
mannp
7148053ad4 Update pyproject.toml (#79) 2024-09-09 19:03:56 +02:00
2 changed files with 28 additions and 17 deletions

View File

@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
[project]
name = "spotify_to_tidal"
version = "1.0.0"
version = "1.0.3"
requires-python = ">= 3.10"
dependencies = [
"spotipy~=2.23.0",
"spotipy~=2.24.0",
"tidalapi==0.7.6",
"pyyaml~=6.0",
"tqdm~=4.64",
"sqlalchemy~=2.0",
"pytest~=7.0",
"pytest~=8.0",
"pytest-mock~=3.8"
]

View File

@@ -178,12 +178,14 @@ async def _fetch_all_from_spotify_in_chunks(fetch_function: Callable) -> List[di
async def get_tracks_from_spotify_playlist(spotify_session: spotipy.Spotify, spotify_playlist):
def _get_tracks_from_spotify_playlist(offset: int, playlist_id: str):
fields = "next,total,limit,items(track(name,album(name,artists),artists,track_number,duration_ms,id,external_ids(isrc)))"
fields = "next,total,limit,items(track(name,album(name,artists),artists,track_number,duration_ms,id,external_ids(isrc))),type"
return spotify_session.playlist_tracks(playlist_id=playlist_id, fields=fields, offset=offset)
print(f"Loading tracks from Spotify playlist '{spotify_playlist['name']}'")
return await repeat_on_request_error( _fetch_all_from_spotify_in_chunks, lambda offset: _get_tracks_from_spotify_playlist(offset=offset, playlist_id=spotify_playlist["id"]))
items = await repeat_on_request_error( _fetch_all_from_spotify_in_chunks, lambda offset: _get_tracks_from_spotify_playlist(offset=offset, playlist_id=spotify_playlist["id"]))
track_filter = lambda item: item.get('type', 'track') == 'track' # type may be 'episode' also
sanity_filter = lambda item: 'album' in item and 'name' in item['album'] and 'artists' in item['album'] and len(item['album']['artists']) > 0
return list(filter(sanity_filter, filter(track_filter, items)))
def populate_track_match_cache(spotify_tracks_: Sequence[t_spotify.SpotifyTrack], tidal_tracks_: Sequence[tidalapi.Track]):
""" Populate the track match cache with all the existing tracks in Tidal playlist corresponding to Spotify playlist """
@@ -282,14 +284,18 @@ async def search_new_tracks_on_tidal(tidal_session: tidalapi.Session, spotify_tr
async def sync_playlist(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, spotify_playlist, tidal_playlist: tidalapi.Playlist | None, config: dict):
""" sync given playlist to tidal """
# Create a new Tidal playlist if required
if not tidal_playlist:
# Get the tracks from both Spotify and Tidal, creating a new Tidal playlist if necessary
spotify_tracks = await get_tracks_from_spotify_playlist(spotify_session, spotify_playlist)
if len(spotify_tracks) == 0:
return # nothing to do
if tidal_playlist:
old_tidal_tracks = await get_all_playlist_tracks(tidal_playlist)
else:
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'])
old_tidal_tracks = []
# Extract the new tracks from the playlist that we haven't already seen before
spotify_tracks = await get_tracks_from_spotify_playlist(spotify_session, spotify_playlist)
old_tidal_tracks = await get_all_playlist_tracks(tidal_playlist)
populate_track_match_cache(spotify_tracks, old_tidal_tracks)
await search_new_tracks_on_tidal(tidal_session, spotify_tracks, spotify_playlist['name'], config)
new_tidal_track_ids = get_tracks_for_new_tidal_playlist(spotify_tracks)
@@ -365,20 +371,25 @@ def get_user_playlist_mappings(spotify_session: spotipy.Spotify, tidal_session:
return results
async def get_playlists_from_spotify(spotify_session: spotipy.Spotify, config):
# get all the user playlists from the Spotify account
# get all the playlists from the Spotify account
playlists = []
print("Loading Spotify playlists")
results = spotify_session.current_user_playlists()
first_results = spotify_session.current_user_playlists()
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])
playlists.extend([p for p in first_results['items']])
user_id = spotify_session.current_user()['id']
# get all the remaining playlists in parallel
if results['next']:
offsets = [ results['limit'] * n for n in range(1, math.ceil(results['total']/results['limit'])) ]
if first_results['next']:
offsets = [ first_results['limit'] * n for n in range(1, math.ceil(first_results['total']/first_results['limit'])) ]
extra_results = await atqdm.gather( *[asyncio.to_thread(spotify_session.current_user_playlists, offset=offset) for offset in offsets ] )
for extra_result in extra_results:
playlists.extend([p for p in extra_result['items'] if p['owner']['id'] == config['spotify']['username'] and not p['id'] in exclude_list])
return playlists
playlists.extend([p for p in extra_result['items']])
# filter out playlists that don't belong to us or are on the exclude list
my_playlist_filter = lambda p: p['owner']['id'] == user_id
exclude_filter = lambda p: not p['id'] in exclude_list
return list(filter( exclude_filter, filter( my_playlist_filter, playlists )))
def get_playlists_from_config(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, config):
# get the list of playlist sync mappings from the configuration file