def link(self): """A :class:`Link` to the playlist.""" if not self.is_loaded: raise spotify.Error('The playlist must be loaded to create a link') sp_link = lib.sp_link_create_from_playlist(self._sp_playlist) if sp_link == ffi.NULL: if not self.is_in_ram: raise spotify.Error( 'The playlist must have been in RAM to create a link') # XXX Figure out why we can still get NULL here even if # the playlist is both loaded and in RAM. raise spotify.Error('Failed to get link from Spotify playlist') return spotify.Link(self._session, sp_link=sp_link, add_ref=False)
def __init__(self, session, uri=None, sp_playlist=None, add_ref=True): super(Playlist, self).__init__() assert uri or sp_playlist, 'uri or sp_playlist is required' self._session = session if uri is not None: playlist = spotify.Link(self._session, uri).as_playlist() if playlist is None: raise spotify.Error( 'Failed to get playlist from Spotify URI: %r' % uri) sp_playlist = playlist._sp_playlist session._cache[sp_playlist] = self add_ref = True if add_ref: lib.sp_playlist_add_ref(sp_playlist) self._sp_playlist = ffi.gc(sp_playlist, lib.sp_playlist_release) self._sp_playlist_callbacks = None # Make sure we remove callbacks in __del__() using the same lib as we # added callbacks with. self._lib = lib
def __getitem__(self, key): # Required by collections.Sequence if isinstance(key, slice): return list(self).__getitem__(key) if not isinstance(key, int): raise TypeError('list indices must be int or slice, not %s' % key.__class__.__name__) if key < 0: key += self.__len__() if not 0 <= key < self.__len__(): raise IndexError('list index out of range') playlist_type = PlaylistType( lib.sp_playlistcontainer_playlist_type(self._sp_playlistcontainer, key)) if playlist_type is PlaylistType.PLAYLIST: sp_playlist = lib.sp_playlistcontainer_playlist( self._sp_playlistcontainer, key) return spotify.Playlist._cached(self._session, sp_playlist, add_ref=True) elif playlist_type in (PlaylistType.START_FOLDER, PlaylistType.END_FOLDER): return PlaylistFolder( id=lib.sp_playlistcontainer_playlist_folder_id( self._sp_playlistcontainer, key), name=utils.get_with_fixed_buffer( 100, lib.sp_playlistcontainer_playlist_folder_name, self._sp_playlistcontainer, key), type=playlist_type) else: raise spotify.Error('Unknown playlist type: %r' % playlist_type)
def clear_unseen_tracks(self, playlist): """Clears unseen tracks from the given ``playlist``.""" result = lib.sp_playlistcontainer_clear_unseen_tracks( self._sp_playlistcontainer, playlist._sp_playlist ) if result == -1: raise spotify.Error('Failed clearing unseen tracks')
def test_repr_if_link_creation_fails(self, link_mock, lib_mock): lib_mock.sp_playlist_is_loaded.return_value = 1 link_mock.side_effect = spotify.Error('error message') sp_playlist = spotify.ffi.cast('sp_playlist *', 42) playlist = spotify.Playlist(self.session, sp_playlist=sp_playlist) result = repr(playlist) self.assertEqual(result, 'Playlist(<error: error message>)')
def _get_more_tracks(self): self._sp_tracks_len = min(self._num_tracks, self._sp_tracks_len + self._BATCH_SIZE) self._sp_tracks = ffi.new('sp_track *[]', self._sp_tracks_len) self._num_tracks = lib.sp_playlistcontainer_get_unseen_tracks( self._sp_playlistcontainer, self._sp_playlist, self._sp_tracks, self._sp_tracks_len) if self._num_tracks < 0: raise spotify.Error('Failed to get unseen tracks for playlist')
def test_lookup_when_offline(session_mock, sp_track_mock, provider, caplog): session_mock.get_link.return_value = sp_track_mock.link sp_track_mock.link.as_track.return_value.load.side_effect = spotify.Error( 'Must be online to load objects') results = provider.lookup('spotify:track:abc') assert len(results) == 0 assert ( 'Failed to lookup "spotify:track:abc": Must be online to load objects' in caplog.text())
def load(session, obj, timeout=None): """Block until the object's data is loaded. If the session isn't logged in, a :exc:`spotify.Error` is raised as Spotify objects cannot be loaded without being online and logged in. The ``obj`` must at least have the :attr:`is_loaded` attribute. If it also has an :meth:`error` method, it will be checked for errors to raise. After ``timeout`` seconds with no results :exc:`~spotify.Timeout` is raised. If unspecified, the ``timeout`` defaults to 10s. Any timeout is better than no timeout, since no timeout would cause programs to potentially hang forever without any information to help debug the issue. The method returns ``self`` to allow for chaining of calls. """ _check_error(obj) if obj.is_loaded: return obj if session.connection.state is not spotify.ConnectionState.LOGGED_IN: raise spotify.Error( 'Session must be logged in and online to load objects: %r' % session.connection.state) if timeout is None: timeout = 10 deadline = time.time() + timeout while not obj.is_loaded: session.process_events() _check_error(obj) if obj.is_loaded: return obj if time.time() > deadline: raise spotify.Timeout(timeout) # Instead of sleeping for a very short time and making a tight loop # here, one might be tempted to sleep for the time returned by the # session.process_events() call above. If no event loop is running, # that could lead to very long waits (up to a minute or so) since no # one is doing anything on "notify_main_thread" session callbacks, # which is intended to interrupt the sleep interval prescribed by # session.process_events(). Thus, it is better to make this loop tight. time.sleep(0.001) _check_error(obj) return obj
def __init__( self, session, canonical_username=None, tracks=None, message='', callback=None, sp_inbox=None, add_ref=True, ): assert ( canonical_username and tracks or sp_inbox ), 'canonical_username and tracks, or sp_inbox, is required' self._session = session self.loaded_event = threading.Event() if sp_inbox is None: canonical_username = utils.to_char(canonical_username) if isinstance(tracks, spotify.Track): tracks = [tracks] message = utils.to_char(message) handle = ffi.new_handle((self._session, self, callback)) self._session._callback_handles.add(handle) sp_inbox = lib.sp_inbox_post_tracks( self._session._sp_session, canonical_username, [t._sp_track for t in tracks], len(tracks), message, _inboxpost_complete_callback, handle, ) add_ref = True if sp_inbox == ffi.NULL: raise spotify.Error('Inbox post request failed to initialize') if add_ref: lib.sp_inbox_add_ref(sp_inbox) self._sp_inbox = ffi.gc(sp_inbox, lib.sp_inbox_release)
def add_new_playlist(self, name, index=None): """Add an empty playlist with ``name`` at the given ``index``. The playlist name must not be space-only or longer than 255 chars. If the ``index`` isn't specified, the new playlist is added at the end of the container. Returns the new playlist. """ self._validate_name(name) sp_playlist = lib.sp_playlistcontainer_add_new_playlist( self._sp_playlistcontainer, utils.to_char(name)) if sp_playlist == ffi.NULL: raise spotify.Error('Playlist creation failed') playlist = spotify.Playlist._cached(self._session, sp_playlist, add_ref=True) if index is not None: self.move_playlist(self.__len__() - 1, index) return playlist
def __init__(self, canonical_username=None, tracks=None, message='', callback=None, sp_inbox=None, add_ref=True): assert canonical_username and tracks or sp_inbox, \ 'canonical_username and tracks, or sp_inbox, is required' self.complete_event = threading.Event() self._callback_handles = set() if sp_inbox is None: canonical_username = utils.to_char(canonical_username) if isinstance(tracks, spotify.Track): tracks = [tracks] message = utils.to_char(message) handle = ffi.new_handle((callback, self)) # TODO Think through the life cycle of the handle object. Can it # happen that we GC the search and handle object, and then later # the callback is called? self._callback_handles.add(handle) sp_inbox = lib.sp_inbox_post_tracks( spotify.session_instance._sp_session, canonical_username, [t._sp_track for t in tracks], len(tracks), message, _inboxpost_complete_callback, handle) add_ref = True if sp_inbox == ffi.NULL: raise spotify.Error('Inbox post request failed to initialize') if add_ref: lib.sp_inbox_add_ref(sp_inbox) self._sp_inbox = ffi.gc(sp_inbox, lib.sp_inbox_release)
def setup(self): while not self._end.is_set(): try: self.current = self.playlist.get(timeout=10) if not self.current.is_loaded: self.current.load() self.current.artists.load() self.session.player.load(self.current) self.update_playing(track=self.current) timeout_count = 0 maxrate = "4000k" bufsize = "8000k" # cmd = "ffmpeg -f s16le -codec:a pcm_s16le -ac 2 -ar 44100 -i - " \ # "-f s16le -ar {} -ac {} -maxrate {} -bufsize {} -loglevel warning pipe:1".format( # self.voice.encoder.sampling_rate, # self.voice.encoder.channels, # maxrate, # bufsize # ) # args = shlex.split(cmd) # self.proc = Popen(args, stdin=PIPE, stdout=PIPE) # self.player = ProcessPlayer(self.proc, self.voice, None) #TODO: Test this before = "-f s16le -codec:a pcm_s16le -ac 2 -ar 44100" options = "-maxrate {} -bufsize {}".format(maxrate, bufsize) self.player = self.voice.create_ffmpeg_player(PIPE, pipe=True, before_options=before, options=options) self.proc = self.player.process self._ready.set() self.session.player.play() time.sleep(1) self.player.start() while not self.frame_queue.empty(): try: while self._pause.is_set(): if self._end.is_set(): self._pause.clear() break else: time.sleep(1) if self._end.is_set() or self._skip.is_set(): self.frame_queue.clear() self._skip.clear() break play_item = self.frame_queue.get(timeout=1) if self._ready.is_set(): self.proc.stdin.write(play_item[1]) except queue.Empty: print("Timeout ", timeout_count) timeout_count += 1 if timeout_count > 30: raise spotify.Error("Timeout while playing track.") except queue.Empty: continue except (spotify.Error, Exception) as e: if isinstance(e, spotify.Error): print("Spotify error!") print(repr(e)) self.session.player.unload() self.cleanup() return self.cleanup()
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 # check if we were passed a file name or search if len(args.uri) == 1 and 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): current_playlist = None current_album = None current_chart = None if isinstance(uri, list): return uri else: if (args.exclude_appears_on and uri.startswith("spotify:artist:")): album_uris = self.web.get_non_appears_on_albums(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 = [ t["track"]["uri"] for t in charts["entries"]["items"] ] 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 = 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.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: 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): 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(): print("\n" + 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) 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 # 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 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 private self.session.social.private_session = True # list of spotify URIs uris = args.uri # calculate total size and time all_tracks = [] uri_tracks = {} for uri in uris: tracks = list(self.get_tracks_from_uri(uri)) uri_tracks[uri] = tracks 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 = uri_tracks[uri] """acepta una playlist album para sincronizar, aunque es al pedo""" 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) self.progress.track_idx += 1 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(track.link.uri) self.progress.track_idx += 1 continue self.session.player.load(track) self.prepare_rip(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(track.link.uri) # 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() sys.exit()
def _lookup_playlist(config, session, web_client, uri): playlist = playlists.playlist_lookup(session, web_client, uri, config["bitrate"]) if playlist is None: raise spotify.Error("Playlist Web API lookup failed") return playlist.tracks
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 error(self): return spotify.Error(spotify.Error.OK)
def test_error_is_an_exception(self): error = spotify.Error(0) self.assertIsInstance(error, Exception)