Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions example_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ spotify:
# - when false: favorites can only be synced manually via --sync-favorites argument
sync_favorites_default: true

# default setting for syncing followed artists when no command line arguments are provided
# - when true: followed artists will be synced by default (overriden when any command line arg provided)
# - when false: artists can only be synced manually via --sync-artists argument
sync_artists_default: false

# increasing these parameters should increase the search speed, while decreasing reduces likelihood of 429 errors
max_concurrency: 10 # max concurrent connections at any given time
rate_limit: 10 # max sustained connections per second
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ or sync just your 'Liked Songs' with:
spotify_to_tidal --sync-favorites
```

or sync just your followed artists with:

```bash
spotify_to_tidal --sync-artists
```

See example_config.yml for more configuration options, and `spotify_to_tidal --help` for more options.

---
Expand Down
11 changes: 11 additions & 0 deletions src/spotify_to_tidal/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def main():
parser.add_argument('--config', default='config.yml', help='location of the config file')
parser.add_argument('--uri', help='synchronize a specific URI instead of the one in the config')
parser.add_argument('--sync-favorites', action=argparse.BooleanOptionalAction, help='synchronize the favorites')
parser.add_argument('--sync-artists', action=argparse.BooleanOptionalAction, help='synchronize followed artists')
args = parser.parse_args()

with open(args.config, 'r') as f:
Expand All @@ -27,19 +28,29 @@ def main():
tidal_playlist = _sync.pick_tidal_playlist_for_spotify_playlist(spotify_playlist, tidal_playlists)
_sync.sync_playlists_wrapper(spotify_session, tidal_session, [tidal_playlist], config)
sync_favorites = args.sync_favorites # only sync favorites if command line argument explicitly passed
sync_artists = args.sync_artists
elif args.sync_artists:
sync_artists = True # sync only the artists
sync_favorites = False
elif args.sync_favorites:
sync_favorites = True # sync only the favorites
sync_artists = False
elif config.get('sync_playlists', None):
# if the config contains a sync_playlists list of mappings then use that
_sync.sync_playlists_wrapper(spotify_session, tidal_session, _sync.get_playlists_from_config(spotify_session, tidal_session, config), config)
sync_favorites = args.sync_favorites is None and config.get('sync_favorites_default', True)
sync_artists = args.sync_artists is None and config.get('sync_artists_default', False)
else:
# otherwise sync all the user playlists in the Spotify account and favorites unless explicitly disabled
_sync.sync_playlists_wrapper(spotify_session, tidal_session, _sync.get_user_playlist_mappings(spotify_session, tidal_session, config), config)
sync_favorites = args.sync_favorites is None and config.get('sync_favorites_default', True)
sync_artists = args.sync_artists is None and config.get('sync_artists_default', False)

if sync_favorites:
_sync.sync_favorites_wrapper(spotify_session, tidal_session, config)

if sync_artists:
_sync.sync_artists_wrapper(spotify_session, tidal_session, config)

if __name__ == '__main__':
main()
Expand Down
2 changes: 1 addition & 1 deletion src/spotify_to_tidal/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'open_tidal_session'
]

SPOTIFY_SCOPES = 'playlist-read-private, user-library-read'
SPOTIFY_SCOPES = 'playlist-read-private, user-library-read, user-follow-read'

def open_spotify_session(config) -> spotipy.Spotify:
credentials_manager = spotipy.SpotifyOAuth(username=config['username'],
Expand Down
63 changes: 62 additions & 1 deletion src/spotify_to_tidal/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from tqdm import tqdm
import traceback
import unicodedata
import math

from .type import spotify as t_spotify

Expand Down Expand Up @@ -414,3 +413,65 @@ def get_playlist_ids(config):
output.append((spotify_playlist, tidal_playlist))
return output

async def get_followed_artists_from_spotify(spotify_session: spotipy.Spotify) -> List[dict]:
""" Get all followed artists from Spotify """
def _get_followed_artists(after=None):
return spotify_session.current_user_followed_artists(after=after)

artists = []
results = await asyncio.to_thread(_get_followed_artists)
artists.extend(results['artists']['items'])

while results['artists']['next']:
results = await asyncio.to_thread(_get_followed_artists, after=results['artists']['cursors']['after'])
artists.extend(results['artists']['items'])

return artists

async def search_artist_on_tidal(artist_name: str, tidal_session: tidalapi.Session) -> tidalapi.Artist | None:
""" Search for an artist on Tidal """
def _search():
results = tidal_session.search(artist_name, models=[tidalapi.artist.Artist])
if results['artists']:
return results['artists'][0]
return None

return await asyncio.to_thread(_search)

async def sync_artists(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, config: dict):
""" Sync followed artists from Spotify to Tidal """
print("Loading followed artists from Spotify")
spotify_artists = await get_followed_artists_from_spotify(spotify_session)

if not spotify_artists:
print("No followed artists found on Spotify")
return

print(f"Searching for {len(spotify_artists)} artists on Tidal")
failed_artists = []

for artist in tqdm(spotify_artists, desc="Syncing artists to Tidal"):
try:
tidal_artist = await search_artist_on_tidal(artist['name'], tidal_session)
if tidal_artist:
try:
tidal_session.user.favorites.add_artist(tidal_artist.id)
except Exception as e:
print(f"Could not follow {artist['name']}: {e}")
failed_artists.append(artist['name'])
else:
failed_artists.append(artist['name'])
except Exception as e:
print(f"Error syncing artist {artist['name']}: {e}")
failed_artists.append(artist['name'])

if failed_artists:
print(f"\nCould not find/follow {len(failed_artists)} artists:")
for artist in failed_artists[:10]: # Show first 10
print(f" - {artist}")
if len(failed_artists) > 10:
print(f" ... and {len(failed_artists) - 10} more")

def sync_artists_wrapper(spotify_session: spotipy.Spotify, tidal_session: tidalapi.Session, config: dict):
asyncio.run(sync_artists(spotify_session, tidal_session, config))

Loading