|
| 1 | +"""Clone a few Drum and Bass Playlists from Spotify to Tidal. |
| 2 | +
|
| 3 | +These playlists are regular updated by awesome curators, but I dont pay Spotify premium. |
| 4 | +So I am running this script once a week, to stay updated on Tidal. |
| 5 | +
|
| 6 | +Two Options: |
| 7 | +- Get the the tracklist into Tidal (clone) |
| 8 | +- Accumulate into larger playlists to get everything covered over the whole year (sink) |
| 9 | +
|
| 10 | +Currently does not reorder tracks, just checks we include everything thats available |
| 11 | +on Tidal. |
| 12 | +""" |
| 13 | + |
| 14 | +import html |
| 15 | +import re |
| 16 | +from datetime import datetime |
| 17 | + |
| 18 | +import typer |
| 19 | +from rich.console import Console |
| 20 | +from rich.table import Table, box |
| 21 | + |
| 22 | +from plistsync.logger import log |
| 23 | +from plistsync.services.spotify import SpotifyLibrary |
| 24 | +from plistsync.services.tidal import ( |
| 25 | + TidalLibrary, |
| 26 | + TidalPlaylistTrack, |
| 27 | +) |
| 28 | + |
| 29 | +spotify_library = SpotifyLibrary() |
| 30 | +tidal_library = TidalLibrary() |
| 31 | + |
| 32 | +# Updated from spotify to tidal, with deletions and reordering |
| 33 | +# https://www.reddit.com/r/DnB/comments/1c7al1q/what_are_the_best_spotify_playlists_for_new_dnb/ |
| 34 | +lists_to_clone = { |
| 35 | + "https://open.spotify.com/playlist/3HSccBIzwpC5QOaUtifSqQ": # |
| 36 | + "H2L New DnB", |
| 37 | + # |
| 38 | + "https://open.spotify.com/playlist/4AOoXhcCyDS26rrnPccDwi": # |
| 39 | + "1MoreThing The Freshest GUESTlist!", |
| 40 | + # |
| 41 | + "https://open.spotify.com/playlist/4yfYTwDYPXn9GuhZxeEg53": # |
| 42 | + "Lennart Hoffmann Brand New DnB 🦀", |
| 43 | + # |
| 44 | + "https://open.spotify.com/playlist/7MGauBXssMrJ7JMa2RTp2w": # |
| 45 | + "Sub Focus DnB Selects", |
| 46 | + # |
| 47 | + "https://open.spotify.com/playlist/0wl7MGZyYsdWfZRhUdNenj": # |
| 48 | + "Lenzman Soulful DnB Essentials", |
| 49 | + # |
| 50 | + "https://open.spotify.com/playlist/1coTr2tQFEfsrX6PFNumay": # |
| 51 | + "LQ Fresh DnB Picks", |
| 52 | + # |
| 53 | + "https://open.spotify.com/playlist/6OYoyDsHzOoH8vJETyprlk": # |
| 54 | + "Emily Makis DnB Vocals That Bang", |
| 55 | + # |
| 56 | + "https://open.spotify.com/playlist/0Zarq4BVkFkZOWkmqsfrjA": # |
| 57 | + "UKF DnB Top 100", |
| 58 | + # |
| 59 | +} |
| 60 | + |
| 61 | +# Only insertions. I use these to collect by year |
| 62 | +lists_to_sink = { |
| 63 | + "https://open.spotify.com/playlist/0Zarq4BVkFkZOWkmqsfrjA": # |
| 64 | + f"UKF DnB Top 100 {datetime.now().year}", |
| 65 | +} |
| 66 | + |
| 67 | + |
| 68 | +def main(): |
| 69 | + log.info(f"Will clone {len(lists_to_clone)} playlists") |
| 70 | + for spotify_url, tidal_target_name in lists_to_clone.items(): |
| 71 | + transfer_playlist(spotify_url, tidal_target_name) |
| 72 | + |
| 73 | + log.info(f"Will sink {len(lists_to_sink)} playlists") |
| 74 | + for spotify_url, tidal_target_name in lists_to_sink.items(): |
| 75 | + transfer_playlist(spotify_url, tidal_target_name, insert_only=True) |
| 76 | + |
| 77 | + |
| 78 | +def transfer_playlist( |
| 79 | + spotify_url: str, |
| 80 | + tidal_target_name: str, |
| 81 | + insert_only: bool = False, |
| 82 | +): |
| 83 | + pass |
| 84 | + |
| 85 | + log.info(f"Transferring {spotify_url}") |
| 86 | + spotify_plist = spotify_library.get_playlist_or_raise(url=spotify_url) |
| 87 | + log.info(f"Found {len(spotify_plist)} tracks in source: {spotify_plist.name}") |
| 88 | + |
| 89 | + tidal_plist = tidal_library.get_playlist(name=tidal_target_name) |
| 90 | + if tidal_plist is None: |
| 91 | + log.debug(f"Creating new plist on tidal: {tidal_target_name}") |
| 92 | + tidal_plist = tidal_library.create_playlist( |
| 93 | + name=tidal_target_name, |
| 94 | + ) |
| 95 | + |
| 96 | + log.info( |
| 97 | + f"Target playlist '{tidal_target_name}' currently has {len(tidal_plist)} tracks" |
| 98 | + ) |
| 99 | + tidal_description = html.unescape( |
| 100 | + re.sub(r"<[^>]+>", "", spotify_plist.description or "") |
| 101 | + ) |
| 102 | + with tidal_plist.edit(): |
| 103 | + tidal_plist.name = tidal_target_name |
| 104 | + tidal_plist.description = ( |
| 105 | + f"{tidal_description}" |
| 106 | + f"\n\n| Last synced with ❤️ using plistsync on " |
| 107 | + f"{datetime.today().strftime('%Y-%m-%d')}" |
| 108 | + f"\n\n| Original playlist: {spotify_url}" |
| 109 | + ) |
| 110 | + |
| 111 | + # for now, the comparison is mostly isrc based |
| 112 | + tidal_isrcs = [t.isrc for t in tidal_plist.tracks if t.isrc is not None] |
| 113 | + spotify_isrcs = [t.isrc for t in tidal_plist.tracks if t.isrc is not None] |
| 114 | + |
| 115 | + if not insert_only: |
| 116 | + log.info("Removing old tracks from Tidal playlist") |
| 117 | + with tidal_plist.edit(): |
| 118 | + for t in reversed(tidal_plist.tracks): |
| 119 | + if t.isrc not in spotify_isrcs: |
| 120 | + tidal_plist.tracks.remove(t) |
| 121 | + |
| 122 | + log.info("Finding tracks to add on Tidal") |
| 123 | + ids_to_lookup = [ |
| 124 | + t.global_ids for t in spotify_plist.tracks if t.isrc not in tidal_isrcs |
| 125 | + ] |
| 126 | + missing_tracks = [ |
| 127 | + TidalPlaylistTrack(t) |
| 128 | + for t in tidal_library.find_many_by_global_ids(ids_to_lookup) |
| 129 | + if t is not None |
| 130 | + ] |
| 131 | + |
| 132 | + log.info(f"Adding {len(missing_tracks)} tracks to Tidal") |
| 133 | + with tidal_plist.edit(): |
| 134 | + tidal_plist.tracks.extend(missing_tracks) |
| 135 | + |
| 136 | + # log what we have synced |
| 137 | + console = Console() |
| 138 | + table = Table(box=box.MINIMAL_DOUBLE_HEAD) |
| 139 | + table.add_column("Artist", style="cyan") |
| 140 | + table.add_column("Title", style="cyan") |
| 141 | + table.add_column("Spotify ID", style="green") |
| 142 | + table.add_column("ISRC", style="magenta") |
| 143 | + table.add_column("Tidal ID", style="white") |
| 144 | + |
| 145 | + for spotify_track in spotify_plist.tracks: |
| 146 | + row = [ |
| 147 | + spotify_track.artists[0], |
| 148 | + spotify_track.name, |
| 149 | + spotify_track.id, |
| 150 | + spotify_track.isrc, |
| 151 | + ] |
| 152 | + try: |
| 153 | + tidal_track = [ |
| 154 | + t for t in tidal_plist.tracks if t.isrc == spotify_track.isrc |
| 155 | + ][0] |
| 156 | + row.append(tidal_track.id) |
| 157 | + except IndexError: |
| 158 | + row.append("[red]Not found[/red]") |
| 159 | + |
| 160 | + table.add_row(*row) |
| 161 | + |
| 162 | + # print instead of log, since logging adds whitespace |
| 163 | + console.print(table) |
| 164 | + |
| 165 | + |
| 166 | +main.__doc__ = __doc__ # use module docstring as help |
| 167 | +if __name__ == "__main__": |
| 168 | + typer.run(main) |
0 commit comments