diff --git a/.gitignore b/.gitignore index dd40d3f..8d1f29b 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ spotify_appkey.key *.m4a tmp/* *~ +.cache-* diff --git a/spotify_ripper/post_actions.py b/spotify_ripper/post_actions.py index 0ec49c3..97630e6 100644 --- a/spotify_ripper/post_actions.py +++ b/spotify_ripper/post_actions.py @@ -9,7 +9,7 @@ import spotify import codecs import shutil -from spotify_ripper.remove_all_from_playlist import remove_all_from_playlist +from spotify_ripper.spotipy_integration import remove_all_from_playlist class PostActions(object): diff --git a/spotify_ripper/remove_all_from_playlist.py b/spotify_ripper/remove_all_from_playlist.py deleted file mode 100644 index 5fc15de..0000000 --- a/spotify_ripper/remove_all_from_playlist.py +++ /dev/null @@ -1,45 +0,0 @@ -import spotipy.util as util -import spotipy.client -import os - -redirect_uri = 'http://www.purple.com' -client_id = '' -client_secret = '' -scope = 'playlist-modify-public playlist-modify-private playlist-read-collaborative' - -# client_id = os.environ["SPOTIPY_CLIENT_ID"] -# client_secret = os.environ["SPOTIPY_CLIENT_SECRET"] -# redirect_uri = os.environ["SPOTIPY_REDIRECT_URI"] - -token = None -spotInstance = None - - -def remove_all_from_playlist(username, playlistURI): - tracks = get_playlist_tracks(username, playlistURI) - - track_ids = [] - for i, item in enumerate(tracks['items']): - track = item['track'] - tid = track['id'] - track_ids.append(tid) - results = spotInstance.user_playlist_remove_all_occurrences_of_tracks(username, rPlaylistID, track_ids) - - -def get_playlist_tracks(username, playlistURI): - global rPlaylistID - p1, p2, p3, p4, rPlaylistID = playlistURI.split(':', 5) - - global token - token = util.prompt_for_user_token(username, scope) - - global spotInstance - spotInstance = spotipy.Spotify(auth=token) - spotInstance.trace = False - - print('Getting Results') - results = spotInstance.user_playlist(username, rPlaylistID, fields="tracks,next") - - tracks = results['tracks'] - - return tracks diff --git a/spotify_ripper/ripper.py b/spotify_ripper/ripper.py index 113d263..49adf42 100644 --- a/spotify_ripper/ripper.py +++ b/spotify_ripper/ripper.py @@ -12,7 +12,7 @@ from spotify_ripper.sync import Sync from spotify_ripper.eventloop import EventLoop from datetime import datetime -from spotify_ripper.remove_all_from_playlist import get_playlist_tracks +from spotify_ripper.spotipy_integration import get_playlist_tracks, init_spotipy import os import sys import time @@ -189,12 +189,16 @@ def run(self): self.ripper_continue.wait() if self.abort.is_set(): return - #set session to provate + + # set session to private self.session.social.private_session = True # list of spotify URIs uris = args.uri + # init spotipy api + init_spotipy(self.session.user.canonical_name) + def get_tracks_from_uri(uri): self.current_playlist = None self.current_album = None @@ -225,11 +229,16 @@ def get_tracks_from_uri(uri): # calculate total size and time all_tracks = [] + all_uris = {} for uri in uris: - tracks = list(get_tracks_from_uri(uri)) + if uri not in all_uris: + all_uris[uri] = list(get_tracks_from_uri(uri)) + tracks = all_uris[uri] # TODO: remove dependency on current_album, ... for idx, track in enumerate(tracks): + print('Loading track {}/{}...'.format(idx, len(tracks)), end='\r') + sys.stdout.flush() # ignore local tracks if track.is_local: @@ -237,6 +246,7 @@ def get_tracks_from_uri(uri): audio_file = self.format_track_path(idx, track) all_tracks.append((track, audio_file)) + print('Loading track {}/{}...'.format(len(tracks), len(tracks))) self.progress.calc_total(all_tracks) @@ -250,7 +260,9 @@ def get_tracks_from_uri(uri): if self.abort.is_set(): break - tracks = list(get_tracks_from_uri(uri)) + if uri not in all_uris: + all_uris[uri] = list(get_tracks_from_uri(uri)) + tracks = all_uris[uri] if args.playlist_sync and self.current_playlist: self.sync = Sync(args, self) @@ -269,8 +281,8 @@ def get_tracks_from_uri(uri): track.load(args.timeout) if track.availability != 1 or track.is_local: print( - Fore.RED + 'Track is not available, ' - 'skipping...' + Fore.RESET) + Fore.RED + 'Track {} - {} is not available, skipping...' + .format(track.artists[0].name, track.name) + Fore.RESET) self.post.log_failure(track) continue @@ -363,7 +375,7 @@ def get_tracks_from_uri(uri): self.post.create_playlist_wpl(tracks) # actually removing the tracks from playlist - self.post.remove_tracks_from_playlist() + # self.post.remove_tracks_from_playlist() # remove libspotify's offline storage cache self.post.remove_offline_cache() @@ -427,7 +439,6 @@ def load_link(self, uri): track = link.as_track() return iter([track]) elif link.type == spotify.LinkType.PLAYLIST: - print('get playlist tracks') self.playlist_uri = uri tracks = get_playlist_tracks(self.session.user.canonical_name, uri) track_list = tracks.get('items') @@ -438,7 +449,6 @@ def load_link(self, uri): tracksIter = iter(uriList) for i in tracksIter: trackList.append(self.session.get_link(i).as_track()) - print('Loading playlist...') return iter(trackList) elif link.type == spotify.LinkType.STARRED: link_user = link.as_user() diff --git a/spotify_ripper/spotipy_integration.py b/spotify_ripper/spotipy_integration.py new file mode 100644 index 0000000..3d4f37b --- /dev/null +++ b/spotify_ripper/spotipy_integration.py @@ -0,0 +1,79 @@ +import sys + +import spotipy.util as util +import spotipy.client +import spotipy.oauth2 as oauth2 +import os + +redirect_uri = 'http://www.purple.com' +client_id = '' +client_secret = '' +scope = 'playlist-modify-public playlist-modify-private playlist-read-collaborative' + +# client_id = os.environ["SPOTIPY_CLIENT_ID"] +# client_secret = os.environ["SPOTIPY_CLIENT_SECRET"] +# redirect_uri = os.environ["SPOTIPY_REDIRECT_URI"] + +token = None +spotInstance = None +spotAuthUsername = None + + +def init_spotipy(username): + global spotAuthUsername + spotAuthUsername = username + + global token + token = util.prompt_for_user_token(username, scope) + + global spotInstance + spotInstance = spotipy.Spotify(auth=token) + spotInstance.trace = False + + +def refresh_access_token(): + global token + token = util.prompt_for_user_token(spotAuthUsername, scope) + + global spotInstance + spotInstance = spotipy.Spotify(auth=token) + spotInstance.trace = False + + +def remove_all_from_playlist(username, playlistURI): + refresh_access_token() + tracks = get_playlist_tracks(username, playlistURI) + + track_ids = [] + for i, item in enumerate(tracks['items']): + track = item['track'] + tid = track['id'] + track_ids.append(tid) + results = spotInstance.user_playlist_remove_all_occurrences_of_tracks(username, rPlaylistID, track_ids) + + +def get_playlist_tracks(username, playlistURI): + refresh_access_token() + global rPlaylistID + p1, p2, p3, p4, rPlaylistID = playlistURI.split(':', 5) + + # the spotify api limits the number of tracks which can be retrieved from a playlist in a single request + # make multiple requests to collect all playlist tracks before continuing + print('Collecting tracks from playlist', end='\r') + tracks = spotInstance.user_playlist(username, rPlaylistID, fields='tracks,next')['tracks'] + sys.stdout.flush() + print('Collecting tracks from playlist ({}/{})'.format(len(tracks['items']), tracks['total']), end='\r') + paged_tracks = tracks + while paged_tracks['next']: + paged_tracks = spotInstance.next(paged_tracks) + tracks['items'].extend(paged_tracks['items']) + sys.stdout.flush() + print('Collecting tracks from playlist ({}/{})'.format(len(tracks['items']), tracks['total']), end='\r') + print('Collecting tracks from playlist ({}/{})'.format(len(tracks['items']), tracks['total'])) + + return tracks + + +def get_track_json(track_uri): + refresh_access_token() + return spotInstance.track(track_uri) diff --git a/spotify_ripper/web.py b/spotify_ripper/web.py index 11dc062..9520f47 100644 --- a/spotify_ripper/web.py +++ b/spotify_ripper/web.py @@ -4,6 +4,7 @@ from colorama import Fore from spotify_ripper.utils import * +from spotify_ripper.spotipy_integration import get_track_json import os import time import spotify @@ -233,12 +234,7 @@ def sanity_check_date(val): self.cache_result("charts", uri, charts_obj) return charts_obj - def get_large_coverart(self, uri): - def get_track_json(track_id): - url = self.api_url('tracks/' + track_id) - return self.request_json(url, "track") - def get_image_data(url): response = self.request_url(url, "cover art") return response.content @@ -248,18 +244,15 @@ def get_image_data(url): if cached_result is not None: return get_image_data(cached_result) - # extract album id from uri - uri_tokens = uri.split(':') - if len(uri_tokens) != 3: - return None - - track = get_track_json(uri_tokens[2]) + track = get_track_json(uri) if track is None: + print(Fore.RED + "Failed to retrieve track information, cover art cannot be set" + Fore.RESET) return None try: images = track['album']['images'] except KeyError: + print(Fore.RED + "Album art not found in track JSON, cover art cannot be set" + Fore.RESET) return None for image in images: