#!/usr/bin/env python3 # =================================================== # Spotify playlist syncer using spotify-downloader # =================================================== # # Author: Prithu Goswami # Email: prithugoswami524@gmail.com # Date: 21 Sep 2018 # # Description: # This script syncs playlist from spotify using the spotify-downloader # module written by ritiek [https://github.com/ritiek/spotify-downloader] # Conifg file : ~/.config/playlist-syncer/config.json # The config file is in json and is read to get a list of playlists to sync # # Requirements: # 'spotdl', 'unicode-slugify', 'spotipy' # # Example config file: # # # { # "playlists": [ # { # "name": "Today's Top Hits", # "id": "37i9dQZF1DXcBWIGoYBM5M", # "owner_id": "spotify", # "location": "/home/user/music/todays-top-hits/", # "snapshot_id": " import json import os import shlex import subprocess import sys import spotipy from slugify import slugify from spotipy.oauth2 import SpotifyClientCredentials from time import strftime ok='{}[]()-_' HOME=os.environ['HOME'] client_id='5a78b4a9902c4ae1b8cba38dade6acc8' client_secret='884b0b01626b4e67888dada40ce49852' config_path=HOME+'/.config/playlist-sync/config.json' log_dir_path=HOME+'/.log/playlist-sync' client_credentials_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret) sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) config={} def create_config(): ans = input("Config file does not exist. Create it? [y/n] ") if ans in "yY": os.mkdir(HOME+'/.config/playlist-sync') global config config={'playlists':[]} with open(config_path, 'w') as jsonfile: json.dump(config, jsonfile, indent=4) ans = input("\nWould you also like to add a playlist? [y/n] ") if ans in "yY": pl_id=input('\nPlaylist Id > ') pl_path=input('Playlist local path (absolute path) > ') add_playlist(pl_id, pl_path) else: print('\nYou can add a playlist by passing the `-a` option to the' 'script\n\nsyntax: $ playlist-sync -a ' '') exit(0) else: exit(0) def add_playlist(plid, path): try: pl=sp.user_playlist(' ', plid) except spotipy.client.SpotifyException: print('Err:Invalid Playlist Id') exit(1) playlist= { "name": pl['name'], "id": pl['id'], "location": path, "snapshot_id": ' ', "last_updated": 'never' } config['playlists'].append(playlist) with open(config_path, 'w') as jsonfile: json.dump(config, jsonfile, indent=4) print('Added playlist: {} \n' 'Sync path: {}'.format(pl['name'], path)) print('\nUpdated config file') if not os.path.exists(path): os.mkdir(path) print('Created directory: {}'.format(path)) def requires_update(playlist_data, playlist): return playlist_data['snapshot_id'] != playlist['snapshot_id'] def get_local_tracknames(playlist): tracks=set() for f in os.listdir(playlist['location']): if f.endswith("mp3"): tracks.add(f[:-4]) return tracks def get_need_to_download(playlist_data, playlist): """ Return a set of urls to be downloaded that are not present in the current local playlist collection """ sp_tracknames=set() loc_tracknames = get_local_tracknames(playlist) sp_tracks=[] for t in playlist_data['tracks']['items']: tr={ 'title':slugify(t['track']['name'], lower=False, spaces=True, ok=ok), 'artist':slugify(t['track']['artists'][0]['name'], spaces=True, lower=False, ok=ok) } sp_track={ 'string':tr['artist']+' - '+tr['title'], 'url':t['track']['external_urls']['spotify'] } sp_tracks.append(sp_track) sp_tracknames.add(sp_track['string']) need_to_download = sp_tracknames-loc_tracknames for t in need_to_download: for track in sp_tracks: if t==track['string']: need_to_download.add(track['url']) #FIXME: If there are duplicates in the playlist remove them before proceeding need_to_download.remove(t) return need_to_download def write_log(data): logname=strftime('%c').replace(' ','-')+'sc.log' with open(log_dir_path+'/'+logname, 'w') as l: l.write(data) def update_m3u(playlist_data, playlist): m3uloc=playlist['location']+slugify(playlist_data['name'], ok=ok)+'.m3u8' with open(m3uloc, 'w') as m3ufile: for t in playlist_data['tracks']['items']: tr={ 'title':slugify(t['track']['name'], lower=False, spaces=True, ok=ok), 'artist':slugify(t['track']['artists'][0]['name'], spaces=True, lower=False, ok=ok) } track_string=tr['artist']+' - '+tr['title']+'.mp3\n' m3ufile.write(track_string) def update_playlist(playlist_data, playlist): print("Updating Playlist: " + playlist['name'] + "\n") track_urls=get_need_to_download(playlist_data, playlist) with open("/tmp/spotdllist.txt", 'w') as f: for a in track_urls: f.write(a+'\n') print('Written files to /tmp/spotdllist.txt') print('Track links to downalod:') for m in track_urls: print(m) command=('spotdl --list /tmp/spotdllist.txt -f {} --trim-silence --overwrite skip' .format(playlist['location'])) args=shlex.split(command) print('\nCalling spotdl') p = subprocess.Popen(args, universal_newlines=True) p_out, p_err = p.communicate() if p.returncode!=0: d = "-----STDOUT------\n"+p_out+'\n'*3+"-----STDERR------\n"+p_err write_log(d) print('Error occured, log written') return else: update_m3u(playlist_data, playlist) print('Updated the m3u playlist') i=0 for pl in config['playlists']: if pl['id']==playlist['id']: config['playlists'][i]['snapshot_id']=playlist_data['snapshot_id'] config['playlists'][i]['last_updated']=strftime('%c') i+=1 with open(config_path, 'w') as jsonfile: json.dump(config, jsonfile, indent=4) print('updated config') return if not os.path.exists(config_path): create_config() else: with open(config_path, 'r') as jsonfile: config = json.load(jsonfile) playlists=config['playlists'] if not playlists: print('There are no playlists to process') print('Add playlist by passing the `-a` option.\n\nsyntax: ' '$ playlist-sync -a ') exit(0) if len(sys.argv)>1: if sys.argv[1]=='-a': add_id=sys.argv[2] add_id_path=sys.argv[3] if add_id_path[-1] != '/': add_id_path+='/' add_playlist(add_id, add_id_path) exit(0) for playlist in playlists: playlist_data=sp.user_playlist(' ', playlist_id=playlist['id']) if requires_update(playlist_data, playlist): update_playlist(playlist_data, playlist)