def test_specifying_pos_args_until_limit(self, app_token): client = Spotify(app_token, max_limits_on=True) s1, = client.search('piano', ('track', ), None, None) with client.max_limits(False): s2, = client.search('piano', ('track', ), None, None) assert s1.limit > s2.limit
def __init__(self, token): super().__init__() self.sp_user = Spotify(token) app_name = 'SpotiCLI' version = '1.20.0917.dev' ###define app parameters self.app_info = f'{Fore.CYAN}{Style.BRIGHT}\n{app_name} {version}{Style.RESET_ALL}' self.intro = self.app_info + '\n' self.prompt = f'{Fore.GREEN}{Style.BRIGHT}spoticli ~$ {Style.RESET_ALL}' self.current_endpoint = '' self.api_delay = 0.3 #hide built-in cmd2 functions. this will leave them available for use but will be hidden from tab completion (and docs) self.hidden_commands.append('alias') self.hidden_commands.append('unalias') self.hidden_commands.append('set') self.hidden_commands.append('edit') self.hidden_commands.append('history') self.hidden_commands.append('load') self.hidden_commands.append('macro') self.hidden_commands.append('py') self.hidden_commands.append('pyscript') self.hidden_commands.append('quit') self.hidden_commands.append('shell') self.hidden_commands.append('shortcuts') self.hidden_commands.append('_relative_load') self.hidden_commands.append('run_pyscript') self.hidden_commands.append('run_script') self.debug = True
def __init__(self, config, app=None): self.config = config self.loop = asyncio.get_event_loop() self.app = app self.device = None self.user_pause = False self.task = None self.active = False self.running = True self.info = {} self.user_info = {} self.token = None self.spotify = Spotify() self.last_update = None self.credentials = Credentials(self.config.client_id, self.config.client_secret, self.config.client_redirect_uri) self.sender = RetryingSender(sender=AsyncSender()) self.playback_device_name = self.config.default_device self.user_info = {} self.now_playing_data = {} self.loop.run_until_complete(self.load_auth()) self.backup_playlist = self.loop.run_until_complete( self.load_and_confirm('backup_playlist')) self.user_playlist = self.loop.run_until_complete( self.load_and_confirm('user_playlist')) self.recent_picks = self.loop.run_until_complete( self.load_and_confirm('recent_picks')) self.blacklist = self.loop.run_until_complete(self.load_blacklist()) self.idle_timeout = 300 self.current_track_user = None
def test_turning_on_max_limits_returns_more(self, app_token): client = Spotify(app_token) s1, = client.search('piano') with client.max_limits(True): s2, = client.search('piano') assert s1.limit < s2.limit
def test_turning_off_max_limits_returns_less(self, app_token): client = Spotify(app_token, max_limits_on=True) s1, = client.search('piano') with client.max_limits(False): s2, = client.search('piano') assert s1.limit > s2.limit
def __init__(self, token: RefreshingToken) -> None: super().__init__() self.artist = "" self.title = "" self.is_playing = False self._position = 0 self._token = token self._spotify = Spotify(self._token) self._event_timestamp = time.time()
def get_artists_data(spotify_client: tk.Spotify, artist_ids: List[str]) -> pd.DataFrame: """ Given a spotify-client and list of artist-ids, compiles a dataframe with metadata for all the artists. The compiled metadata contains the following fields for each artist: 1. artist-id 2. artist-name 3. artist-genre - if artist has multiple generes, there will a row for the artist for each genre 4. artist-popularity - (0-100) measure of artist's popularity """ # Query Spotify for artists metadata artists_metadata = spotify_client.artists( artist_ids) if len(artist_ids) > 0 else [] artist_dict_list = [] for artist in artists_metadata: artist_dict = {} soundprintutils.update_dict_by_schema(artist_dict, ArtisterCommon.ARTIST_ID, artist.id) soundprintutils.update_dict_by_schema(artist_dict, ArtisterCommon.ARTIST_NAME, artist.name) soundprintutils.update_dict_by_schema(artist_dict, ArtisterCommon.ARTIST_POPULARITY, artist.popularity) genres = artist.genres artist_dict_list += soundprintutils.normalize_dict_field_list( artist_dict, genres, ArtisterCommon.ARTIST_GENRE) return pd.DataFrame(artist_dict_list, columns=ArtisterCommon.SCHEMA)
def test_modify_default_sender_instance(self): instance = MagicMock() from tekore import sender, Spotify sender.default_sender_instance = instance s = Spotify() self.assertIs(s.sender, instance)
def test_modify_default_sender_type(self): instance = MagicMock() type_mock = MagicMock(return_value=instance) from tekore import sender, Spotify sender.default_sender_type = type_mock s = Spotify() self.assertIs(s.sender, instance)
def test_instance_has_precedence_over_type(self): instance = MagicMock() type_mock = MagicMock(return_value=MagicMock()) from tekore import sender, Spotify sender.default_sender_type = type_mock sender.default_sender_instance = instance s = Spotify() self.assertIs(s.sender, instance)
def get_song_spotify(app_token): global loopnum global attempts spotify = Spotify(app_token) playlist = spotify.playlist(input("enter spotify playlist id")) # Spotify pLaylist id goes here while_loop = True while while_loop: try: track = spotify.playlist_tracks(playlist.id).items[loopnum].track song = track.name artist = track.artists artist = artist[0] artist = artist.name full = str(song) + ' by ' + str(artist) print(str(loopnum + 1) + '. ' + full) get_song_youtube(full) except IndexError: while_loop = False print("Completed Successfully")
def get_tracks_played_after(spotify_client: tk.Spotify, after_timestamp_ms: int) -> pd.DataFrame: """ Using the spotify-client, this function will query Spotify Web API to get recently played tracks after the point of time specified by the provided timestamp. The results are extracted, parsed and returned in a pandas DataFrame with schema according to ListenerCommon#SCHEMA. The column for ListenerCommon#LISTENED_TIME is not populated for any row and is NaN :param spotify_client: Client with access token to query Spotify Web API :param after_timestamp_ms: Epoch time in milliseconds after which recently played tracks are to be queried :return: pandas DataFrame with relevant fields of recently played soundtracks on Spotify """ df = pd.DataFrame([], columns=ListenerCommon.SCHEMA) response = spotify_client.playback_recently_played(limit=50, after=after_timestamp_ms) while len(response.items) > 0: df = df.append(pd.DataFrame(extract_playback_info(response.items), columns=ListenerCommon.SCHEMA)) if response.cursors is None: break else: next_after_ms = int(response.cursors.after) response = spotify_client.playback_recently_played(limit=50, after=next_after_ms) return df
def get_song_spotify(app_token): global attempts spotify = Spotify(app_token) playlist_id_spotify = input("Enter the spotify playlist id") playlist = spotify.playlist_items(playlist_id_spotify, as_tracks=True) print(playlist) playlist = playlist["items"] print(playlist) try: i = 0 songIds = [] whileLoop = True # Gets the song ids from the returned dictionary while whileLoop: subPlaylist = playlist[i] subPlaylist.pop("added_at", None) subPlaylist.pop("added_by", None) subPlaylist.pop("is_local", None) subPlaylist.pop("primary_color", None) subPlaylist = subPlaylist["track"] subPlaylist.pop("album", None) subPlaylist.pop("artists", None) subPlaylist.pop("available_markets", None) subPlaylist = subPlaylist["id"] print(subPlaylist) songIds.append(subPlaylist) i += 1 except IndexError: pass for i in range(len(songIds)): track = spotify.track(songIds[i], market=None) artist = track.artists artist = artist[0] print(f"{track.name} by {artist.name}") get_song_youtube(f"{track.name} by {artist.name}")
async def load_auth(self): try: token_contents = await FileUtils.get_data_from_file( self.config.data_directory, 'token') #conf=(self.config["client_id"], self.config["client_secret"], self.config["client_redirect_uri"]) #if isfile(os.path.join(self.config['base_directory'], 'token.json')): # with open(os.path.join(self.config['base_directory'], 'token.json'),'r') as jsonfile: # token_contents=json.loads(jsonfile.read()) #self.token = RefreshingToken(None, self.credentials) #self.token.refresh_user_token(conf,token_contents['refresh_token']) #logger.info('Using refresh Token: %s' % token_contents['refresh_token']) token = self.credentials.refresh_user_token( token_contents['refresh_token']) #logger.info('pre Token: %s' % token) self.token = RefreshingToken(token, self.credentials) self.spotify = Spotify(token=self.token, sender=self.sender, max_limits_on=True) #else: # logger.error('!! Error token file not found and no refresh token available.') except: logger.error('.. Error loading token', exc_info=True)
async def set_token(self, code): try: logger.info('.. Setting token from code: %s...' % code[:10]) self.code = code token = self.credentials.request_user_token(code) self.token = RefreshingToken(token, self.credentials) logger.info('.. Token is now: %s' % self.token) await self.save_auth(token=self.token, code=self.code) self.spotify = Spotify(token=self.token, sender=self.sender, max_limits_on=True) #await self.monitor_token() # This is currently removed for troubleshooting when a device gets picked #if self.spotify: # if not self.device: # await self.set_playback_device(self.config['default_device']) await self.update_list('update') await self.update_now_playing() except: logger.error('Error setting token from code %s' % code[:10], exc_info=True)
async def startup() -> None: global _credentials, _api _credentials = Credentials( conf.CLIENT_ID, conf.CLIENT_SECRET, conf.REDIRECT_URI, sender=AsyncSender( AsyncClient(http2=True, timeout=conf.SPOTIFY_API_TIMEOUT)), asynchronous=True, ) _api = Spotify( sender=AsyncSender( AsyncClient(http2=True, timeout=conf.SPOTIFY_API_TIMEOUT)), asynchronous=True, ) await database.connect() _logger.info("melodiam-auth started")
def get_albums_data(spotify_client: tk.Spotify, album_ids: List[str]) -> pd.DataFrame: """ Given a spotify-client and list of album-ids, compiles a dataframe with metadata for all the albums. The compiled metadata contains the following fields for each album: 1. album-id 2. album-name 3. album-type - album/single/compilation 4. genre - if album has multiple generes, there will a row for the album for each genre 5. album-release-date 6. label - Music label for releasing the album 7. album-track-count - total number of tracks in album 8. album-popularity - (0-100) number signifying album's popularity """ # Query Spotify for albums metadata albums_metadata = spotify_client.albums( album_ids) if len(album_ids) > 0 else [] album_dict_list = [] for album in albums_metadata: album_dict = {} soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.ALBUM_ID, album.id) soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.TYPE, album.album_type) soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.LABEL, album.label) soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.NAME, album.name) soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.POPULARITY, album.popularity) soundprintutils.update_dict_by_schema( album_dict, AlbumerCommon.RELEASE_DATE, pd.to_datetime(album.release_date).timestamp()) soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.TOTAL_TRACKS, album.total_tracks) genres = album.genres album_dict_list += soundprintutils.normalize_dict_field_list( album_dict, genres, AlbumerCommon.GENRE) return pd.DataFrame(album_dict_list, columns=AlbumerCommon.SCHEMA)
class sofa_spotify_controller(object): def __init__(self, config, app=None): self.config = config self.loop = asyncio.get_event_loop() self.app = app self.device = None self.user_pause = False self.task = None self.active = False self.running = True self.info = {} self.user_info = {} self.token = None self.spotify = Spotify() self.last_update = None self.credentials = Credentials(self.config.client_id, self.config.client_secret, self.config.client_redirect_uri) self.sender = RetryingSender(sender=AsyncSender()) self.playback_device_name = self.config.default_device self.user_info = {} self.now_playing_data = {} self.loop.run_until_complete(self.load_auth()) self.backup_playlist = self.loop.run_until_complete( self.load_and_confirm('backup_playlist')) self.user_playlist = self.loop.run_until_complete( self.load_and_confirm('user_playlist')) self.recent_picks = self.loop.run_until_complete( self.load_and_confirm('recent_picks')) self.blacklist = self.loop.run_until_complete(self.load_blacklist()) self.idle_timeout = 300 self.current_track_user = None async def start(self): try: nowplaying = await self.update_now_playing() except: logger.error('.! error starting initial nowplaying check', exc_info=True) self.active = False @property def auth_url(self): try: return self.credentials.user_authorisation_url( scope=tekore.scope.every) except: logger.error('.. error retrieving authorization url', exc_info=True) return "" async def load_blacklist(self): try: return await FileUtils.get_data_from_file( self.config.data_directory, 'blacklist') except: logger.error('!! could not load blacklist') return [] async def load_and_confirm(self, list_name): # Load queues from disk and ensure they have a selection_tracker uuid to help deal with unique key requirements in the client playlist = {} try: playlist = await FileUtils.get_data_from_file( self.config.data_directory, list_name) if playlist == None: playlist = {'tracks': []} if type(playlist) == list: logger.error('!! Error - legacy format playlist for %s' % list_name) playlist = {"tracks": playlist} await FileUtils.save_data_to_file(self.config.data_directory, list_name, playlist) if 'tracks' in playlist and len(playlist['tracks']) > 0: for item in playlist['tracks']: if 'selection_tracker' not in item: item['selection_tracker'] = str(uuid.uuid4()) else: playlist['tracks'] = [] except: logger.error('!! error loading and checking list: %s' % listname) return playlist async def load_auth(self): try: token_contents = await FileUtils.get_data_from_file( self.config.data_directory, 'token') #conf=(self.config["client_id"], self.config["client_secret"], self.config["client_redirect_uri"]) #if isfile(os.path.join(self.config['base_directory'], 'token.json')): # with open(os.path.join(self.config['base_directory'], 'token.json'),'r') as jsonfile: # token_contents=json.loads(jsonfile.read()) #self.token = RefreshingToken(None, self.credentials) #self.token.refresh_user_token(conf,token_contents['refresh_token']) #logger.info('Using refresh Token: %s' % token_contents['refresh_token']) token = self.credentials.refresh_user_token( token_contents['refresh_token']) #logger.info('pre Token: %s' % token) self.token = RefreshingToken(token, self.credentials) self.spotify = Spotify(token=self.token, sender=self.sender, max_limits_on=True) #else: # logger.error('!! Error token file not found and no refresh token available.') except: logger.error('.. Error loading token', exc_info=True) async def save_auth(self, token=None, code=None): try: token_data = { 'last_code': code, "type": token.token_type, "access_token": token.access_token, "refresh_token": token.refresh_token, "expires_at": token.expires_at } logger.info('.. saving token data: %s' % token_data) async with aiofiles.open( os.path.join(self.config.data_directory, 'token.json'), 'w') as f: await f.write(json.dumps(token_data)) except: logger.error('.. Error saving token and code' % (token[:10], code[:10]), exc_info=True) async def set_token(self, code): try: logger.info('.. Setting token from code: %s...' % code[:10]) self.code = code token = self.credentials.request_user_token(code) self.token = RefreshingToken(token, self.credentials) logger.info('.. Token is now: %s' % self.token) await self.save_auth(token=self.token, code=self.code) self.spotify = Spotify(token=self.token, sender=self.sender, max_limits_on=True) #await self.monitor_token() # This is currently removed for troubleshooting when a device gets picked #if self.spotify: # if not self.device: # await self.set_playback_device(self.config['default_device']) await self.update_list('update') await self.update_now_playing() except: logger.error('Error setting token from code %s' % code[:10], exc_info=True) def authenticated(func): def wrapper(self): #logger.info('checking authentication') if self.token and self.spotify: return func(self) else: logger.info('must be authenticated before using spotify API') #return False raise AuthorizationNeeded return wrapper async def get_user(self): try: if self.token: #logger.info('user: %s' % await self.spotify.current_user()) userobj = await self.spotify.current_user() return userobj.asbuiltin() except tekore.client.decor.error.Unauthorised: logger.error('.. Invalid access token: %s' % self.token.access_token) except: logger.error('.. error getting user info', exc_info=True) return {} async def add_blacklist_term(self, term): if not self.blacklist: self.blacklist = [] if term not in self.blacklist: self.blacklist.append(term) logger.info('.. added %s to blacklist' % term) await FileUtils.save_data_to_file(self.config.data_directory, 'blacklist', self.blacklist) return True return False async def remove_blacklist_term(self, term): if not self.blacklist: self.blacklist = [] if self.blacklist and term in self.blacklist: self.blacklist.remove(term) logger.info('.. removed %s from blacklist' % term) await FileUtils.save_data_to_file(self.config.data_directory, 'blacklist', self.blacklist) return True return False async def filter_tracks_blacklist(self, tracks): results = [] for track in tracks: artist_title = ("%s %s" % (track['artist'], track['name'])).lower() if any(word.lower() in artist_title for word in self.blacklist): logger.info('.. filtered result: %s' % artist_title) else: results.append(track) return results async def restart_local_playback_device(self): # This allows you to select a playback device by name try: stdoutdata = subprocess.getoutput("systemctl restart raspotify") logger.info('>> restart local playback device %s' % stdoutdata) return True except: logger.error('Error restarting local playback', exc_info=True) return False async def set_playback_device(self, name, restart=True): # This allows you to select a playback device by name try: # try to restart the local spotifyd since it tends to fail over time devs = await self.spotify.playback_devices() for dev in devs: if dev.id == name or dev.name == name: logger.info('.. transferring to device %s (%s)' % (dev.name, dev.id)) await self.spotify.playback_transfer(dev.id) self.device = dev.id self.playback_device_name = dev.name return True logger.info('did not find local playback device %s. restarting' % name) await self.restart_local_playback_device() await asyncio.sleep(2) devs = await self.spotify.playback_devices() for dev in devs: if dev.name == name: logger.info('transferring to %s' % dev.id) await self.spotify.playback_transfer(dev.id) self.device = dev.id self.playback_device_name = dev.name return True return False except: logger.error('Error setting playback device to %s' % name, exc_info=True) async def check_playback_devices(self): # This allows you to select a playback device by name try: devs = await self.spotify.playback_devices() for dev in devs: logger.info('Device: %s' % dev) except: logger.error('Error checking playback devices', exc_info=True) return False async def check_playback_device(self): try: devs = await self.spotify.playback_devices() for dev in devs: if dev.is_active: return True except: logger.error('Error checking playback device', exc_info=True) return False @authenticated async def get_active_playback_device(self): # This allows you to select a playback device by name try: devs = await self.spotify.playback_devices() for dev in devs: #logger.info('dev: %s %s' % (dev.is_active, dev)) if dev.is_active: return { "name": dev.name, "id": dev.id, "volume": dev.volume_percent } except: logger.error('Error checking playback devices', exc_info=True) return {} @authenticated async def get_playback_devices(self): try: result = [] devs = await self.spotify.playback_devices() for dev in devs: result.append({ "name": dev.name, "id": dev.id, "is_active": dev.is_active, "volume_percent": dev.volume_percent }) except: logger.error('!! Error listing playback devices', exc_info=True) return result async def get_user_playlist(self, name): try: playlists = await self.spotify.followed_playlists() for playlist in playlists.items: if playlist.name == name: logger.info('found playlist: %s %s' % (playlist.name, playlist.owner)) return {"name": playlist.name, "id": playlist.id} return {} except: logger.error('Error searching spotify', exc_info=True) return {} async def get_playlist_source_data(self, id, list_type): if list_type == "playlist": playlist = await self.spotify.playlist(id) covers = await self.spotify.playlist_cover_image(playlist.id) if len(covers) > 0: cover = covers[0].url return { "name": playlist.name, "art": cover, "owner": playlist.owner.id, "display": playlist.name } if list_type == "radio": track = await self.spotify.track(id) return { "name": track.name, "art": track.album.images[0].url, "artist": track.artists[0].name, "album": track.album.name, "display": track.name + " - " + track.artists[0].name + " radio" } return {} async def get_user_playlists(self): try: display_list = [] playlists = await self.spotify.followed_playlists() #playlists = self.spotify.followed_playlists() for playlist in playlists.items: #logger.info('found playlist: %s %s' % (playlist.name, playlist.owner.id)) try: cover = "" covers = await self.spotify.playlist_cover_image( playlist.id) if len(covers) > 0: cover = covers[0].url except concurrent.futures._base.CancelledError: logger.error('Error getting cover for %s (cancelled)' % playlist.name) except: logger.error('Error getting cover for %s' % playlist.name, exc_info=True) display_list.append({ "name": playlist.name, "id": playlist.id, "art": cover, "owner": playlist.owner.id }) return display_list except: logger.error('Error getting user playlists from spotify', exc_info=True) return [] async def get_playlist_tracks(self, id): try: display_list = [] #playlist = self.spotify.playlist(id) tracks = await self.spotify.playlist_items(id) #tracks = await self.spotify.playlist_tracks(id) tracks = self.spotify.all_items(tracks) logger.info('.. Tracks: %s' % tracks) async for track in tracks: display_list.append({ "id": track.track.id, 'selection_tracker': str(uuid.uuid4()), "name": track.track.name, "art": track.track.album.images[0].url, "artist": track.track.artists[0].name, "album": track.track.album.name, "url": track.track.href }) return display_list except: logger.error('Error getting spotify playlist tracks', exc_info=True) return [] async def add_radio(self, song_id, limit=50): try: #track = await self.spotify.track(song_id) recommendations = await self.spotify.recommendations( track_ids=[song_id], limit=limit) #logger.info('recommendations: %s' % recommendations) track_list = [] for track in recommendations.tracks: pltrack = { "id": track.id, "name": track.name, "art": track.album.images[0].url, "artist": track.artists[0].name, "album": track.album.name, "url": track.href, "votes": 1, "count": 0 } logger.info('Adding track: %s - %s' % (pltrack['artist'], pltrack['name'])) track_list.append(pltrack) track_list = await self.filter_tracks_blacklist(track_list) self.backup_playlist = { "id": song_id, "type": "radio", "tracks": list(track_list) } self.backup_playlist.update(await self.get_playlist_source_data( song_id, "radio")) await FileUtils.save_data_to_file(self.config.data_directory, 'backup_playlist', self.backup_playlist) return track_list except: logger.error('Error setting backup playlist from radio track %s' % song_id, exc_info=True) return [] async def search(self, search, types=('track', ), limit=20): try: display_list = [] result = await self.spotify.search(search, types=types, limit=limit) for track in result[0].items: display_list.append({ "id": track.id, "name": track.name, "art": track.album.images[0].url, "artist": track.artists[0].name, "album": track.album.name, "url": track.href }) display_list = await self.filter_tracks_blacklist(display_list) return display_list except: logger.error('Error searching spotify', exc_info=True) return [] async def add_track_to_playlist(self, song_id, playlist_id): try: #playlist=await self.get_user_playlist("Discovered") #playlist_id=playlist['id'] track = await self.spotify.track(song_id) track_data = self.get_track_data(track) await self.spotify.playlist_add(playlist_id, [track.uri]) return track_data except: logger.error('Error adding tracks to playlist %s' % dir(track), exc_info=True) return {} async def add_to_recent_picks(self, track, user=None): recent = [] if 'tracks' in self.recent_picks: for prev_pick in self.recent_picks['tracks']: logger.info('prev: %s' % prev_pick) if prev_pick['id'] != track.id: recent.append(prev_pick) new_track = { "id": track.id, "name": track.name, "art": track.album.images[0].url, "artist": track.artists[0].name, "album": track.album.name, "url": track.href, "user": user, "time": datetime.now().isoformat() } recent.append(new_track) self.recent_picks['tracks'] = recent[-25:] await FileUtils.save_data_to_file(self.config.data_directory, 'recent_picks', {self.recent_picks}) async def add_track(self, song_id, user=None): try: track = await self.spotify.track(song_id) track_json = { "id": track.id, "name": track.name, "art": track.album.images[0].url, "artist": track.artists[0].name, "album": track.album.name, "url": track.href, "user": user, "time": datetime.now().isoformat() } logger.info('Adding track for %s: %s - %s' % (user, track_json['artist'], track_json['name'])) self.user_playlist['tracks'].append(track_json) self.user_playlist['tracks'] = await self.filter_tracks_blacklist( self.user_playlist['tracks']) await FileUtils.save_data_to_file(self.config.data_directory, 'user_playlist', self.user_playlist) self.loop.create_task(self.add_to_recent_picks(track, user)) await self.update_list('update') track_data = await self.get_track_data( track) # get json version for returning to web user return track_data except: logger.error('Error adding song %s' % song_id, exc_info=True) return {} async def del_track(self, song_id): try: remove_count = 0 newlist = [] for song in self.user_playlist['tracks']: if song['id'] != song_id: logger.info('Adding non-delete: %s vs %s' % (song['id'], song_id)) newlist.append(song) else: remove_count += 1 self.user_playlist['tracks'] = newlist await FileUtils.save_data_to_file(self.config.data_directory, 'user_playlist', self.user_playlist) #self.app.saveJSON('user_playlist', self.user_playlist) newlist = [] for song in self.backup_playlist['tracks']: if song['id'] != song_id: newlist.append(song) else: remove_count += 1 self.backup_playlist['tracks'] = newlist await FileUtils.save_data_to_file(self.config.data_directory, 'backup_playlist', self.backup_playlist) #self.app.saveJSON('backup_playlist', self.backup_playlist) await self.update_list('update') return {"removed": remove_count} except: logger.error('Error adding song %s' % song_id, exc_info=True) return [] async def shuffle_backup(self): try: promoted_list = [] working_backup = [] ids = [] for item in self.backup_playlist['tracks']: if item['id'] not in ids: ids.append(item['id']) else: logger.info('dupe track: %s' % item) if 'promoted' in item and item['promoted'] == True: promoted_list.append(item) else: working_backup.append(item) random.shuffle(working_backup) self.backup_playlist['tracks'] = promoted_list + working_backup #logger.info('.. new backup list: %s' % self.backup_playlist) return self.backup_playlist except: logger.error('Error shuffling backup list', exc_info=True) return [] async def get_queue(self): return {'user': self.user_playlist, 'backup': self.backup_playlist} async def get_recent_picks(self): return {"recent": self.recent_picks} async def clear_queue(self): self.user_playlist = {} self.backup_playlist = {} async def list_next_tracks(self, maxcount=5): try: next_tracks = [] next_tracks = self.user_playlist['tracks'][:maxcount] if len(next_tracks) < maxcount: remaining = maxcount - len(next_tracks) next_tracks = next_tracks + self.backup_playlist[ 'tracks'][:remaining] return next_tracks except: logger.error('Error getting next tracks', exc_info=True) return [] async def update_list(self, action): try: nowplaying = await self.now_playing() await self.app.update({'playlist': action}) #await self.app.server.add_sse_update({'playlist':action}) except: logger.error('Error updating now playing subscribers', exc_info=True) return [] async def get_track_data(self, track): try: #logger.info('track type: %s' % track.json()) if not track: return {} item = getattr(track, 'item', track) data = { "id": item.id, "name": item.name, "art": item.album.images[0].url, "artist": item.artists[0].name, "album": item.album.name, "url": item.href, "is_playing": getattr(track, 'is_playing', False), "length": int(item.duration_ms / 1000), "position": int(getattr(track, 'progress_ms', 0) / 1000) } return data except: logger.error('.. error getting track data from %s' % track, exc_info=True) return {} async def now_playing(self): try: nowplaying = {} npdata = None #pb=await self.spotify.playback_recently_played() #logger.info('test: %s' % pb) if await self.check_playback_device(): npdata = await self.spotify.playback_currently_playing() else: await self.set_playback_device('jukebox') recent = await self.spotify.playback_recently_played(limit=1) npdata = recent.items[0].track nowplaying = await self.get_track_data(npdata) if self.current_track_user: nowplaying['user'] = self.current_track_user except requests.exceptions.HTTPError: logger.warn('.. Token may have expired: %s' % self.token) self.active = False except tekore.Unauthorised: logger.error( '!! Error - Unauthorized to Spotify - token may be missing or expired.' ) self.active = False except: logger.error('Error getting now playing', exc_info=True) self.active = False return nowplaying async def pause(self): try: if await self.check_playback_device(): logger.info('-> sending pause to spotify') await self.spotify.playback_pause() await self.update_now_playing() self.user_pause = True return True except: logger.error('Error pausing', exc_info=True) return False async def play(self): try: if not await self.get_active_playback_device(): #logger.error('!! error - no playback device: %s' % await self.get_active_playback_device()) await self.set_playback_device(self.playback_device_name) if not await self.get_active_playback_device(): return False logger.info(".. playing on %s" % await self.get_active_playback_device()) playing = await self.spotify.playback_currently_playing() if playing.item == None: await self.next_track() try: await self.spotify.playback_resume() except tekore.Forbidden: logger.error('!! error - could not resume playback', exc_info=True) await self.next_track() self.active = True await self.update_now_playing() self.user_pause = False return True # TODO: need handler for this error: # tekore.client.decor.error.NotFound: Error in https://api.spotify.com/v1/me/player/play: # 404: Player command failed: No active device found # Requires an active device and the user has none. except: logger.error('Error playing', exc_info=True) return False async def set_backup_playlist(self, playlist_id): try: track_list = await self.get_playlist_tracks(playlist_id) for item in track_list: item['selection_tracker'] = str(uuid.uuid4()) self.backup_playlist = { "id": playlist_id, "type": "playlist", "tracks": list(track_list) } self.backup_playlist.update(await self.get_playlist_source_data( playlist_id, "playlist")) await FileUtils.save_data_to_file(self.config.data_directory, 'backup_playlist', self.backup_playlist) return track_list except: logger.error('Error setting backup playlist', exc_info=True) return [] async def get_playlist(self, playlist_id): try: track_list = await self.get_playlist_tracks(playlist_id) return list(track_list) except: logger.error('Error setting backup playlist', exc_info=True) return [] async def track_ready(self): try: if 'tracks' in self.user_playlist and len( self.user_playlist['tracks']) > 0: return True if 'tracks' in self.backup_playlist and len( self.backup_playlist['tracks']) > 0: return True except: logger.error('Error checking for ready track from queues', exc_info=True) return False async def get_next_track(self): try: next_track = {} next_track = await self.pop_user_track() if next_track: self.current_track_user = next_track['user'] logger.info('.. pulling user track: %s - %s' % (next_track['artist'], next_track['name'])) else: next_track = await self.pop_backup_track() if next_track: self.current_track_user = None logger.info('.. pulling backup track: %s - %s' % (next_track['artist'], next_track['name'])) return next_track except: logger.error('!! Error getting next track from queues', exc_info=True) return {} async def pop_user_track(self): try: if 'tracks' in self.user_playlist and len( self.user_playlist['tracks']) > 0: next_track = self.user_playlist['tracks'].pop(0) await FileUtils.save_data_to_file(self.config.data_directory, 'user_playlist', self.user_playlist) return next_track else: return {} except: logger.error('Error getting track from backup playlist') return {} async def pop_backup_track(self): try: if 'tracks' in self.backup_playlist and len( self.backup_playlist['tracks']) > 0: next_track = self.backup_playlist['tracks'].pop(0) await FileUtils.save_data_to_file(self.config.data_directory, 'backup_playlist', self.backup_playlist) #self.app.saveJSON('backup_playlist', self.backup_playlist) return next_track else: return {} except: logger.error('Error getting track from backup playlist', exc_info=True) return {} async def promote_backup_track(self, song_id, super_promote=False): try: newlist = [] promoted_track = None promoted_count = 0 for song in self.backup_playlist['tracks']: if song['id'] == song_id: promoted_track = song else: if 'promoted' in song and song['promoted'] == True: promoted_count += 1 newlist.append(song) if promoted_track: if super_promote: result = await self.add_track(promoted_track['id']) else: promoted_track['promoted'] = True if promoted_count == 0: newlist.insert(0, promoted_track) else: newlist.insert(promoted_count, promoted_track) self.backup_playlist['tracks'] = newlist await FileUtils.save_data_to_file(self.config.data_directory, 'backup_playlist', self.backup_playlist) await self.update_list('update') return {"promoted": song_id} except: logger.error('Error adding song %s' % song_id, exc_info=True) return [] async def next_track(self): try: next_track = await self.get_next_track() if next_track: self.active = True result = await self.play_id(next_track['id']) await self.update_list('pop') await self.update_now_playing() return result else: logger.warning('!! No more tracks to play') self.active = False except: logger.error('Error trying to play', exc_info=True) self.active = False async def play_id(self, id): try: result = await self.spotify.playback_start_tracks([id]) self.active = True return result except: logger.error('!! Error trying to play id %s' % id, exc_info=True) self.active = False return False async def seek_pos(self, position): try: logger.info('.. seeking to %s / %s' % (int(position) * 1000, self.now_playing_data['nowplaying']['length'])) await self.spotify.playback_seek(int(position) * 1000) result = await self.update_now_playing() self.active = True return result except: logger.error('!! Error trying to seek to position %s' % position, exc_info=True) self.active = False def stop(self): self.task.cancel() async def update_now_playing(self, event=None): try: try: iso = self.last_update.isoformat() except: iso = None track_change = False if "nowplaying" in self.now_playing_data: now_playing_data = dict(self.now_playing_data["nowplaying"]) if event == None: now_playing_data = await self.now_playing() logger.info('.. Updating nowplaying data: %s' % now_playing_data) else: if event['player_event'] == 'change': logger.info('.. event change: %s / %s' % (event, now_playing_data)) if event['track_id'] == event['old_track_id']: track_change = True elif 'track_id' in self.now_playing_data[ 'nowplaying'] and event[ 'track_id'] != now_playing_data['track_id']: now_playing_data = await self.now_playing() logger.info('.. Updating nowplaying data: %s' % now_playing_data) elif event['player_event'] == "playing": logger.info( '.. Updating nowplaying data play state only') now_playing_data['is_playing'] = True elif event['player_event'] == "paused": logger.info( '.. Updating nowplaying data paused state only') now_playing_data['is_playing'] = False else: now_playing_data = await self.now_playing() logger.info('.. Updating nowplaying data: %s' % now_playing_data) idle = True if 'is_playing' in now_playing_data: if now_playing_data['is_playing']: self.active = True idle = False self.last_update = datetime.now() # or now_playing_data['position']==0 if track_change and not self.user_pause: if not event or event['old_track_id'] == event['track_id']: logger.info( '.. track ended: %s - %s' % (now_playing_data['artist'], now_playing_data['name'])) self.active = False idle = False await self.next_track() # If there is a next track, it should trigger the librespot reply if self.last_update and (datetime.now() - self.last_update ).total_seconds() < self.idle_timeout: idle = False #new_data={"idle": idle, "user_pause": self.user_pause, "last_update": iso, "nowplaying": now_playing_data, "device": await self.get_active_playback_device(), "devices": await self.get_playback_devices() } new_data = { "idle": idle, "user_pause": self.user_pause, "last_update": iso, "nowplaying": now_playing_data } await self.app.update(new_data) self.now_playing_data = new_data return self.now_playing_data except: logger.error('Error updating now playing subscribers', exc_info=True) return {} async def control(self, command, params=None): # consolidating most of the media controls down to a single control function that calls the actual tekore/spotify # commands. Only commands that return updated now playing data should be used through control. Other reporting # commands should use specific functions. # Each of these commands needs to be reviewed for duplication of the update_now_playing data if command == "nowplaying": pass # no command needed, update now playing will run elif command == "play": result = await self.app.spotify_controller.play() elif command == "pause": result = await self.app.spotify_controller.pause() elif command == "next": result = await self.app.spotify_controller.next_track() elif command == "seek": result = await self.app.spotify_controller.seek_pos(params[0]) elif command == "set_device": result = await self.app.spotify_controller.set_playback_device( params[0]) else: return {"error": "invalid command: %s" % command} #result = await self.app.spotify_controller.update_now_playing() if not self.now_playing_data: result = await self.app.spotify_controller.update_now_playing() else: result = self.now_playing_data #logger.info('.. nowplaying update: %s / %s' % (command, result)) return result
def client(): return Spotify('token')
def test_request_with_closed_client_raises(self): client = Spotify() client.close() with pytest.raises(RuntimeError): client.track('id')
async def test_request_with_closed_async_client_raises(self): client = Spotify(asynchronous=True) await client.close() with pytest.raises(RuntimeError): await client.track('id')
def test_specifying_limit_kwarg_overrides_max_limits(self, app_token): client = Spotify(app_token, max_limits_on=True) s, = client.search('piano', limit=1) assert s.limit == 1
def test_specifying_limit_pos_arg_overrides_max_limits(self, app_token): client = Spotify(app_token, max_limits_on=True) s, = client.search('piano', ('track', ), None, None, 1) assert s.limit == 1
def test_chunked_context_disables(self): client = Spotify(chunked_on=True) with client.chunked(False): assert client.chunked_on is False
def test_chunked_context_enables(self): client = Spotify() with client.chunked(True): assert client.chunked_on is True
def test_returns_model_list(self, app_token, track_ids): client = Spotify(app_token, chunked_on=True) tracks = client.tracks(track_ids) assert isinstance(tracks, ModelList)
class SpotifyWebAPI(APIBase): player_name: str = "Spotify" artist: str = None title: str = None is_playing: bool = None def __init__(self, token: RefreshingToken) -> None: super().__init__() self.artist = "" self.title = "" self.is_playing = False self._position = 0 self._token = token self._spotify = Spotify(self._token) self._event_timestamp = time.time() def connect_api(self) -> None: self._refresh_metadata() @property def position(self) -> int: """ _refresh_metadata() has to be called because the song position is constantly changing. """ self._refresh_metadata() return self._position def _refresh_metadata(self) -> None: """ Refreshes the metadata of the player: artist, title, whether it's playing or not, and the current position. """ metadata = self._spotify.playback_currently_playing() if metadata is None or metadata.item is None: raise ConnectionNotReady("No song currently playing") self.artist = metadata.item.artists[0].name self.title = metadata.item.name # Some local songs don't have an artist name so `split_title` # is called in an attempt to manually get it from the title. if self.artist == '': self.artist, self.title = split_title(self.title) self._position = metadata.progress_ms self.is_playing = metadata.is_playing def event_loop(self) -> None: """ The event loop callback that checks if changes happen. This is called periodically within the Qt window. It checks for changes in: * The playback status (playing/paused) to change the player's too * The currently playing song: if a new song started, it's played * The position """ # Previous properties are saved to compare them with the new ones # after the metadata refresh artist = self.artist title = self.title position = self._position is_playing = self.is_playing self._refresh_metadata() # First checking if a new song started, so that position or status # changes are related to the new song. if self.artist != artist or self.title != title: logging.info("New video detected") self.new_song_signal.emit(self.artist, self.title, 0) if self.is_playing != is_playing: logging.info("Status change detected") self.status_signal.emit(self.is_playing) # The position difference between calls is compared to the elapsed # time to know whether the position has been modified. # Changes will be ignored unless the position difference is # greater than the elapsed time (plus a margin) or if it's negative # (backwards). playback_diff = self._position - position calls_diff = int((time.time() - self._event_timestamp) * 1000) if playback_diff >= (calls_diff + 100) or playback_diff < 0: logging.info("Position change detected") self.position_signal.emit(self._position) # The time passed between calls is refreshed self._event_timestamp = time.time()
def test_too_many_chunked_succeeds(self, app_token, track_ids): client = Spotify(app_token, chunked_on=True) tracks = client.tracks(track_ids) assert len(track_ids) == len(tracks)
def __init__(self, api_key): self.api_key = api_key client_id, client_secret = api_key.split(':') self.token = request_client_token(client_id, client_secret) self.spotify = Spotify(self.token)
async def test_async_too_many_chunked_succeeds(self, app_token, track_ids): client = Spotify(app_token, chunked_on=True, asynchronous=True) tracks = await client.tracks(track_ids) assert len(track_ids) == len(tracks) await client.close()