class Ripper(threading.Thread): name = 'SpotifyRipperThread' audio_file = None pcm_file = None wav_file = None rip_proc = None pipe = None current_playlist = None current_album = None idx_digits = 3 login_success = False progress = None sync = None post = None dev_null = None rip_queue = queue.Queue() # threading events logged_in = threading.Event() logged_out = threading.Event() ripping = threading.Event() end_of_track = threading.Event() finished = threading.Event() abort = threading.Event() def __init__(self, args): threading.Thread.__init__(self) # initialize progress meter self.progress = Progress(args, self) self.args = args # initially logged-out self.logged_out.set() config = spotify.Config() default_dir = default_settings_dir() self.post = PostActions(args, self) # application key location if args.key is not None: config.load_application_key_file(args.key[0]) else: if not os.path.exists(default_dir): os.makedirs(default_dir) app_key_path = os.path.join(default_dir, "spotify_appkey.key") if not os.path.exists(app_key_path): print("\n" + Fore.YELLOW + "Please copy your spotify_appkey.key to " + default_dir + ", or use the --key|-k option" + Fore.RESET) sys.exit(1) config.load_application_key_file(app_key_path) # settings directory if args.settings is not None: settings_dir = norm_path(args.settings[0]) config.settings_location = settings_dir config.cache_location = settings_dir else: config.settings_location = default_dir config.cache_location = default_dir self.session = spotify.Session(config=config) self.session.volume_normalization = args.normalize # disable scrobbling self.session.social.set_scrobbling( spotify.SocialProvider.SPOTIFY, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.FACEBOOK, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.LASTFM, spotify.ScrobblingState.LOCAL_DISABLED) bit_rates = dict([('160', BitRate.BITRATE_160K), ('320', BitRate.BITRATE_320K), ('96', BitRate.BITRATE_96K)]) self.session.preferred_bitrate(bit_rates[args.quality]) self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed) self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track) self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery) self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost) self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in) self.event_loop = EventLoop(self.session, 0.1, self) def stop_event_loop(self): if self.event_loop.isAlive(): self.event_loop.stop() self.event_loop.join() def run(self): args = self.args # start event loop self.event_loop.start() # login print("Logging in...") if args.last: self.login_as_last() elif args.user is not None and args.password is None: password = getpass.getpass() self.login(args.user[0], password) else: self.login(args.user[0], args.password[0]) if not self.login_success: print(Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) self.stop_event_loop() self.finished.set() return # check if we were passed a file name or search if len(args.uri) == 1 and os.path.exists(args.uri[0]): uris = [line.strip() for line in open(args.uri[0])] elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"): uris = [list(self.search_query(args.uri[0]))] else: uris = args.uri def get_tracks_from_uri(uri): if isinstance(uri, list): return uri else: if (args.exclude_appears_on and uri.startswith("spotify:artist:")): album_uris = self.load_artist_albums(uri) return itertools.chain(*[ self.load_link(album_uri) for album_uri in album_uris ]) else: return self.load_link(uri) # calculate total size and time all_tracks = [] for uri in uris: tracks = get_tracks_from_uri(uri) all_tracks += list(tracks) self.progress.calc_total(all_tracks) if self.progress.total_size > 0: print("Total Download Size: " + format_size(self.progress.total_size)) # create track iterator for uri in uris: if self.abort.is_set(): break tracks = list(get_tracks_from_uri(uri)) if args.flat_with_index and self.current_playlist: self.idx_digits = len(str(len(self.current_playlist.tracks))) if args.playlist_sync and self.current_playlist: self.sync = Sync(args, self) self.sync.sync_playlist(self.current_playlist) # ripping loop for idx, track in enumerate(tracks): try: if self.abort.is_set(): break print('Loading track...') track.load() if track.availability != 1: print(Fore.RED + 'Track is not available, ' 'skipping...' + Fore.RESET) self.post.log_failure(track) continue self.audio_file = self.format_track_path(idx, track) if not args.overwrite and os.path.exists(self.audio_file): print(Fore.YELLOW + "Skipping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) self.post.queue_remove_from_playlist(idx) continue self.session.player.load(track) self.prepare_rip(idx, track) self.session.player.play() while not self.end_of_track.is_set() or \ not self.rip_queue.empty(): try: if self.abort.is_set(): break rip_item = self.rip_queue.get(timeout=1) self.rip(self.session, rip_item[0], rip_item[1], rip_item[2]) except queue.Empty: pass if self.abort.is_set(): self.session.player.play(False) self.end_of_track.set() self.post.clean_up_partial() self.post.log_failure(track) break self.end_of_track.clear() self.finish_rip(track) # update id3v2 with metadata and embed front cover image set_metadata_tags(args, self.audio_file, track) # make a note of the index and remove all the # tracks from the playlist when everything is done self.post.queue_remove_from_playlist(idx) except (spotify.Error, Exception) as e: if isinstance(e, Exception): print(Fore.RED + "Spotify error detected" + Fore.RESET) print(str(e)) print("Skipping to next track...") self.session.player.play(False) self.post.clean_up_partial() self.post.log_failure(track) continue if not self.abort.is_set(): # create playlist m3u file if needed self.post.create_playlist_m3u(tracks) # create playlist wpl file if needed self.post.create_playlist_wpl(tracks) # actually removing the tracks from playlist self.post.remove_tracks_from_playlist() # logout, we are done self.post.end_failure_log() self.post.print_summary() self.logout() self.stop_event_loop() self.finished.set() def load_link(self, uri): # blank out current playlist/album self.current_playlist = None self.current_album = None # ignore if the uri is just blank (e.g. from a file) if not uri: return iter([]) link = self.session.get_link(uri) if link.type == spotify.LinkType.TRACK: track = link.as_track() return iter([track]) elif link.type == spotify.LinkType.PLAYLIST: self.current_playlist = link.as_playlist() print('Loading playlist...') self.current_playlist.load() return iter(self.current_playlist.tracks) elif link.type == spotify.LinkType.STARRED: link_user = link.as_user() if link_user is not None: starred = self.session.get_starred(link_user.canonical_name) else: starred = self.session.get_starred() if starred is not None: print('Loading starred playlist...') starred.load() return iter(starred.tracks) else: print(Fore.RED + "Could not load starred playlist..." + Fore.RESET) return iter([]) elif link.type == spotify.LinkType.ALBUM: album = link.as_album() album_browser = album.browse() print('Loading album browser...') album_browser.load() self.current_album = album return iter(album_browser.tracks) elif link.type == spotify.LinkType.ARTIST: artist = link.as_artist() artist_browser = artist.browse() print('Loading artist browser...') artist_browser.load() return iter(artist_browser.tracks) return iter([]) # excludes 'appears on' albums def load_artist_albums(self, uri): def get_albums_json(offset): url = 'https://api.spotify.com/v1/artists/' + \ uri_tokens[2] + \ '/albums/?=album_type=album,single,compilation' + \ '&limit=50&offset=' + str(offset) print(Fore.GREEN + "Attempting to retrieve albums " "from Spotify's Web API" + Fore.RESET) print(Fore.CYAN + url + Fore.RESET) req = requests.get(url) if req.status_code == 200: return req.json() else: print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET) return None # extract artist id from uri uri_tokens = uri.split(':') if len(uri_tokens) != 3: return [] # it is possible we won't get all the albums on the first request offset = 0 album_uris = [] total = None while total is None or offset < total: try: # rate limit if not first request if total is None: time.sleep(1.0) albums = get_albums_json(offset) if albums is None: break # extract album URIs album_uris += [album['uri'] for album in albums['items']] offset = len(album_uris) if total is None: total = albums['total'] except KeyError as e: break print(str(len(album_uris)) + " albums found") return album_uris def search_query(self, query): args = self.args print("Searching for query: " + query) try: result = self.session.search(query) result.load() except spotify.Error as e: print(str(e)) return iter([]) # list tracks print(Fore.GREEN + "Results" + Fore.RESET) for track_idx, track in enumerate(result.tracks): print(" " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" + to_ascii(args, track.album.name) + "] " + to_ascii(args, track.artists[0].name) + " - " + to_ascii(args, track.name) + " (" + str(track.popularity) + ")") pick = raw_input("Pick track(s) (ex 1-3,5): ") def get_track(i): if i >= 0 and i < len(result.tracks): return iter([result.tracks[i]]) return iter([]) pattern = re.compile("^[0-9 ,\-]+$") if pick.isdigit(): pick = int(pick) - 1 return get_track(pick) elif pick.lower() == "a" or pick.lower() == "all": return iter(result.tracks) elif pattern.match(pick): def range_string(comma_string): def hyphen_range(hyphen_string): x = [int(x) - 1 for x in hyphen_string.split('-')] return range(x[0], x[-1] + 1) return itertools.chain( *[hyphen_range(r) for r in comma_string.split(',')]) picks = sorted(set(list(range_string(pick)))) return itertools.chain(*[get_track(p) for p in picks]) if pick != "": print(Fore.RED + "Invalid selection" + Fore.RESET) return iter([]) def on_music_delivery(self, session, audio_format, frame_bytes, num_frames): try: self.rip_queue.put_nowait( (audio_format.sample_rate, frame_bytes, num_frames)) except queue.Full: print(Fore.RED + "rip_queue is full. dropped music data" + Fore.RESET) return num_frames def on_connection_state_changed(self, session): if session.connection.state is spotify.ConnectionState.LOGGED_IN: self.login_success = True self.logged_in.set() self.logged_out.clear() elif session.connection.state is spotify.ConnectionState.LOGGED_OUT: self.logged_in.clear() self.logged_out.set() def on_logged_in(self, session, error): if error is spotify.ErrorType.OK: print("Logged in as " + session.user.display_name) else: error_map = { 9: "CLIENT_TOO_OLD", 8: "UNABLE_TO_CONTACT_SERVER", 6: "BAD_USERNAME_OR_PASSWORD", 7: "USER_BANNED", 15: "USER_NEEDS_PREMIUM", 16: "OTHER_TRANSIENT", 10: "OTHER_PERMANENT" } print("Logged in failed: " + error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error))) self.login_success = False self.logged_in.set() def play_token_lost(self, session): print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET) self.abort_rip() def on_end_of_track(self, session): self.session.player.play(False) self.end_of_track.set() def login(self, user, password): """login into Spotify""" self.session.login(user, password, remember_me=True) self.logged_in.wait() def login_as_last(self): """login as the previous logged in user""" try: self.session.relogin() self.logged_in.wait() except spotify.Error as e: self.login_success = False print(str(e)) def logout(self): """logout from Spotify""" time.sleep(0.1) if self.logged_in.is_set(): print('Logging out...') self.session.logout() self.logged_out.wait() def album_artists_web(self, uri): def get_album_json(album_id): url = 'https://api.spotify.com/v1/albums/' + album_id print(Fore.GREEN + "Attempting to retrieve album " "from Spotify's Web API" + Fore.RESET) print(Fore.CYAN + url + Fore.RESET) req = requests.get(url) if req.status_code == 200: return req.json() else: print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET) return None # extract album id from uri uri_tokens = uri.split(':') if len(uri_tokens) != 3: return None album = get_album_json(uri_tokens[2]) if album is None: return None return [artist['name'] for artist in album['artists']] def format_track_path(self, idx, track): args = self.args _base_dir = base_dir(args) audio_file = args.format[0].strip() track_artist = to_ascii(args, escape_filename_part(track.artists[0].name)) track_artists = to_ascii( args, ", ".join([artist.name for artist in track.artists])) if len(track.artists) > 1: featuring_artists = to_ascii( args, ", ".join([artist.name for artist in track.artists[1:]])) else: featuring_artists = "" album_artist = to_ascii( args, self.current_album.artist.name if self.current_album is not None else track_artist) album_artists_web = track_artists # only retrieve album_artist_web if it exists in the format string if (self.current_album is not None and audio_file.find("{album_artists_web}") >= 0): artist_array = self.album_artists_web(self.current_album.link.uri) if artist_array is not None: album_artists_web = to_ascii(args, ", ".join(artist_array)) album = to_ascii(args, escape_filename_part(track.album.name)) track_name = to_ascii(args, escape_filename_part(track.name)) year = str(track.album.year) extension = args.output_type idx_str = str(idx + 1) track_num = str(track.index) disc_num = str(track.disc) if self.current_playlist is not None: playlist_name = to_ascii(args, self.current_playlist.name) playlist_owner = to_ascii(args, self.current_playlist.owner.display_name) else: playlist_name = "No Playlist" playlist_owner = "No Playlist Owner" user = self.session.user.display_name tags = { "track_artist": track_artist, "track_artists": track_artists, "album_artist": album_artist, "album_artists_web": album_artists_web, "artist": track_artist, "artists": track_artists, "album": album, "track_name": track_name, "track": track_name, "year": year, "ext": extension, "extension": extension, "idx": idx_str, "index": idx_str, "track_num": track_num, "track_idx": track_num, "track_index": track_num, "disc_num": disc_num, "disc_idx": disc_num, "disc_index": disc_num, "playlist": playlist_name, "playlist_name": playlist_name, "playlist_owner": playlist_owner, "playlist_user": playlist_owner, "playlist_username": playlist_owner, "user": user, "username": user, "feat_artists": featuring_artists, "featuring_artists": featuring_artists } fill_tags = { "idx", "index", "track_num", "track_idx", "track_index", "disc_num", "disc_idx", "disc_index" } prefix_tags = {"feat_artists", "featuring_artists"} paren_tags = {"track_name", "track"} for tag in tags.keys(): audio_file = audio_file.replace("{" + tag + "}", tags[tag]) if tag in fill_tags: match = re.search(r"\{" + tag + r":\d+\}", audio_file) if match: tokens = audio_file[match.start():match.end()]\ .strip("{}").split(":") tag_filled = tags[tag].zfill(int(tokens[1])) audio_file = audio_file[:match.start()] + tag_filled + \ audio_file[match.end():] if tag in prefix_tags: # don't print prefix if there are no values if len(tags[tag]) > 0: match = re.search(r"\{" + tag + r":[^\}]+\}", audio_file) if match: tokens = audio_file[match.start():match.end()]\ .strip("{}").split(":") audio_file = audio_file[:match.start()] + tokens[1] + \ " " + tags[tag] + audio_file[match.end():] else: match = re.search(r"\s*\{" + tag + r":[^\}]+\}", audio_file) if match: audio_file = audio_file[:match.start()] + \ audio_file[match.end():] if tag in paren_tags: match = re.search(r"\{" + tag + r":paren\}", audio_file) if match: match_tag = re.search(r"(.*)\s+-\s+([^-]+)", tags[tag]) if match_tag: audio_file = audio_file[:match.start()] + \ match_tag.group(1) + " (" + \ match_tag.group(2) + ")" + \ audio_file[match.end():] else: audio_file = audio_file[:match.start()] + tags[tag] + \ audio_file[match.end():] # in case the file name is too long def truncate(_str, max_size): return _str[:max_size].strip() if len(_str) > max_size else _str def truncate_dir_path(dir_path): path_tokens = dir_path.split(os.pathsep) path_tokens = [truncate(token, 255) for token in path_tokens] return os.pathsep.join(path_tokens) def truncate_file_name(file_name): tokens = file_name.rsplit(os.extsep, 1) if len(tokens) > 1: tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1) else: tokens[0] = truncate(tokens[0], 255) return os.extsep.join(tokens) # ensure each component in path is no more than 255 chars long tokens = audio_file.rsplit(os.pathsep, 1) if len(tokens) > 1: audio_file = os.path.join(truncate_dir_path(tokens[0]), truncate_file_name(tokens[1])) else: audio_file = truncate_file_name(tokens[0]) # prepend base_dir audio_file = to_ascii(args, os.path.join(_base_dir, audio_file)) # create directory if it doesn't exist audio_path = os.path.dirname(audio_file) if not os.path.exists(audio_path): os.makedirs(audio_path) return audio_file def prepare_rip(self, idx, track): args = self.args # reset progress self.progress.prepare_track(track) if self.progress.total_tracks > 1: print(Fore.GREEN + "[ " + str(idx + 1) + " / " + str(self.progress.total_tracks) + " ] Ripping " + track.link.uri + Fore.RESET) else: print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) file_size = calc_file_size(self.args, track) print("Track Download Size: " + format_size(file_size)) if args.output_type == "wav": self.wav_file = wave.open(self.audio_file, "wb") self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed')) elif args.output_type == "pcm": self.pcm_file = open(self.audio_file, 'wb') elif args.output_type == "flac": self.rip_proc = Popen([ "flac", "-f", str("-" + args.comp), "--silent", "--endian", "little", "--channels", "2", "--bps", "16", "--sample-rate", "44100", "--sign", "signed", "-o", self.audio_file, "-" ], stdin=PIPE) elif args.output_type == "alac.m4a": self.rip_proc = Popen([ "avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar", "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-", "-acodec", "alac", self.audio_file ], stdin=PIPE) elif args.output_type == "ogg": if args.cbr: self.rip_proc = Popen([ "oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o", self.audio_file, "-" ], stdin=PIPE) else: self.rip_proc = Popen([ "oggenc", "--quiet", "--raw", "-q", args.vbr, "-o", self.audio_file, "-" ], stdin=PIPE) elif args.output_type == "opus": if args.cbr: self.rip_proc = Popen([ "opusenc", "--quiet", "--comp", args.comp, "--cvbr", "--bitrate", str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100", "-", self.audio_file ], stdin=PIPE) else: self.rip_proc = Popen([ "opusenc", "--quiet", "--comp", args.comp, "--vbr", "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-", self.audio_file ], stdin=PIPE) elif args.output_type == "aac": if self.dev_null is None: self.dev_null = open(os.devnull, 'wb') if args.cbr: self.rip_proc = Popen([ "faac", "-P", "-X", "-b", args.bitrate, "-o", self.audio_file, "-" ], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) else: self.rip_proc = Popen([ "faac", "-P", "-X", "-q", args.vbr, "-o", self.audio_file, "-" ], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) elif args.output_type == "m4a": if args.cbr: self.rip_proc = Popen([ "fdkaac", "-S", "-R", "-b", args.bitrate, "-o", self.audio_file, "-" ], stdin=PIPE) else: self.rip_proc = Popen([ "fdkaac", "-S", "-R", "-m", args.vbr, "-o", self.audio_file, "-" ], stdin=PIPE) elif args.output_type == "mp3": lame_args = ["lame", "--silent"] if args.stereo_mode is not None: lame_args.extend(["-m", args.stereo_mode]) if args.cbr: lame_args.extend(["-cbr", "-b", args.bitrate]) else: lame_args.extend(["-V", args.vbr]) lame_args.extend(["-h", "-r", "-", self.audio_file]) self.rip_proc = Popen(lame_args, stdin=PIPE) if self.rip_proc is not None: self.pipe = self.rip_proc.stdin self.ripping.set() def finish_rip(self, track): self.progress.end_track() if self.pipe is not None: print(Fore.GREEN + 'Rip complete' + Fore.RESET) self.pipe.flush() self.pipe.close() # wait for process to end before continuing ret_code = self.rip_proc.wait() if ret_code != 0: print(Fore.YELLOW + "Warning: encoder returned non-zero " "error code " + str(ret_code) + Fore.RESET) self.rip_proc = None self.pipe = None if self.wav_file is not None: self.wav_file.close() self.wav_file = None if self.pcm_file is not None: self.pcm_file.flush() os.fsync(self.pcm_file.fileno()) self.pcm_file.close() self.pcm_file = None self.ripping.clear() self.post.log_success(track) def rip(self, session, sample_rate, frame_bytes, num_frames): if self.ripping.is_set(): self.progress.update_progress(num_frames, sample_rate) if self.pipe is not None: self.pipe.write(frame_bytes) if self.wav_file is not None: self.wav_file.writeframes(frame_bytes) if self.pcm_file is not None: self.pcm_file.write(frame_bytes) def abort_rip(self): self.ripping.clear() self.abort.set()
class Ripper(threading.Thread): name = 'SpotifyRipperThread' audio_file = None pcm_file = None wav_file = None rip_proc = None pipe = None current_playlist = None current_album = None idx_digits = 3 login_success = False progress = None sync = None post = None dev_null = None rip_queue = queue.Queue() # threading events logged_in = threading.Event() logged_out = threading.Event() ripping = threading.Event() end_of_track = threading.Event() finished = threading.Event() abort = threading.Event() def __init__(self, args): threading.Thread.__init__(self) # initialize progress meter self.progress = Progress(args, self) self.args = args # initially logged-out self.logged_out.set() config = spotify.Config() default_dir = default_settings_dir() self.post = PostActions(args, self) # application key location if args.key is not None: config.load_application_key_file(args.key[0]) else: if not os.path.exists(default_dir): os.makedirs(default_dir) app_key_path = os.path.join(default_dir, "spotify_appkey.key") if not os.path.exists(app_key_path): print("\n" + Fore.YELLOW + "Please copy your spotify_appkey.key to " + default_dir + ", or use the --key|-k option" + Fore.RESET) sys.exit(1) config.load_application_key_file(app_key_path) # settings directory if args.settings is not None: settings_dir = norm_path(args.settings[0]) config.settings_location = settings_dir config.cache_location = settings_dir else: config.settings_location = default_dir config.cache_location = default_dir self.session = spotify.Session(config=config) self.session.volume_normalization = args.normalize # disable scrobbling self.session.social.set_scrobbling( spotify.SocialProvider.SPOTIFY, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.FACEBOOK, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.LASTFM, spotify.ScrobblingState.LOCAL_DISABLED) bit_rates = dict([ ('160', BitRate.BITRATE_160K), ('320', BitRate.BITRATE_320K), ('96', BitRate.BITRATE_96K)]) self.session.preferred_bitrate(bit_rates[args.quality]) self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed) self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track) self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery) self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost) self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in) self.event_loop = EventLoop(self.session, 0.1, self) def stop_event_loop(self): if self.event_loop.isAlive(): self.event_loop.stop() self.event_loop.join() def run(self): args = self.args # start event loop self.event_loop.start() # login print("Logging in...") if args.last: self.login_as_last() elif args.user is not None and args.password is None: password = getpass.getpass() self.login(args.user[0], password) else: self.login(args.user[0], args.password[0]) if not self.login_success: print( Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET) self.stop_event_loop() self.finished.set() return # check if we were passed a file name or search if len(args.uri) == 1 and os.path.exists(args.uri[0]): uris = [line.strip() for line in open(args.uri[0])] elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"): uris = [list(self.search_query(args.uri[0]))] else: uris = args.uri def get_tracks_from_uri(uri): if isinstance(uri, list): return uri else: if (args.exclude_appears_on and uri.startswith("spotify:artist:")): album_uris = self.load_artist_albums(uri) return itertools.chain( *[self.load_link(album_uri) for album_uri in album_uris]) else: return self.load_link(uri) # calculate total size and time all_tracks = [] for uri in uris: tracks = get_tracks_from_uri(uri) all_tracks += list(tracks) self.progress.calc_total(all_tracks) if self.progress.total_size > 0: print( "Total Download Size: " + format_size(self.progress.total_size)) # create track iterator for uri in uris: if self.abort.is_set(): break tracks = list(get_tracks_from_uri(uri)) if args.flat_with_index and self.current_playlist: self.idx_digits = len(str(len(self.current_playlist.tracks))) if args.playlist_sync and self.current_playlist: self.sync = Sync(args, self) self.sync.sync_playlist(self.current_playlist) # ripping loop for idx, track in enumerate(tracks): try: if self.abort.is_set(): break print('Loading track...') track.load() if track.availability != 1: print( Fore.RED + 'Track is not available, ' 'skipping...' + Fore.RESET) self.post.log_failure(track) continue self.audio_file = self.format_track_path(idx, track) if not args.overwrite and os.path.exists(self.audio_file): print( Fore.YELLOW + "Skipping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) self.post.queue_remove_from_playlist(idx) continue self.session.player.load(track) self.prepare_rip(idx, track) self.session.player.play() while not self.end_of_track.is_set() or \ not self.rip_queue.empty(): try: if self.abort.is_set(): break rip_item = self.rip_queue.get(timeout=1) self.rip(self.session, rip_item[0], rip_item[1], rip_item[2]) except queue.Empty: pass if self.abort.is_set(): self.session.player.play(False) self.end_of_track.set() self.post.clean_up_partial() self.post.log_failure(track) break self.end_of_track.clear() self.finish_rip(track) # update id3v2 with metadata and embed front cover image set_metadata_tags(args, self.audio_file, track) # make a note of the index and remove all the # tracks from the playlist when everything is done self.post.queue_remove_from_playlist(idx) except (spotify.Error, Exception) as e: if isinstance(e, Exception): print(Fore.RED + "Spotify error detected" + Fore.RESET) print(str(e)) print("Skipping to next track...") self.session.player.play(False) self.post.clean_up_partial() self.post.log_failure(track) continue if not self.abort.is_set(): # create playlist m3u file if needed self.post.create_playlist_m3u(tracks) # create playlist wpl file if needed self.post.create_playlist_wpl(tracks) # actually removing the tracks from playlist self.post.remove_tracks_from_playlist() # logout, we are done self.post.end_failure_log() self.post.print_summary() self.logout() self.stop_event_loop() self.finished.set() def load_link(self, uri): # blank out current playlist/album self.current_playlist = None self.current_album = None # ignore if the uri is just blank (e.g. from a file) if not uri: return iter([]) link = self.session.get_link(uri) if link.type == spotify.LinkType.TRACK: track = link.as_track() return iter([track]) elif link.type == spotify.LinkType.PLAYLIST: self.current_playlist = link.as_playlist() print('Loading playlist...') self.current_playlist.load() return iter(self.current_playlist.tracks) elif link.type == spotify.LinkType.STARRED: link_user = link.as_user() if link_user is not None: starred = self.session.get_starred(link_user.canonical_name) else: starred = self.session.get_starred() if starred is not None: print('Loading starred playlist...') starred.load() return iter(starred.tracks) else: print( Fore.RED + "Could not load starred playlist..." + Fore.RESET) return iter([]) elif link.type == spotify.LinkType.ALBUM: album = link.as_album() album_browser = album.browse() print('Loading album browser...') album_browser.load() self.current_album = album return iter(album_browser.tracks) elif link.type == spotify.LinkType.ARTIST: artist = link.as_artist() artist_browser = artist.browse() print('Loading artist browser...') artist_browser.load() return iter(artist_browser.tracks) return iter([]) # excludes 'appears on' albums def load_artist_albums(self, uri): def get_albums_json(offset): url = 'https://api.spotify.com/v1/artists/' + \ uri_tokens[2] + \ '/albums/?=album_type=album,single,compilation' + \ '&limit=50&offset=' + str(offset) print( Fore.GREEN + "Attempting to retrieve albums " "from Spotify's Web API" + Fore.RESET) print(Fore.CYAN + url + Fore.RESET) req = requests.get(url) if req.status_code == 200: return req.json() else: print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET) return None # extract artist id from uri uri_tokens = uri.split(':') if len(uri_tokens) != 3: return [] # it is possible we won't get all the albums on the first request offset = 0 album_uris = [] total = None while total is None or offset < total: try: # rate limit if not first request if total is None: time.sleep(1.0) albums = get_albums_json(offset) if albums is None: break # extract album URIs album_uris += [album['uri'] for album in albums['items']] offset = len(album_uris) if total is None: total = albums['total'] except KeyError as e: break print(str(len(album_uris)) + " albums found") return album_uris def search_query(self, query): args = self.args print("Searching for query: " + query) try: result = self.session.search(query) result.load() except spotify.Error as e: print(str(e)) return iter([]) # list tracks print(Fore.GREEN + "Results" + Fore.RESET) for track_idx, track in enumerate(result.tracks): print(" " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" + to_ascii(args, track.album.name) + "] " + to_ascii(args, track.artists[0].name) + " - " + to_ascii(args, track.name) + " (" + str(track.popularity) + ")") pick = raw_input("Pick track(s) (ex 1-3,5): ") def get_track(i): if i >= 0 and i < len(result.tracks): return iter([result.tracks[i]]) return iter([]) pattern = re.compile("^[0-9 ,\-]+$") if pick.isdigit(): pick = int(pick) - 1 return get_track(pick) elif pick.lower() == "a" or pick.lower() == "all": return iter(result.tracks) elif pattern.match(pick): def range_string(comma_string): def hyphen_range(hyphen_string): x = [int(x) - 1 for x in hyphen_string.split('-')] return range(x[0], x[-1] + 1) return itertools.chain( *[hyphen_range(r) for r in comma_string.split(',')]) picks = sorted(set(list(range_string(pick)))) return itertools.chain(*[get_track(p) for p in picks]) if pick != "": print(Fore.RED + "Invalid selection" + Fore.RESET) return iter([]) def on_music_delivery(self, session, audio_format, frame_bytes, num_frames): try: self.rip_queue.put_nowait((audio_format.sample_rate, frame_bytes, num_frames)) except queue.Full: print(Fore.RED + "rip_queue is full. dropped music data" + Fore.RESET) return num_frames def on_connection_state_changed(self, session): if session.connection.state is spotify.ConnectionState.LOGGED_IN: self.login_success = True self.logged_in.set() self.logged_out.clear() elif session.connection.state is spotify.ConnectionState.LOGGED_OUT: self.logged_in.clear() self.logged_out.set() def on_logged_in(self, session, error): if error is spotify.ErrorType.OK: print("Logged in as " + session.user.display_name) else: error_map = { 9: "CLIENT_TOO_OLD", 8: "UNABLE_TO_CONTACT_SERVER", 6: "BAD_USERNAME_OR_PASSWORD", 7: "USER_BANNED", 15: "USER_NEEDS_PREMIUM", 16: "OTHER_TRANSIENT", 10: "OTHER_PERMANENT" } print("Logged in failed: " + error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error))) self.login_success = False self.logged_in.set() def play_token_lost(self, session): print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET) self.abort_rip() def on_end_of_track(self, session): self.session.player.play(False) self.end_of_track.set() def login(self, user, password): """login into Spotify""" self.session.login(user, password, remember_me=True) self.logged_in.wait() def login_as_last(self): """login as the previous logged in user""" try: self.session.relogin() self.logged_in.wait() except spotify.Error as e: self.login_success = False print(str(e)) def logout(self): """logout from Spotify""" time.sleep(0.1) if self.logged_in.is_set(): print('Logging out...') self.session.logout() self.logged_out.wait() def album_artists_web(self, uri): def get_album_json(album_id): url = 'https://api.spotify.com/v1/albums/' + album_id print( Fore.GREEN + "Attempting to retrieve album " "from Spotify's Web API" + Fore.RESET) print(Fore.CYAN + url + Fore.RESET) req = requests.get(url) if req.status_code == 200: return req.json() else: print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET) return None # extract album id from uri uri_tokens = uri.split(':') if len(uri_tokens) != 3: return None album = get_album_json(uri_tokens[2]) if album is None: return None return [artist['name'] for artist in album['artists']] def format_track_path(self, idx, track): args = self.args _base_dir = base_dir(args) audio_file = args.format[0].strip() track_artist = to_ascii( args, escape_filename_part(track.artists[0].name)) track_artists = to_ascii(args, ", ".join( [artist.name for artist in track.artists])) if len(track.artists) > 1: featuring_artists = to_ascii(args, ", ".join( [artist.name for artist in track.artists[1:]])) else: featuring_artists = "" album_artist = to_ascii( args, self.current_album.artist.name if self.current_album is not None else track_artist) album_artists_web = track_artists # only retrieve album_artist_web if it exists in the format string if (self.current_album is not None and audio_file.find("{album_artists_web}") >= 0): artist_array = self.album_artists_web(self.current_album.link.uri) if artist_array is not None: album_artists_web = to_ascii(args, ", ".join(artist_array)) album = to_ascii(args, escape_filename_part(track.album.name)) track_name = to_ascii(args, escape_filename_part(track.name)) year = str(track.album.year) extension = args.output_type idx_str = str(idx + 1) track_num = str(track.index) disc_num = str(track.disc) if self.current_playlist is not None: playlist_name = to_ascii(args, self.current_playlist.name) playlist_owner = to_ascii( args, self.current_playlist.owner.display_name) else: playlist_name = "No Playlist" playlist_owner = "No Playlist Owner" user = self.session.user.display_name tags = { "track_artist": track_artist, "track_artists": track_artists, "album_artist": album_artist, "album_artists_web": album_artists_web, "artist": track_artist, "artists": track_artists, "album": album, "track_name": track_name, "track": track_name, "year": year, "ext": extension, "extension": extension, "idx": idx_str, "index": idx_str, "track_num": track_num, "track_idx": track_num, "track_index": track_num, "disc_num": disc_num, "disc_idx": disc_num, "disc_index": disc_num, "playlist": playlist_name, "playlist_name": playlist_name, "playlist_owner": playlist_owner, "playlist_user": playlist_owner, "playlist_username": playlist_owner, "user": user, "username": user, "feat_artists": featuring_artists, "featuring_artists": featuring_artists } fill_tags = {"idx", "index", "track_num", "track_idx", "track_index", "disc_num", "disc_idx", "disc_index"} prefix_tags = {"feat_artists", "featuring_artists"} paren_tags = {"track_name", "track"} for tag in tags.keys(): audio_file = audio_file.replace("{" + tag + "}", tags[tag]) if tag in fill_tags: match = re.search(r"\{" + tag + r":\d+\}", audio_file) if match: tokens = audio_file[match.start():match.end()]\ .strip("{}").split(":") tag_filled = tags[tag].zfill(int(tokens[1])) audio_file = audio_file[:match.start()] + tag_filled + \ audio_file[match.end():] if tag in prefix_tags: # don't print prefix if there are no values if len(tags[tag]) > 0: match = re.search(r"\{" + tag + r":[^\}]+\}", audio_file) if match: tokens = audio_file[match.start():match.end()]\ .strip("{}").split(":") audio_file = audio_file[:match.start()] + tokens[1] + \ " " + tags[tag] + audio_file[match.end():] else: match = re.search(r"\s*\{" + tag + r":[^\}]+\}", audio_file) if match: audio_file = audio_file[:match.start()] + \ audio_file[match.end():] if tag in paren_tags: match = re.search(r"\{" + tag + r":paren\}", audio_file) if match: match_tag = re.search(r"(.*)\s+-\s+([^-]+)", tags[tag]) if match_tag: audio_file = audio_file[:match.start()] + \ match_tag.group(1) + " (" + \ match_tag.group(2) + ")" + \ audio_file[match.end():] else: audio_file = audio_file[:match.start()] + tags[tag] + \ audio_file[match.end():] # in case the file name is too long def truncate(_str, max_size): return _str[:max_size].strip() if len(_str) > max_size else _str def truncate_dir_path(dir_path): path_tokens = dir_path.split(os.pathsep) path_tokens = [truncate(token, 255) for token in path_tokens] return os.pathsep.join(path_tokens) def truncate_file_name(file_name): tokens = file_name.rsplit(os.extsep, 1) if len(tokens) > 1: tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1) else: tokens[0] = truncate(tokens[0], 255) return os.extsep.join(tokens) # ensure each component in path is no more than 255 chars long tokens = audio_file.rsplit(os.pathsep, 1) if len(tokens) > 1: audio_file = os.path.join( truncate_dir_path(tokens[0]), truncate_file_name(tokens[1])) else: audio_file = truncate_file_name(tokens[0]) # prepend base_dir audio_file = to_ascii(args, os.path.join(_base_dir, audio_file)) # create directory if it doesn't exist audio_path = os.path.dirname(audio_file) if not os.path.exists(audio_path): os.makedirs(audio_path) return audio_file def prepare_rip(self, idx, track): args = self.args # reset progress self.progress.prepare_track(track) if self.progress.total_tracks > 1: print(Fore.GREEN + "[ " + str(idx + 1) + " / " + str( self.progress.total_tracks) + " ] Ripping " + track.link.uri + Fore.RESET) else: print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) file_size = calc_file_size(self.args, track) print("Track Download Size: " + format_size(file_size)) if args.output_type == "wav": self.wav_file = wave.open(self.audio_file, "wb") self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed')) elif args.output_type == "pcm": self.pcm_file = open(self.audio_file, 'wb') elif args.output_type == "flac": self.rip_proc = Popen( ["flac", "-f", str("-" + args.comp), "--silent", "--endian", "little", "--channels", "2", "--bps", "16", "--sample-rate", "44100", "--sign", "signed", "-o", self.audio_file, "-"], stdin=PIPE) elif args.output_type == "alac.m4a": self.rip_proc = Popen( ["avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar", "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-", "-acodec", "alac", self.audio_file], stdin=PIPE) elif args.output_type == "ogg": if args.cbr: self.rip_proc = Popen( ["oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE) else: self.rip_proc = Popen( ["oggenc", "--quiet", "--raw", "-q", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE) elif args.output_type == "opus": if args.cbr: self.rip_proc = Popen( ["opusenc", "--quiet", "--comp", args.comp, "--cvbr", "--bitrate", str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100", "-", self.audio_file], stdin=PIPE) else: self.rip_proc = Popen( ["opusenc", "--quiet", "--comp", args.comp, "--vbr", "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-", self.audio_file], stdin=PIPE) elif args.output_type == "aac": if self.dev_null is None: self.dev_null = open(os.devnull, 'wb') if args.cbr: self.rip_proc = Popen( ["faac", "-P", "-X", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) else: self.rip_proc = Popen( ["faac", "-P", "-X", "-q", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) elif args.output_type == "m4a": if args.cbr: self.rip_proc = Popen( ["fdkaac", "-S", "-R", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE) else: self.rip_proc = Popen( ["fdkaac", "-S", "-R", "-m", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE) elif args.output_type == "mp3": lame_args = ["lame", "--silent"] if args.stereo_mode is not None: lame_args.extend(["-m", args.stereo_mode]) if args.cbr: lame_args.extend(["-cbr", "-b", args.bitrate]) else: lame_args.extend(["-V", args.vbr]) lame_args.extend(["-h", "-r", "-", self.audio_file]) self.rip_proc = Popen(lame_args, stdin=PIPE) if self.rip_proc is not None: self.pipe = self.rip_proc.stdin self.ripping.set() def finish_rip(self, track): self.progress.end_track() if self.pipe is not None: print(Fore.GREEN + 'Rip complete' + Fore.RESET) self.pipe.flush() self.pipe.close() # wait for process to end before continuing ret_code = self.rip_proc.wait() if ret_code != 0: print( Fore.YELLOW + "Warning: encoder returned non-zero " "error code " + str(ret_code) + Fore.RESET) self.rip_proc = None self.pipe = None if self.wav_file is not None: self.wav_file.close() self.wav_file = None if self.pcm_file is not None: self.pcm_file.flush() os.fsync(self.pcm_file.fileno()) self.pcm_file.close() self.pcm_file = None self.ripping.clear() self.post.log_success(track) def rip(self, session, sample_rate, frame_bytes, num_frames): if self.ripping.is_set(): self.progress.update_progress(num_frames, sample_rate) if self.pipe is not None: self.pipe.write(frame_bytes) if self.wav_file is not None: self.wav_file.writeframes(frame_bytes) if self.pcm_file is not None: self.pcm_file.write(frame_bytes) def abort_rip(self): self.ripping.clear() self.abort.set()
class Ripper(threading.Thread): name = 'SpotifyRipperThread' audio_file = None pcm_file = None wav_file = None rip_proc = None pipe = None current_playlist = None current_album = None current_chart = None login_success = False progress = None sync = None post = None web = None dev_null = None stop_time = None track_path_cache = {} playlist_uri = None rip_queue = queue.Queue() # threading events logged_in = threading.Event() logged_out = threading.Event() ripper_continue = threading.Event() ripping = threading.Event() end_of_track = threading.Event() finished = threading.Event() abort = threading.Event() skip = threading.Event() play_token_resume = threading.Event() def __init__(self, args): threading.Thread.__init__(self) # initialize progress meter self.progress = Progress(args, self) self.args = args # initially logged-out self.logged_out.set() config = spotify.Config() default_dir = default_settings_dir() self.post = PostActions(args, self) self.web = WebAPI(args, self) proxy = os.environ.get('http_proxy') if proxy is not None: config.proxy = proxy # application key location if args.key is not None: config.load_application_key_file(args.key) else: if not path_exists(default_dir): os.makedirs(enc_str(default_dir)) app_key_path = os.path.join(default_dir, "spotify_appkey.key") if not path_exists(app_key_path): print("\n" + Fore.YELLOW + "Please copy your spotify_appkey.key to " + default_dir + ", or use the --key|-k option" + Fore.RESET) sys.exit(1) config.load_application_key_file(app_key_path) # settings directory if args.settings is not None: settings_dir = norm_path(args.settings) config.settings_location = settings_dir config.cache_location = settings_dir else: config.settings_location = default_dir config.cache_location = default_dir self.session = spotify.Session(config=config) self.session.volume_normalization = args.normalize # disable scrobbling self.session.social.set_scrobbling( spotify.SocialProvider.SPOTIFY, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.FACEBOOK, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.LASTFM, spotify.ScrobblingState.LOCAL_DISABLED) bit_rates = dict([('160', BitRate.BITRATE_160K), ('320', BitRate.BITRATE_320K), ('96', BitRate.BITRATE_96K)]) self.session.preferred_bitrate(bit_rates[args.quality]) self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed) self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track) self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery) self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost) self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in) self.event_loop = EventLoop(self.session, 0.1, self) def stop_event_loop(self): if self.event_loop.isAlive(): self.event_loop.stop() self.event_loop.join() # executes on main thread (not SpotifyRipper thread) def login(self): args = self.args print("Logging in...") if args.last: self.login_as_last() if not self.login_success and args.user is not None: # remove old saved password self.session.forget_me() if args.password is None: password = getpass.getpass() self.login_as_user(args.user, password) else: self.login_as_user(args.user, args.password) return self.login_success def run(self): args = self.args # start event loop self.event_loop.start() # wait for main thread to login self.ripper_continue.wait() if self.abort.is_set(): return #set session to provate self.session.social.private_session = True # list of spotify URIs uris = args.uri def get_tracks_from_uri(uri): self.current_playlist = None self.current_album = None self.current_chart = None if isinstance(uri, list): return uri else: if (uri.startswith("spotify:artist:") and (args.artist_album_type is not None or args.artist_album_market is not None)): album_uris = self.web.get_albums_with_filter(uri) return itertools.chain(*[ self.load_link(album_uri) for album_uri in album_uris ]) elif uri.startswith("spotify:charts:"): charts = self.web.get_charts(uri) if charts is not None: self.current_chart = charts chart_uris = charts["tracks"] return itertools.chain(*[ self.load_link(chart_uri) for chart_uri in chart_uris ]) else: return iter([]) else: return self.load_link(uri) # calculate total size and time all_tracks = [] for uri in uris: tracks = list(get_tracks_from_uri(uri)) # TODO: remove dependency on current_album, ... for idx, track in enumerate(tracks): # ignore local tracks if track.is_local: continue audio_file = self.format_track_path(idx, track) all_tracks.append((track, audio_file)) self.progress.calc_total(all_tracks) if self.progress.total_size > 0: print("Total Download Size: " + format_size(self.progress.total_size)) # create track iterator for uri in uris: if self.abort.is_set(): break tracks = list(get_tracks_from_uri(uri)) if args.playlist_sync and self.current_playlist: self.sync = Sync(args, self) self.sync.sync_playlist(self.current_playlist) # ripping loop for idx, track in enumerate(tracks): try: self.check_stop_time() self.skip.clear() if self.abort.is_set(): break print('Loading track...') track.load(args.timeout) if track.availability != 1 or track.is_local: print(Fore.RED + 'Track is not available, ' 'skipping...' + Fore.RESET) self.post.log_failure(track) continue self.audio_file = self.format_track_path(idx, track) if not args.overwrite and path_exists(self.audio_file): if is_partial(self.audio_file, track): print("Overwriting partial file") else: print(Fore.YELLOW + "Skipping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) continue self.session.player.load(track) self.prepare_rip(idx, track) self.session.player.play() timeout_count = 0 while not self.end_of_track.is_set() or \ not self.rip_queue.empty(): try: if self.abort.is_set() or self.skip.is_set(): break rip_item = self.rip_queue.get(timeout=1) if self.abort.is_set() or self.skip.is_set(): break self.rip(self.session, rip_item[0], rip_item[1], rip_item[2]) except queue.Empty: timeout_count += 1 if timeout_count > 60: raise spotify.Error("Timeout while " "ripping track") if self.skip.is_set(): extra_line = "" if self.play_token_resume.is_set() \ else "\n" print(extra_line + Fore.YELLOW + "User skipped track... " + Fore.RESET) self.session.player.play(False) self.post.clean_up_partial() self.post.log_failure(track) self.end_of_track.clear() self.progress.end_track(show_end=False) self.ripping.clear() continue if self.abort.is_set(): self.session.player.play(False) self.end_of_track.set() self.post.clean_up_partial() self.post.log_failure(track) break self.end_of_track.clear() self.finish_rip(track) # update id3v2 with metadata and embed front cover image set_metadata_tags(args, self.audio_file, idx, track, self) # finally log success self.post.log_success(track) except (spotify.Error, Exception) as e: if isinstance(e, Exception): print(Fore.RED + "Spotify error detected" + Fore.RESET) print(str(e)) traceback.print_exc() print("Skipping to next track...") self.session.player.play(False) self.post.clean_up_partial() self.post.log_failure(track) continue # create playlist m3u file if needed self.post.create_playlist_m3u(tracks) # create playlist wpl file if needed self.post.create_playlist_wpl(tracks) # remove libspotify's offline storage cache self.post.remove_offline_cache() # logout, we are done self.post.end_failure_log() self.post.print_summary() self.logout() self.stop_event_loop() self.finished.set() sys.exit() def check_stop_time(self): args = self.args def wait_for_resume(resume_time): while datetime.now() < resume_time and not self.abort.is_set(): time.sleep(1) def stop_time_triggered(): print(Fore.YELLOW + "Stop time of " + self.stop_time.strftime("%H:%M") + " has been triggered, stopping..." + Fore.RESET) if args.resume_after is not None: resume_time = parse_time_str(args.resume_after) print(Fore.YELLOW + "Script will resume at " + resume_time.strftime("%H:%M") + Fore.RESET) wait_for_resume(resume_time) self.stop_time = None else: self.abort.set() if args.stop_after is not None: if self.stop_time is None: self.stop_time = parse_time_str(args.stop_after) print(Fore.YELLOW + "Script will stop after " + self.stop_time.strftime("%H:%M") + Fore.RESET) if self.stop_time < datetime.now(): stop_time_triggered() # we also wait if the "play token" was lost elif self.play_token_resume.is_set(): resume_time = parse_time_str(args.play_token_resume) print(Fore.YELLOW + "Script will resume at " + resume_time.strftime("%H:%M") + Fore.RESET) wait_for_resume(resume_time) self.play_token_resume.clear() def load_link(self, uri): # ignore if the uri is just blank (e.g. from a file) if not uri: return iter([]) trackList = [] uriList = [] args = self.args link = self.session.get_link(uri) track_list = [] if link.type == spotify.LinkType.TRACK: 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') for n in track_list: thisTrack = n.get('track') thisTrackuri = thisTrack.get('uri') uriList.append(thisTrackuri) 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() def load_starred(): if link_user is not None: return self.session.get_starred(link_user.canonical_name) else: return self.session.get_starred() starred = load_starred() attempt_count = 1 while starred is None: if attempt_count > 3: print(Fore.RED + "Could not load starred playlist..." + Fore.RESET) return iter([]) print("Attempt " + str(attempt_count) + " failed: Spotify " + "returned None for starred playlist, trying again in " + "5 seconds...") time.sleep(5.0) starred = load_starred() attempt_count += 1 print('Loading starred playlist...') starred.load(args.timeout) return iter(starred.tracks) elif link.type == spotify.LinkType.ALBUM: album = link.as_album() album_browser = album.browse() print('Loading album browser...') album_browser.load(args.timeout) self.current_album = album return iter(album_browser.tracks) elif link.type == spotify.LinkType.ARTIST: artist = link.as_artist() artist_browser = artist.browse() print('Loading artist browser...') artist_browser.load(args.timeout) return iter(artist_browser.tracks) return iter([]) def search_query(self, query): print("Searching for query: " + query) try: result = self.session.search(query) result.load(self.args.timeout) except spotify.Error as e: print(str(e)) return iter([]) if len(result.tracks) == 0: print(Fore.RED + "No Results" + Fore.RESET) return iter([]) # list tracks print(Fore.GREEN + "Results" + Fore.RESET) for track_idx, track in enumerate(result.tracks): print(" " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" + to_ascii(track.album.name) + "] " + to_ascii(track.artists[0].name) + " - " + to_ascii(track.name) + " (" + str(track.popularity) + ")") pick = raw_input("Pick track(s) (ex 1-3,5): ") def get_track(i): if i >= 0 and i < len(result.tracks): return iter([result.tracks[i]]) return iter([]) pattern = re.compile("^[0-9 ,\-]+$") if pick.isdigit(): pick = int(pick) - 1 return get_track(pick) elif pick.lower() == "a" or pick.lower() == "all": return iter(result.tracks) elif pattern.match(pick): def range_string(comma_string): def hyphen_range(hyphen_string): x = [int(x) - 1 for x in hyphen_string.split('-')] return range(x[0], x[-1] + 1) return itertools.chain( *[hyphen_range(r) for r in comma_string.split(',')]) picks = sorted(set(list(range_string(pick)))) return itertools.chain(*[get_track(p) for p in picks]) if pick != "": print(Fore.RED + "Invalid selection" + Fore.RESET) return iter([]) def on_music_delivery(self, session, audio_format, frame_bytes, num_frames): try: self.rip_queue.put_nowait( (audio_format.sample_rate, frame_bytes, num_frames)) except queue.Full: print(Fore.RED + "rip_queue is full. dropped music data" + Fore.RESET) return num_frames def on_connection_state_changed(self, session): if session.connection.state is spotify.ConnectionState.LOGGED_IN: self.login_success = True self.logged_in.set() self.logged_out.clear() elif session.connection.state is spotify.ConnectionState.LOGGED_OUT: self.logged_in.clear() self.ripper_continue.clear() self.logged_out.set() def on_logged_in(self, session, error): if error is spotify.ErrorType.OK: print("Logged in as " + session.user.display_name) else: error_map = { 9: "CLIENT_TOO_OLD", 8: "UNABLE_TO_CONTACT_SERVER", 6: "BAD_USERNAME_OR_PASSWORD", 7: "USER_BANNED", 15: "USER_NEEDS_PREMIUM", 16: "OTHER_TRANSIENT", 10: "OTHER_PERMANENT" } print("Logged in failed: " + error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error))) self.login_success = False self.logged_in.set() def play_token_lost(self, session): if self.args.play_token_resume is not None: print("\n" + Fore.RED + "Play token lost, waiting " + self.args.play_token_resume + " to resume..." + Fore.RESET) self.play_token_resume.set() self.skip.set() else: print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET) self.abort_rip() def on_end_of_track(self, session): self.session.player.play(False) self.end_of_track.set() def login_as_user(self, user, password): """login into Spotify""" self.session.login(user, password, remember_me=True) self.logged_in.wait() def login_as_last(self): """login as the previous logged in user""" try: self.session.relogin() self.logged_in.wait() except spotify.Error as e: self.login_success = False print(str(e)) def logout(self): """logout from Spotify""" time.sleep(0.1) if self.logged_in.is_set(): print('Logging out...') self.session.logout() self.logged_out.wait() def format_track_path(self, idx, track): args = self.args # check if we cached the result already track.load(args.timeout) if track.link.uri in self.track_path_cache: return self.track_path_cache[track.link.uri] audio_file = \ format_track_string(self, args.format.strip(), idx, track) # in case the file name is too long def truncate(_str, max_size): return _str[:max_size].strip() if len(_str) > max_size else _str def truncate_dir_path(dir_path): path_tokens = dir_path.split(os.sep) path_tokens = [truncate(token, 255) for token in path_tokens] return os.sep.join(path_tokens) def truncate_file_name(file_name): tokens = file_name.rsplit(os.extsep, 1) if len(tokens) > 1: tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1) else: tokens[0] = truncate(tokens[0], 255) return os.extsep.join(tokens) # ensure each component in path is no more than 255 chars long if args.windows_safe: tokens = audio_file.rsplit(os.sep, 1) if len(tokens) > 1: audio_file = os.path.join(truncate_dir_path(tokens[0]), truncate_file_name(tokens[1])) else: audio_file = truncate_file_name(tokens[0]) # replace filename if args.replace is not None: audio_file = self.replace_filename(audio_file, args.replace) # remove not allowed characters in filename (windows) if args.windows_safe: audio_file = re.sub('[:"*?<>|]', '', audio_file) # prepend base_dir audio_file = to_ascii(os.path.join(base_dir(), audio_file)) if args.normalized_ascii: audio_file = to_normalized_ascii(audio_file) # create directory if it doesn't exist audio_path = os.path.dirname(audio_file) if not path_exists(audio_path): os.makedirs(enc_str(audio_path)) self.track_path_cache[track.link.uri] = audio_file return audio_file def replace_filename(self, filename, pattern_list): for pattern in pattern_list: repl = pattern.split('/') filename = re.sub(repl[0], repl[1], filename) return filename def prepare_rip(self, idx, track): args = self.args # reset progress self.progress.prepare_track(track) if self.progress.total_tracks > 1: print(Fore.GREEN + "[ " + str(self.progress.track_idx) + " / " + str(self.progress.total_tracks + self.progress.skipped_tracks) + " ] Ripping " + track.link.uri + Fore.WHITE + "\t(ESC to skip)" + Fore.RESET) else: print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) file_size = calc_file_size(track) print("Track Download Size: " + format_size(file_size)) if args.output_type == "wav" or args.plus_wav: audio_file = change_file_extension(self.audio_file, "wav") if \ args.output_type != "wav" else self.audio_file wav_file = audio_file if sys.version_info >= (3, 0) \ else enc_str(audio_file) self.wav_file = wave.open(wav_file, "wb") self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed')) if args.output_type == "pcm" or args.plus_pcm: audio_file = change_file_extension(self.audio_file, "pcm") if \ args.output_type != "pcm" else self.audio_file self.pcm_file = open(enc_str(audio_file), 'wb') audio_file_enc = enc_str(self.audio_file) if args.output_type == "flac": self.rip_proc = Popen([ "flac", "-f", ("-" + str(args.comp)), "--silent", "--endian", "little", "--channels", "2", "--bps", "16", "--sample-rate", "44100", "--sign", "signed", "-o", audio_file_enc, "-" ], stdin=PIPE) elif args.output_type == "aiff": self.rip_proc = Popen([ "sox", "-q", "--endian", "little", "--channels", "2", "--bits", "16", "--rate", "44100", "--encoding", "unsigned-integer", "-t", "raw", "-", audio_file_enc ], stdin=PIPE) elif args.output_type == "alac.m4a": self.rip_proc = Popen([ "avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar", "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-", "-acodec", "alac", audio_file_enc ], stdin=PIPE) elif args.output_type == "ogg": if args.cbr: self.rip_proc = Popen([ "oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o", audio_file_enc, "-" ], stdin=PIPE) else: self.rip_proc = Popen([ "oggenc", "--quiet", "--raw", "-q", args.vbr, "-o", audio_file_enc, "-" ], stdin=PIPE) elif args.output_type == "opus": if args.cbr: self.rip_proc = Popen([ "opusenc", "--quiet", "--comp", args.comp, "--cvbr", "--bitrate", str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100", "-", audio_file_enc ], stdin=PIPE) else: self.rip_proc = Popen([ "opusenc", "--quiet", "--comp", args.comp, "--vbr", "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-", audio_file_enc ], stdin=PIPE) elif args.output_type == "aac": if self.dev_null is None: self.dev_null = open(os.devnull, 'wb') if args.cbr: self.rip_proc = Popen([ "faac", "-P", "-X", "-b", args.bitrate, "-o", audio_file_enc, "-" ], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) else: self.rip_proc = Popen([ "faac", "-P", "-X", "-q", args.vbr, "-o", audio_file_enc, "-" ], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) elif args.output_type == "m4a": if args.cbr: self.rip_proc = Popen([ "fdkaac", "-S", "-R", "-b", args.bitrate, "-o", audio_file_enc, "-" ], stdin=PIPE) else: self.rip_proc = Popen([ "fdkaac", "-S", "-R", "-m", args.vbr, "-o", audio_file_enc, "-" ], stdin=PIPE) elif args.output_type == "mp3": lame_args = ["lame", "--silent"] if args.stereo_mode is not None: lame_args.extend(["-m", args.stereo_mode]) if args.cbr: lame_args.extend(["-cbr", "-b", args.bitrate]) else: lame_args.extend(["-V", args.vbr]) lame_args.extend(["-h", "-r", "-", audio_file_enc]) self.rip_proc = Popen(lame_args, stdin=PIPE) if self.rip_proc is not None: self.pipe = self.rip_proc.stdin self.ripping.set() def finish_rip(self, track): self.progress.end_track() if self.pipe is not None: print(Fore.GREEN + 'Rip complete' + Fore.RESET) self.pipe.flush() self.pipe.close() # wait for process to end before continuing ret_code = self.rip_proc.wait() if ret_code != 0: print(Fore.YELLOW + "Warning: encoder returned non-zero " "error code " + str(ret_code) + Fore.RESET) self.rip_proc = None self.pipe = None if self.wav_file is not None: self.wav_file.close() self.wav_file = None if self.pcm_file is not None: self.pcm_file.flush() os.fsync(self.pcm_file.fileno()) self.pcm_file.close() self.pcm_file = None self.ripping.clear() def rip(self, session, sample_rate, frame_bytes, num_frames): if self.ripping.is_set(): self.progress.update_progress(num_frames, sample_rate) if self.pipe is not None: self.pipe.write(frame_bytes) if self.wav_file is not None: self.wav_file.writeframes(frame_bytes) if self.pcm_file is not None: self.pcm_file.write(frame_bytes) def abort_rip(self): self.ripping.clear() self.abort.set()
class Ripper(threading.Thread): name = 'SpotifyRipperThread' audio_file = None pcm_file = None wav_file = None rip_proc = None pipe = None current_playlist = None current_album = None current_chart = None login_success = False progress = None sync = None post = None web = None dev_null = None stop_time = None track_path_cache = {} rip_queue = queue.Queue() # threading events logged_in = threading.Event() logged_out = threading.Event() ripper_continue = threading.Event() ripping = threading.Event() end_of_track = threading.Event() finished = threading.Event() abort = threading.Event() skip = threading.Event() play_token_resume = threading.Event() def __init__(self, args): threading.Thread.__init__(self) # initialize progress meter self.progress = Progress(args, self) self.args = args # initially logged-out self.logged_out.set() config = spotify.Config() default_dir = default_settings_dir() self.post = PostActions(args, self) self.web = WebAPI(args, self) proxy = os.environ.get('http_proxy') if proxy is not None: config.proxy = proxy # application key location if args.key is not None: config.load_application_key_file(args.key) else: if not path_exists(default_dir): os.makedirs(enc_str(default_dir)) app_key_path = os.path.join(default_dir, "spotify_appkey.key") if not path_exists(app_key_path): print("\n" + Fore.YELLOW + "Please copy your spotify_appkey.key to " + default_dir + ", or use the --key|-k option" + Fore.RESET) sys.exit(1) config.load_application_key_file(app_key_path) # settings directory if args.settings is not None: settings_dir = norm_path(args.settings) config.settings_location = settings_dir config.cache_location = settings_dir else: config.settings_location = default_dir config.cache_location = default_dir self.session = spotify.Session(config=config) self.session.volume_normalization = args.normalize # disable scrobbling self.session.social.set_scrobbling( spotify.SocialProvider.SPOTIFY, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.FACEBOOK, spotify.ScrobblingState.LOCAL_DISABLED) self.session.social.set_scrobbling( spotify.SocialProvider.LASTFM, spotify.ScrobblingState.LOCAL_DISABLED) bit_rates = dict([ ('160', BitRate.BITRATE_160K), ('320', BitRate.BITRATE_320K), ('96', BitRate.BITRATE_96K)]) self.session.preferred_bitrate(bit_rates[args.quality]) self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed) self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track) self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery) self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost) self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in) self.event_loop = EventLoop(self.session, 0.1, self) def stop_event_loop(self): if self.event_loop.isAlive(): self.event_loop.stop() self.event_loop.join() # executes on main thread (not SpotifyRipper thread) def login(self): args = self.args print("Logging in...") if args.last: self.login_as_last() if not self.login_success and args.user is not None: # remove old saved password self.session.forget_me() if args.password is None: password = getpass.getpass() self.login_as_user(args.user, password) else: self.login_as_user(args.user, args.password) return self.login_success def run(self): args = self.args # start event loop self.event_loop.start() # wait for main thread to login self.ripper_continue.wait() if self.abort.is_set(): return # list of spotify URIs uris = args.uri def get_tracks_from_uri(uri): self.current_playlist = None self.current_album = None self.current_chart = None if isinstance(uri, list): return uri else: if (uri.startswith("spotify:artist:") and (args.artist_album_type is not None or args.artist_album_market is not None)): album_uris = self.web.get_albums_with_filter(uri) return itertools.chain( *[self.load_link(album_uri) for album_uri in album_uris]) elif uri.startswith("spotify:charts:"): charts = self.web.get_charts(uri) if charts is not None: self.current_chart = charts chart_uris = charts["tracks"] return itertools.chain( *[self.load_link(chart_uri) for chart_uri in chart_uris]) else: return iter([]) else: return self.load_link(uri) # calculate total size and time all_tracks = [] for uri in uris: tracks = list(get_tracks_from_uri(uri)) # TODO: remove dependency on current_album, ... for idx, track in enumerate(tracks): # ignore local tracks if track.is_local: continue audio_file = self.format_track_path(idx, track) all_tracks.append((track, audio_file)) self.progress.calc_total(all_tracks) if self.progress.total_size > 0: print( "Total Download Size: " + format_size(self.progress.total_size)) # create track iterator for uri in uris: if self.abort.is_set(): break tracks = list(get_tracks_from_uri(uri)) if args.playlist_sync and self.current_playlist: self.sync = Sync(args, self) self.sync.sync_playlist(self.current_playlist) # ripping loop for idx, track in enumerate(tracks): try: self.check_stop_time() self.skip.clear() if self.abort.is_set(): break print('Loading track...') track.load() if track.availability != 1 or track.is_local: print( Fore.RED + 'Track is not available, ' 'skipping...' + Fore.RESET) self.post.log_failure(track) continue self.audio_file = self.format_track_path(idx, track) if not args.overwrite and path_exists(self.audio_file): if is_partial(self.audio_file, track): print("Overwriting partial file") else: print( Fore.YELLOW + "Skipping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) self.post.queue_remove_from_playlist(idx) continue self.session.player.load(track) self.prepare_rip(idx, track) self.session.player.play() timeout_count = 0 while not self.end_of_track.is_set() or \ not self.rip_queue.empty(): try: if self.abort.is_set() or self.skip.is_set(): break rip_item = self.rip_queue.get(timeout=1) if self.abort.is_set() or self.skip.is_set(): break self.rip(self.session, rip_item[0], rip_item[1], rip_item[2]) except queue.Empty: timeout_count += 1 if timeout_count > 60: raise spotify.Error("Timeout while " "ripping track") if self.skip.is_set(): extra_line = "" if self.play_token_resume.is_set() \ else "\n" print(extra_line + Fore.YELLOW + "User skipped track... " + Fore.RESET) self.session.player.play(False) self.post.clean_up_partial() self.post.log_failure(track) self.end_of_track.clear() self.progress.end_track(show_end=False) self.ripping.clear() continue if self.abort.is_set(): self.session.player.play(False) self.end_of_track.set() self.post.clean_up_partial() self.post.log_failure(track) break self.end_of_track.clear() self.finish_rip(track) # update id3v2 with metadata and embed front cover image set_metadata_tags(args, self.audio_file, idx, track, self) # make a note of the index and remove all the # tracks from the playlist when everything is done self.post.queue_remove_from_playlist(idx) # finally log success self.post.log_success(track) except (spotify.Error, Exception) as e: if isinstance(e, Exception): print(Fore.RED + "Spotify error detected" + Fore.RESET) print(str(e)) traceback.print_exc() print("Skipping to next track...") self.session.player.play(False) self.post.clean_up_partial() self.post.log_failure(track) continue # create playlist m3u file if needed self.post.create_playlist_m3u(tracks) # create playlist wpl file if needed self.post.create_playlist_wpl(tracks) # actually removing the tracks from playlist self.post.remove_tracks_from_playlist() # remove libspotify's offline storage cache self.post.remove_offline_cache() # logout, we are done self.post.end_failure_log() self.post.print_summary() self.logout() self.stop_event_loop() self.finished.set() def check_stop_time(self): args = self.args def wait_for_resume(resume_time): while datetime.now() < resume_time and not self.abort.is_set(): time.sleep(1) def stop_time_triggered(): print(Fore.YELLOW + "Stop time of " + self.stop_time.strftime("%H:%M") + " has been triggered, stopping..." + Fore.RESET) if args.resume_after is not None: resume_time = parse_time_str(args.resume_after) print(Fore.YELLOW + "Script will resume at " + resume_time.strftime("%H:%M") + Fore.RESET) wait_for_resume(resume_time) self.stop_time = None else: self.abort.set() if args.stop_after is not None: if self.stop_time is None: self.stop_time = parse_time_str(args.stop_after) print(Fore.YELLOW + "Script will stop after " + self.stop_time.strftime("%H:%M") + Fore.RESET) if self.stop_time < datetime.now(): stop_time_triggered() # we also wait if the "play token" was lost elif self.play_token_resume.is_set(): resume_time = parse_time_str(args.play_token_resume) print(Fore.YELLOW + "Script will resume at " + resume_time.strftime("%H:%M") + Fore.RESET) wait_for_resume(resume_time) self.play_token_resume.clear() def load_link(self, uri): # ignore if the uri is just blank (e.g. from a file) if not uri: return iter([]) link = self.session.get_link(uri) if link.type == spotify.LinkType.TRACK: track = link.as_track() return iter([track]) elif link.type == spotify.LinkType.PLAYLIST: self.current_playlist = link.as_playlist() attempt_count = 1 while self.current_playlist is None: if attempt_count > 3: print(Fore.RED + "Could not load playlist..." + Fore.RESET) return iter([]) print("Attempt " + str(attempt_count) + " failed: Spotify " + "returned None for playlist, trying again in 5 " + "seconds...") time.sleep(5.0) self.current_playlist = link.as_playlist() attempt_count += 1 print('Loading playlist...') self.current_playlist.load() return iter(self.current_playlist.tracks) elif link.type == spotify.LinkType.STARRED: link_user = link.as_user() def load_starred(): if link_user is not None: return self.session.get_starred(link_user.canonical_name) else: return self.session.get_starred() starred = load_starred() attempt_count = 1 while starred is None: if attempt_count > 3: print(Fore.RED + "Could not load starred playlist..." + Fore.RESET) return iter([]) print("Attempt " + str(attempt_count) + " failed: Spotify " + "returned None for starred playlist, trying again in " + "5 seconds...") time.sleep(5.0) starred = load_starred() attempt_count += 1 print('Loading starred playlist...') starred.load() return iter(starred.tracks) elif link.type == spotify.LinkType.ALBUM: album = link.as_album() album_browser = album.browse() print('Loading album browser...') album_browser.load() self.current_album = album return iter(album_browser.tracks) elif link.type == spotify.LinkType.ARTIST: artist = link.as_artist() artist_browser = artist.browse() print('Loading artist browser...') artist_browser.load() return iter(artist_browser.tracks) return iter([]) def search_query(self, query): print("Searching for query: " + query) try: result = self.session.search(query) result.load() except spotify.Error as e: print(str(e)) return iter([]) if len(result.tracks) == 0: print(Fore.RED + "No Results" + Fore.RESET) return iter([]) # list tracks print(Fore.GREEN + "Results" + Fore.RESET) for track_idx, track in enumerate(result.tracks): print(" " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" + to_ascii(track.album.name) + "] " + to_ascii(track.artists[0].name) + " - " + to_ascii(track.name) + " (" + str(track.popularity) + ")") pick = raw_input("Pick track(s) (ex 1-3,5): ") def get_track(i): if i >= 0 and i < len(result.tracks): return iter([result.tracks[i]]) return iter([]) pattern = re.compile("^[0-9 ,\-]+$") if pick.isdigit(): pick = int(pick) - 1 return get_track(pick) elif pick.lower() == "a" or pick.lower() == "all": return iter(result.tracks) elif pattern.match(pick): def range_string(comma_string): def hyphen_range(hyphen_string): x = [int(x) - 1 for x in hyphen_string.split('-')] return range(x[0], x[-1] + 1) return itertools.chain( *[hyphen_range(r) for r in comma_string.split(',')]) picks = sorted(set(list(range_string(pick)))) return itertools.chain(*[get_track(p) for p in picks]) if pick != "": print(Fore.RED + "Invalid selection" + Fore.RESET) return iter([]) def on_music_delivery(self, session, audio_format, frame_bytes, num_frames): try: self.rip_queue.put_nowait((audio_format.sample_rate, frame_bytes, num_frames)) except queue.Full: print(Fore.RED + "rip_queue is full. dropped music data" + Fore.RESET) return num_frames def on_connection_state_changed(self, session): if session.connection.state is spotify.ConnectionState.LOGGED_IN: self.login_success = True self.logged_in.set() self.logged_out.clear() elif session.connection.state is spotify.ConnectionState.LOGGED_OUT: self.logged_in.clear() self.ripper_continue.clear() self.logged_out.set() def on_logged_in(self, session, error): if error is spotify.ErrorType.OK: print("Logged in as " + session.user.display_name) else: error_map = { 9: "CLIENT_TOO_OLD", 8: "UNABLE_TO_CONTACT_SERVER", 6: "BAD_USERNAME_OR_PASSWORD", 7: "USER_BANNED", 15: "USER_NEEDS_PREMIUM", 16: "OTHER_TRANSIENT", 10: "OTHER_PERMANENT" } print("Logged in failed: " + error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error))) self.login_success = False self.logged_in.set() def play_token_lost(self, session): if self.args.play_token_resume is not None: print("\n" + Fore.RED + "Play token lost, waiting " + self.args.play_token_resume + " to resume..." + Fore.RESET) self.play_token_resume.set() self.skip.set() else: print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET) self.abort_rip() def on_end_of_track(self, session): self.session.player.play(False) self.end_of_track.set() def login_as_user(self, user, password): """login into Spotify""" self.session.login(user, password, remember_me=True) self.logged_in.wait() def login_as_last(self): """login as the previous logged in user""" try: self.session.relogin() self.logged_in.wait() except spotify.Error as e: self.login_success = False print(str(e)) def logout(self): """logout from Spotify""" time.sleep(0.1) if self.logged_in.is_set(): print('Logging out...') self.session.logout() self.logged_out.wait() def format_track_path(self, idx, track): args = self.args # check if we cached the result already track.load() if track.link.uri in self.track_path_cache: return self.track_path_cache[track.link.uri] audio_file = \ format_track_string(self, args.format.strip(), idx, track) # in case the file name is too long def truncate(_str, max_size): return _str[:max_size].strip() if len(_str) > max_size else _str def truncate_dir_path(dir_path): path_tokens = dir_path.split(os.sep) path_tokens = [truncate(token, 255) for token in path_tokens] return os.sep.join(path_tokens) def truncate_file_name(file_name): tokens = file_name.rsplit(os.extsep, 1) if len(tokens) > 1: tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1) else: tokens[0] = truncate(tokens[0], 255) return os.extsep.join(tokens) # ensure each component in path is no more than 255 chars long if args.windows_safe: tokens = audio_file.rsplit(os.sep, 1) if len(tokens) > 1: audio_file = os.path.join( truncate_dir_path(tokens[0]), truncate_file_name(tokens[1])) else: audio_file = truncate_file_name(tokens[0]) # replace filename if args.replace is not None: audio_file = self.replace_filename(audio_file, args.replace) # remove not allowed characters in filename (windows) if args.windows_safe: audio_file = re.sub('[:"*?<>|]', '', audio_file) # prepend base_dir audio_file = to_ascii(os.path.join(base_dir(), audio_file)) if args.normalized_ascii: audio_file = to_normalized_ascii(audio_file) # create directory if it doesn't exist audio_path = os.path.dirname(audio_file) if not path_exists(audio_path): os.makedirs(enc_str(audio_path)) self.track_path_cache[track.link.uri] = audio_file return audio_file def replace_filename(self, filename, pattern_list): for pattern in pattern_list: repl = pattern.split('/') filename = re.sub(repl[0], repl[1], filename) return filename def prepare_rip(self, idx, track): args = self.args # reset progress self.progress.prepare_track(track) if self.progress.total_tracks > 1: print(Fore.GREEN + "[ " + str(self.progress.track_idx) + " / " + str(self.progress.total_tracks + self.progress.skipped_tracks) + " ] Ripping " + track.link.uri + Fore.WHITE + "\t(ESC to skip)" + Fore.RESET) else: print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET) print(Fore.CYAN + self.audio_file + Fore.RESET) file_size = calc_file_size(track) print("Track Download Size: " + format_size(file_size)) if args.output_type == "wav" or args.plus_wav: audio_file = change_file_extension(self.audio_file, "wav") if \ args.output_type != "wav" else self.audio_file wav_file = audio_file if sys.version_info >= (3, 0) \ else enc_str(audio_file) self.wav_file = wave.open(wav_file, "wb") self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed')) if args.output_type == "pcm" or args.plus_pcm: audio_file = change_file_extension(self.audio_file, "pcm") if \ args.output_type != "pcm" else self.audio_file self.pcm_file = open(enc_str(audio_file), 'wb') audio_file_enc = enc_str(self.audio_file) if args.output_type == "flac": self.rip_proc = Popen( ["flac", "-f", ("-" + str(args.comp)), "--silent", "--endian", "little", "--channels", "2", "--bps", "16", "--sample-rate", "44100", "--sign", "signed", "-o", audio_file_enc, "-"], stdin=PIPE) elif args.output_type == "aiff": self.rip_proc = Popen( ["sox", "-q", "--endian", "little", "--channels", "2", "--bits", "16", "--rate", "44100", "--encoding", "unsigned-integer", "-t", "raw", "-", audio_file_enc], stdin=PIPE) elif args.output_type == "alac.m4a": self.rip_proc = Popen( ["avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar", "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-", "-acodec", "alac", audio_file_enc], stdin=PIPE) elif args.output_type == "ogg": if args.cbr: self.rip_proc = Popen( ["oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o", audio_file_enc, "-"], stdin=PIPE) else: self.rip_proc = Popen( ["oggenc", "--quiet", "--raw", "-q", args.vbr, "-o", audio_file_enc, "-"], stdin=PIPE) elif args.output_type == "opus": if args.cbr: self.rip_proc = Popen( ["opusenc", "--quiet", "--comp", args.comp, "--cvbr", "--bitrate", str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100", "-", audio_file_enc], stdin=PIPE) else: self.rip_proc = Popen( ["opusenc", "--quiet", "--comp", args.comp, "--vbr", "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-", audio_file_enc], stdin=PIPE) elif args.output_type == "aac": if self.dev_null is None: self.dev_null = open(os.devnull, 'wb') if args.cbr: self.rip_proc = Popen( ["faac", "-P", "-X", "-b", args.bitrate, "-o", audio_file_enc, "-"], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) else: self.rip_proc = Popen( ["faac", "-P", "-X", "-q", args.vbr, "-o", audio_file_enc, "-"], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null) elif args.output_type == "m4a": if args.cbr: self.rip_proc = Popen( ["fdkaac", "-S", "-R", "-b", args.bitrate, "-o", audio_file_enc, "-"], stdin=PIPE) else: self.rip_proc = Popen( ["fdkaac", "-S", "-R", "-m", args.vbr, "-o", audio_file_enc, "-"], stdin=PIPE) elif args.output_type == "mp3": lame_args = ["lame", "--silent"] if args.stereo_mode is not None: lame_args.extend(["-m", args.stereo_mode]) if args.cbr: lame_args.extend(["-cbr", "-b", args.bitrate]) else: lame_args.extend(["-V", args.vbr]) lame_args.extend(["-h", "-r", "-", audio_file_enc]) self.rip_proc = Popen(lame_args, stdin=PIPE) if self.rip_proc is not None: self.pipe = self.rip_proc.stdin self.ripping.set() def finish_rip(self, track): self.progress.end_track() if self.pipe is not None: print(Fore.GREEN + 'Rip complete' + Fore.RESET) self.pipe.flush() self.pipe.close() # wait for process to end before continuing ret_code = self.rip_proc.wait() if ret_code != 0: print( Fore.YELLOW + "Warning: encoder returned non-zero " "error code " + str(ret_code) + Fore.RESET) self.rip_proc = None self.pipe = None if self.wav_file is not None: self.wav_file.close() self.wav_file = None if self.pcm_file is not None: self.pcm_file.flush() os.fsync(self.pcm_file.fileno()) self.pcm_file.close() self.pcm_file = None self.ripping.clear() def rip(self, session, sample_rate, frame_bytes, num_frames): if self.ripping.is_set(): self.progress.update_progress(num_frames, sample_rate) if self.pipe is not None: self.pipe.write(frame_bytes) if self.wav_file is not None: self.wav_file.writeframes(frame_bytes) if self.pcm_file is not None: self.pcm_file.write(frame_bytes) def abort_rip(self): self.ripping.clear() self.abort.set()