def album_from_title_artist(title: str, artists: List[str], sp: spotipy.Spotify) -> Optional[Album]: """ Return an album """ q = query(title, artists) if q in search_memo: search_result = search_memo[q] else: search_result = sp.search(q, type="album", limit=50) print(f"Key error looking up the query {q}") album_id = find_album_id_from_search(search_result, artists) if album_id: try: tracks = album_tracks_memo[album_id] except KeyError: tracks = sp.album_tracks(album_id) print(f"Key error looking up the track with id {album_id}") return Album( title, album_id, artists, [ Song( title=item["name"], uri=item["uri"], features={}, artists=[artist["name"] for artist in item["artists"]], ) for item in tracks["items"] ], ) return None
def cache(album_descriptions: List[AlbumDescription], sp: spotipy.Spotify) -> None: """ Cache the results for the given album descriptions for fast lookup later. Calling this before using the spotify methods on a list of albums will improve performance. """ global search_memo global album_tracks_memo global feature_memo missed = [False, False, False] for title, artists in album_descriptions: q = query(title, artists) if q not in search_memo: missed[0] = True search_memo[q] = sp.search(query(title, artists), type="album", limit=50) album_id = find_album_id_from_search(search_memo[q], artists) if album_id is not None and album_id not in album_tracks_memo: missed[1] = True album_tracks_memo[album_id] = sp.album_tracks(album_id) if missed[0]: set_memo(search_memo, "search") if missed[1]: set_memo(album_tracks_memo, "tracks") songs = get_songs(album_descriptions, sp) for songs_chunk in util.chunks(iter(songs), 100): seenAllSongs = True song_links = [song["uri"] for song in songs_chunk] for link in song_links: if link not in feature_memo: seenAllSongs = False if not seenAllSongs: missed[2] = True feature_list = sp.audio_features(song_links) for uri, features in zip(song_links, feature_list): feature_memo[uri] = features if missed[2]: set_memo(feature_memo, "features")
def play( self, client: spotipy.Spotify, spotify_device_id:str, uri:str, random_song:bool, position:str, ignore_fully_played:str, country_code:str=None ) -> None: _LOGGER.debug( "Playing URI: %s on device-id: %s", uri, spotify_device_id, ) if uri.find("show") > 0: show_episodes_info = client.show_episodes(uri, market=country_code) if show_episodes_info and len(show_episodes_info["items"]) > 0: if ignore_fully_played: for episode in show_episodes_info["items"]: if not episode["resume_point"]["fully_played"]: episode_uri = episode["external_urls"]["spotify"] break else: episode_uri = show_episodes_info["items"][0]["external_urls"][ "spotify" ] _LOGGER.debug( "Playing episode using uris (latest podcast playlist)= for uri: %s", episode_uri, ) client.start_playback(device_id=spotify_device_id, uris=[episode_uri]) elif uri.find("episode") > 0: _LOGGER.debug("Playing episode using uris= for uri: %s", uri) client.start_playback(device_id=spotify_device_id, uris=[uri]) elif uri.find("track") > 0: _LOGGER.debug("Playing track using uris= for uri: %s", uri) client.start_playback(device_id=spotify_device_id, uris=[uri]) else: if uri == "random": _LOGGER.debug( "Cool, you found the easter egg with playing a random playlist" ) playlists = client.user_playlists("me", 50) no_playlists = len(playlists["items"]) uri = playlists["items"][random.randint(0, no_playlists - 1)]["uri"] kwargs = {"device_id": spotify_device_id, "context_uri": uri} if random_song: if uri.find("album") > 0: results = client.album_tracks(uri, market=country_code) position = random.randint(0, results["total"] - 1) elif uri.find("playlist") > 0: results = client.playlist_tracks(uri) position = random.randint(0, results["total"] - 1) _LOGGER.debug("Start playback at random position: %s", position) if uri.find("artist") < 1: kwargs["offset"] = {"position": position} _LOGGER.debug( 'Playing context uri using context_uri for uri: "%s" (random_song: %s)', uri, random_song, ) client.start_playback(**kwargs)
class TestSpotipy(unittest.TestCase): """ These tests require user authentication - provide client credentials using the following environment variables :: 'SPOTIPY_CLIENT_USERNAME' 'SPOTIPY_CLIENT_ID' 'SPOTIPY_CLIENT_SECRET' 'SPOTIPY_REDIRECT_URI' """ creep_urn = 'spotify:track:3HfB5hBU0dmBt8T0iCmH42' creep_id = '3HfB5hBU0dmBt8T0iCmH42' creep_url = 'http://open.spotify.com/track/3HfB5hBU0dmBt8T0iCmH42' el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK' pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT' weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL' radiohead_urn = 'spotify:artist:4Z8W4fKeB5YxbusRsdQVPb' angeles_haydn_urn = 'spotify:album:1vAbqAeuJVWNAe7UR00bdM' bad_id = 'BAD_ID' @classmethod def setUpClass(self): missing = list(filter(lambda var: not os.getenv(CCEV[var]), CCEV)) if missing: raise Exception( 'Please set the client credentials for the test application using the following environment variables: {}' .format(CCEV.values())) self.username = os.getenv(CCEV['client_username']) self.scope = 'user-library-read' self.token = prompt_for_user_token(self.username, scope=self.scope) self.spotify = Spotify(auth=self.token) def test_artist_urn(self): artist = self.spotify.artist(self.radiohead_urn) self.assertTrue(artist['name'] == 'Radiohead') def test_artists(self): results = self.spotify.artists([self.weezer_urn, self.radiohead_urn]) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 2) def test_album_urn(self): album = self.spotify.album(self.pinkerton_urn) self.assertTrue(album['name'] == 'Pinkerton') def test_album_tracks(self): results = self.spotify.album_tracks(self.pinkerton_urn) self.assertTrue(len(results['items']) == 10) def test_album_tracks_many(self): results = self.spotify.album_tracks(self.angeles_haydn_urn) tracks = results['items'] total, received = results['total'], len(tracks) while received < total: results = self.spotify.album_tracks(self.angeles_haydn_urn, offset=received) tracks.extend(results['items']) received = len(tracks) self.assertEqual(received, total) def test_albums(self): results = self.spotify.albums( [self.pinkerton_urn, self.pablo_honey_urn]) self.assertTrue('albums' in results) self.assertTrue(len(results['albums']) == 2) def test_track_urn(self): track = self.spotify.track(self.creep_urn) self.assertTrue(track['name'] == 'Creep') def test_track_id(self): track = self.spotify.track(self.creep_id) self.assertTrue(track['name'] == 'Creep') def test_track_url(self): track = self.spotify.track(self.creep_url) self.assertTrue(track['name'] == 'Creep') def test_track_bad_urn(self): try: track = self.spotify.track(self.el_scorcho_bad_urn) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_tracks(self): results = self.spotify.tracks([self.creep_url, self.el_scorcho_urn]) self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']) == 2) def test_artist_top_tracks(self): results = self.spotify.artist_top_tracks(self.weezer_urn) self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']) == 10) def test_artist_related_artists(self): results = self.spotify.artist_related_artists(self.weezer_urn) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 20) for artist in results['artists']: if artist['name'] == 'Jimmy Eat World': found = True self.assertTrue(found) def test_artist_search(self): results = self.spotify.search(q='weezer', type='artist') self.assertTrue('artists' in results) self.assertTrue(len(results['artists']['items']) > 0) self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer') def test_artist_search_with_market(self): results = self.spotify.search(q='weezer', type='artist', market='GB') self.assertTrue('artists' in results) self.assertTrue(len(results['artists']['items']) > 0) self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer') def test_artist_albums(self): results = self.spotify.artist_albums(self.weezer_urn) self.assertTrue('items' in results) self.assertTrue(len(results['items']) > 0) found = False for album in results['items']: if album['name'] == 'Hurley': found = True self.assertTrue(found) def test_search_timeout(self): sp = Spotify(auth=self.token, requests_timeout=.01) try: results = sp.search(q='my*', type='track') self.assertTrue(False, 'unexpected search timeout') except requests.Timeout: self.assertTrue(True, 'expected search timeout') def test_album_search(self): results = self.spotify.search(q='weezer pinkerton', type='album') self.assertTrue('albums' in results) self.assertTrue(len(results['albums']['items']) > 0) self.assertTrue( results['albums']['items'][0]['name'].find('Pinkerton') >= 0) def test_track_search(self): results = self.spotify.search(q='el scorcho weezer', type='track') self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']['items']) > 0) self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') def test_user(self): user = self.spotify.user(user='******') self.assertTrue(user['uri'] == 'spotify:user:plamere') def test_track_bad_id(self): try: track = self.spotify.track(self.bad_id) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_track_bad_id(self): try: track = self.spotify.track(self.bad_id) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_unauthenticated_post_fails(self): with self.assertRaises(SpotifyException) as cm: self.spotify.user_playlist_create("spotify", "Best hits of the 90s") self.assertTrue(cm.exception.http_status == 401 or cm.exception.http_status == 403) def test_custom_requests_session(self): sess = requests.Session() sess.headers["user-agent"] = "spotipy-test" with_custom_session = Spotify(auth=self.token, requests_session=sess) self.assertTrue( with_custom_session.user(user="******")["uri"] == "spotify:user:akx") def test_force_no_requests_session(self): with_no_session = Spotify(auth=self.token, requests_session=False) self.assertFalse(isinstance(with_no_session._session, requests.Session)) self.assertTrue( with_no_session.user(user="******")["uri"] == "spotify:user:akx")
class AuthTestSpotipy(unittest.TestCase): """ These tests require client authentication - provide client credentials using the following environment variables :: 'SPOTIPY_CLIENT_ID' 'SPOTIPY_CLIENT_SECRET' """ playlist = "spotify:user:plamere:playlist:2oCEWyyAPbZp9xhVSxZavx" four_tracks = [ "spotify:track:6RtPijgfPKROxEzTHNRiDp", "spotify:track:7IHOIqZUUInxjVkko181PB", "4VrWlk8IQxevMvERoX08iC", "http://open.spotify.com/track/3cySlItpiPiIAzU3NyHCJf" ] two_tracks = [ "spotify:track:6RtPijgfPKROxEzTHNRiDp", "spotify:track:7IHOIqZUUInxjVkko181PB" ] other_tracks = [ "spotify:track:2wySlB6vMzCbQrRnNGOYKa", "spotify:track:29xKs5BAHlmlX1u4gzQAbJ", "spotify:track:1PB7gRWcvefzu7t3LJLUlf" ] bad_id = 'BAD_ID' creep_urn = 'spotify:track:6b2oQwSGFkzsMtQruIWm2p' creep_id = '6b2oQwSGFkzsMtQruIWm2p' creep_url = 'http://open.spotify.com/track/6b2oQwSGFkzsMtQruIWm2p' el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK' pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT' weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL' radiohead_urn = 'spotify:artist:4Z8W4fKeB5YxbusRsdQVPb' angeles_haydn_urn = 'spotify:album:1vAbqAeuJVWNAe7UR00bdM' heavyweight_urn = 'spotify:show:5c26B28vZMN8PG0Nppmn5G' heavyweight_id = '5c26B28vZMN8PG0Nppmn5G' heavyweight_url = 'https://open.spotify.com/show/5c26B28vZMN8PG0Nppmn5G' reply_all_urn = 'spotify:show:7gozmLqbcbr6PScMjc0Zl4' heavyweight_ep1_urn = 'spotify:episode:68kq3bNz6hEuq8NtdfwERG' heavyweight_ep1_id = '68kq3bNz6hEuq8NtdfwERG' heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG' reply_all_ep1_urn = 'spotify:episode:1KHjbpnmNpFmNTczQmTZlR' @classmethod def setUpClass(self): self.spotify = Spotify( client_credentials_manager=SpotifyClientCredentials()) self.spotify.trace = False def test_audio_analysis(self): result = self.spotify.audio_analysis(self.four_tracks[0]) assert ('beats' in result) def test_audio_features(self): results = self.spotify.audio_features(self.four_tracks) self.assertTrue(len(results) == len(self.four_tracks)) for track in results: assert ('speechiness' in track) def test_audio_features_with_bad_track(self): bad_tracks = ['spotify:track:bad'] input = self.four_tracks + bad_tracks results = self.spotify.audio_features(input) self.assertTrue(len(results) == len(input)) for track in results[:-1]: if track is not None: assert ('speechiness' in track) self.assertTrue(results[-1] is None) def test_recommendations(self): results = self.spotify.recommendations(seed_tracks=self.four_tracks, min_danceability=0, max_loudness=0, target_popularity=50) self.assertTrue(len(results['tracks']) == 20) def test_artist_urn(self): artist = self.spotify.artist(self.radiohead_urn) self.assertTrue(artist['name'] == 'Radiohead') def test_artists(self): results = self.spotify.artists([self.weezer_urn, self.radiohead_urn]) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 2) def test_album_urn(self): album = self.spotify.album(self.pinkerton_urn) self.assertTrue(album['name'] == 'Pinkerton') def test_album_tracks(self): results = self.spotify.album_tracks(self.pinkerton_urn) self.assertTrue(len(results['items']) == 10) def test_album_tracks_many(self): results = self.spotify.album_tracks(self.angeles_haydn_urn) tracks = results['items'] total, received = results['total'], len(tracks) while received < total: results = self.spotify.album_tracks(self.angeles_haydn_urn, offset=received) tracks.extend(results['items']) received = len(tracks) self.assertEqual(received, total) def test_albums(self): results = self.spotify.albums( [self.pinkerton_urn, self.pablo_honey_urn]) self.assertTrue('albums' in results) self.assertTrue(len(results['albums']) == 2) def test_track_urn(self): track = self.spotify.track(self.creep_urn) self.assertTrue(track['name'] == 'Creep') def test_track_id(self): track = self.spotify.track(self.creep_id) self.assertTrue(track['name'] == 'Creep') self.assertTrue(track['popularity'] > 0) def test_track_url(self): track = self.spotify.track(self.creep_url) self.assertTrue(track['name'] == 'Creep') def test_track_bad_urn(self): try: self.spotify.track(self.el_scorcho_bad_urn) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_tracks(self): results = self.spotify.tracks([self.creep_url, self.el_scorcho_urn]) self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']) == 2) def test_artist_top_tracks(self): results = self.spotify.artist_top_tracks(self.weezer_urn) self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']) == 10) def test_artist_related_artists(self): results = self.spotify.artist_related_artists(self.weezer_urn) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 20) for artist in results['artists']: if artist['name'] == 'Jimmy Eat World': found = True self.assertTrue(found) def test_artist_search(self): results = self.spotify.search(q='weezer', type='artist') self.assertTrue('artists' in results) self.assertTrue(len(results['artists']['items']) > 0) self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer') def test_artist_search_with_market(self): results = self.spotify.search(q='weezer', type='artist', market='GB') self.assertTrue('artists' in results) self.assertTrue(len(results['artists']['items']) > 0) self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer') def test_artist_search_with_multiple_markets(self): total = 5 countries_list = ['GB', 'US', 'AU'] countries_tuple = ('GB', 'US', 'AU') results_multiple = self.spotify.search_markets(q='weezer', type='artist', markets=countries_list) results_all = self.spotify.search_markets(q='weezer', type='artist') results_tuple = self.spotify.search_markets(q='weezer', type='artist', markets=countries_tuple) results_limited = self.spotify.search_markets(q='weezer', limit=3, type='artist', markets=countries_list, total=total) self.assertTrue( all('artists' in results_multiple[country] for country in results_multiple)) self.assertTrue( all('artists' in results_all[country] for country in results_all)) self.assertTrue( all('artists' in results_tuple[country] for country in results_tuple)) self.assertTrue( all('artists' in results_limited[country] for country in results_limited)) self.assertTrue( all( len(results_multiple[country]['artists']['items']) > 0 for country in results_multiple)) self.assertTrue( all( len(results_all[country]['artists']['items']) > 0 for country in results_all)) self.assertTrue( all( len(results_tuple[country]['artists']['items']) > 0 for country in results_tuple)) self.assertTrue( all( len(results_limited[country]['artists']['items']) > 0 for country in results_limited)) self.assertTrue( all(results_multiple[country]['artists']['items'][0]['name'] == 'Weezer' for country in results_multiple)) self.assertTrue( all(results_all[country]['artists']['items'][0]['name'] == 'Weezer' for country in results_all)) self.assertTrue( all(results_tuple[country]['artists']['items'][0]['name'] == 'Weezer' for country in results_tuple)) self.assertTrue( all(results_limited[country]['artists']['items'][0]['name'] == 'Weezer' for country in results_limited)) total_limited_results = 0 for country in results_limited: total_limited_results += len( results_limited[country]['artists']['items']) self.assertTrue(total_limited_results <= total) def test_artist_albums(self): results = self.spotify.artist_albums(self.weezer_urn) self.assertTrue('items' in results) self.assertTrue(len(results['items']) > 0) found = False for album in results['items']: if album['name'] == 'Hurley': found = True self.assertTrue(found) def test_search_timeout(self): client_credentials_manager = SpotifyClientCredentials() sp = spotipy.Spotify( requests_timeout=0.01, client_credentials_manager=client_credentials_manager) # depending on the timing or bandwidth, this raises a timeout or connection error" self.assertRaises( (requests.exceptions.Timeout, requests.exceptions.ConnectionError), lambda: sp.search(q='my*', type='track')) def test_album_search(self): results = self.spotify.search(q='weezer pinkerton', type='album') self.assertTrue('albums' in results) self.assertTrue(len(results['albums']['items']) > 0) self.assertTrue( results['albums']['items'][0]['name'].find('Pinkerton') >= 0) def test_track_search(self): results = self.spotify.search(q='el scorcho weezer', type='track') self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']['items']) > 0) self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') def test_user(self): user = self.spotify.user(user='******') self.assertTrue(user['uri'] == 'spotify:user:plamere') def test_track_bad_id(self): try: self.spotify.track(self.bad_id) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_show_urn(self): show = self.spotify.show(self.heavyweight_urn, market="US") self.assertTrue(show['name'] == 'Heavyweight') def test_show_id(self): show = self.spotify.show(self.heavyweight_id, market="US") self.assertTrue(show['name'] == 'Heavyweight') def test_show_url(self): show = self.spotify.show(self.heavyweight_url, market="US") self.assertTrue(show['name'] == 'Heavyweight') def test_show_bad_urn(self): with self.assertRaises(SpotifyException): self.spotify.show("bogus_urn", market="US") def test_shows(self): results = self.spotify.shows( [self.heavyweight_urn, self.reply_all_urn], market="US") self.assertTrue('shows' in results) self.assertTrue(len(results['shows']) == 2) def test_show_episodes(self): results = self.spotify.show_episodes(self.heavyweight_urn, market="US") self.assertTrue(len(results['items']) > 1) def test_show_episodes_many(self): results = self.spotify.show_episodes(self.reply_all_urn, market="US") episodes = results['items'] total, received = results['total'], len(episodes) while received < total: results = self.spotify.show_episodes(self.reply_all_urn, offset=received, market="US") episodes.extend(results['items']) received = len(episodes) self.assertEqual(received, total) def test_episode_urn(self): episode = self.spotify.episode(self.heavyweight_ep1_urn, market="US") self.assertTrue(episode['name'] == '#1 Buzz') def test_episode_id(self): episode = self.spotify.episode(self.heavyweight_ep1_id, market="US") self.assertTrue(episode['name'] == '#1 Buzz') def test_episode_url(self): episode = self.spotify.episode(self.heavyweight_ep1_url, market="US") self.assertTrue(episode['name'] == '#1 Buzz') def test_episode_bad_urn(self): with self.assertRaises(SpotifyException): self.spotify.episode("bogus_urn", market="US") def test_episodes(self): results = self.spotify.episodes( [self.heavyweight_ep1_urn, self.reply_all_ep1_urn], market="US") self.assertTrue('episodes' in results) self.assertTrue(len(results['episodes']) == 2) def test_unauthenticated_post_fails(self): with self.assertRaises(SpotifyException) as cm: self.spotify.user_playlist_create("spotify", "Best hits of the 90s") self.assertTrue(cm.exception.http_status == 401 or cm.exception.http_status == 403) def test_custom_requests_session(self): sess = requests.Session() sess.headers["user-agent"] = "spotipy-test" with_custom_session = spotipy.Spotify( client_credentials_manager=SpotifyClientCredentials(), requests_session=sess) self.assertTrue( with_custom_session.user(user="******")["uri"] == "spotify:user:akx") sess.close() def test_force_no_requests_session(self): with_no_session = spotipy.Spotify( client_credentials_manager=SpotifyClientCredentials(), requests_session=False) self.assertNotIsInstance(with_no_session._session, requests.Session) user = with_no_session.user(user="******") self.assertEqual(user["uri"], "spotify:user:akx")
class SpotifyService(MusicService): """ Spotify client music service. """ def __init__(self, state, client_id=None, client_secret=None, redirect_uri=None, device_name=None): """ @see Service.__init__() For the real meanings of the kwargs, read the Spotipy documentation: https://spotipy.readthedocs.io You can also set them as the environment variables:: * ``SPOTIPY_CLIENT_ID`` * ``SPOTIPY_CLIENT_SECRET`` * ``SPOTIPY_REDIRECT_URI`` :type client_id: str :param client_id: The Spotify client ID. :type client_secret: str :param client_secret: The Spotify client secret. :type redirect_uri: str :param redirect_uri: The Spotify redirect URI. :type device_name: str :param device_name: The name of the device to control, if not the default one. """ super(SpotifyService, self).__init__("SpotifyService", state, "Spotify") self._client_id = client_id self._client_secret = client_secret self._redirect_uri = redirect_uri self._device_name = device_name self._device_id = None self._spotify = None self._volume = None def set_volume(self, volume): """ @see MusicService.set_volume() """ if MIN_VOLUME <= volume <= MAX_VOLUME: self._volume = volume self._spotify.volume(100.0 * (volume - MIN_VOLUME) / (MAX_VOLUME - MIN_VOLUME), device_id=self._device_id) else: raise ValueError("Bad volume: %s", volume) def get_volume(): """ @see MusicService.get_volume() """ return self._volume def is_playing(self): """ Whether the player is playing. :rtype: bool :return: Whether the player is playing. """ try: cur = self._spotify.currently_playing() return cur['is_playing'] except: return False def play(self, uris): """ Set the list of URIs playing. """ LOG.info("Playing: %s", ' '.join(uris)) self._spotify.start_playback(uris=uris, device_id=self._device_id) def pause(self): """ Pause any currently playing music. """ self._spotify.pause_playback(device_id=self._device_id) def unpause(self): """ Resume any currently paused music. """ self._spotify.start_playback(device_id=self._device_id) def _start(self): """ @see Startable._start() """ # This is what we need to be able to do scope = ','.join(('user-library-read', 'user-read-playback-state', 'user-modify-playback-state')) # Create the authorization manager, and then use that to create the # client auth_manager = SpotifyOAuth(client_id=self._client_id, client_secret=self._client_secret, redirect_uri=self._redirect_uri, scope=scope) self._spotify = Spotify(auth_manager=auth_manager) # See what devices we have to hand try: if self._device_name is not None: LOG.info("Looking for device named '%s'", self._device_name) devices = self._spotify.devices() for device in devices['devices']: # Say what we see name = device['name'] id_ = device['id'] type_ = device['type'] active = device['is_active'] vol = device['volume_percent'] LOG.info("Found %sactive %s device: '%s'", '' if active else 'in', type_, name) # See if we're looking for a specific device, if not just snoop # the volume from the first active one if self._device_name is not None: if name == self._device_name: LOG.info("Matched '%s' to ID '%s'", name, id_) self._device_id = id_ self._volume = vol / 100.0 * MAX_VOLUME else: if active and self._volume is None: self._volume = vol / 100.0 * MAX_VOLUME except Exception as e: LOG.warning("Unable to determine active Spoify devices: %s", e) # If we were looking for a specific device then make sure that we found # it in the list if self._device_name is not None and self._device_id is None: raise ValueError("Failed to find device with name '%s'" % (self._device_name, )) def _stop(self): """ @see Startable._stop() """ try: self._spotify.pause_playback(device_id=self._device_id) except: # Best effort pass def _match_artist(self, artist): """ @see MusicService._match_artist() """ artist = ' '.join(artist).lower() LOG.debug("Matching artist '%s'", artist) result = self._spotify.search(artist, type='artist') if 'artists' in result and 'items' in result['artists']: items = result['artists']['items'] LOG.debug("Checking %d results", len(items)) for item in items: name = item.get('name', '').lower() LOG.debug("Matching against '%s'", name) if fuzz.ratio(name, artist) > 80: return True return False def _get_stop_handler(self, tokens): """ @see MusicService._get_stop_handler() """ return _SpotifyServicePauseHandler(self, tokens) def _get_play_handler(self, tokens): """ @see MusicService._get_play_handler() """ return _SpotifyServiceUnpauseHandler(self, tokens) def _get_toggle_pause_handler(self, tokens): """ @see MusicService._get_toggle_pause_handler() """ return _SpotifyServiceTogglePauseHandler(self, tokens) def _get_handler_for(self, tokens, platform_match, genre, artist, song_or_album): """ @see MusicService._get_handler_for() """ # Do nothing if we have no name if song_or_album is None or len(song_or_album) == 0: return None # Normalise to strings name = ' '.join(song_or_album).lower() if artist is None or len(artist) == 0: artist = None else: artist = ' '.join(artist).lower() # We will put all the track URIs in here uris = [] # Search by track name then album name, these are essentially the same # logic for which in ('track', 'album'): LOG.info("Looking for '%s'%s as a %s", name, " by '%s'" % artist if artist else '', which) # This is the key in the results plural = which + 's' # Try using the song_or_album as the name result = self._spotify.search(name, type=which) if not result: LOG.info("No results") continue # Did we get back any tracks if plural not in result: LOG.error("%s was not in result keys: %s", plural, result.keys()) continue # We got some results back, let's assign scores to them all results = result[plural] matches = [] for item in results.get('items', []): # It must have a uri if 'uri' not in item and item['uri']: LOG.error("No URI in %s", item) # Look at all the candidate entries if 'name' in item: # See if this is better than any existing match name_score = fuzz.ratio(name, item['name'].lower()) LOG.debug("'%s' matches '%s' with score %d", item['name'], name, name_score) # Check to make sure that we have an artist match as well if artist is None: # Treat as a wildcard artist_score = 100 else: artist_score = 0 for entry in item.get('artists', []): score = fuzz.ratio(artist, entry.get('name', '').lower()) LOG.debug("Artist match score for '%s' was %d", entry.get('name', ''), score) if score > artist_score: artist_score = score LOG.debug("Artist match score was %d", artist_score) # Only consider cases where the scores look "good enough" if name_score > 75 and artist_score > 75: LOG.debug("Adding match") matches.append((item, name_score, artist_score)) # Anything? if len(matches) > 0: LOG.debug("Got %d matches", len(matches)) # Order them accordingly matches.sort(key=lambda e: (e[1], e[2])) # Now, pick the top one best = matches[0] item = best[0] LOG.debug("Best match was: %s", item) # Extract the info item_name = item.get('name', None) or name artists = item.get('artists', []) artist_name = (artists[0].get('name', None) if len(artists) > 0 else None) or artist # Description of what we are playing what = item_name if item_name else name if artist_name: what += " by " + artist_name what += " on Spotify" # The score is the geometric value of the two score = sqrt(best[1] * best[1] + best[2] * best[2]) / 100.0 # The should be here assert 'uri' in item, "Missing URI in %s" % (item, ) uri = item['uri'] # If we are an album then grab the track URIs if which == 'album': tracks = self._spotify.album_tracks(uri) if tracks and 'items' in tracks: uris = [track['uri'] for track in tracks['items']] else: # Just the track uris = [uri] # And we're done break # Otherwise assume that it's an artist if len(uris) == 0 and artist is None: LOG.info("Looking for '%s' as an artist", name) result = self._spotify.search(name, type='artist') LOG.debug("Got: %s", result) if result and 'artists' in result and 'items' in result['artists']: items = sorted(result['artists']['items'], key=lambda entry: fuzz.ratio( name, entry.get('name', '').lower()), reverse=True) # Look at the best one, if any LOG.debug("Got %d matches", len(items)) if len(items) > 0: match = items[0] who = match['name'] what = "%s on Spotify" % (who, ) score = fuzz.ratio(who.lower(), name) # Find all their albums if 'uri' in match: LOG.debug("Got match: %s", match['uri']) artist_albums = self._spotify.artist_albums( match['uri']) for album in artist_albums.get('items', []): # Append all the tracks LOG.debug("Looking at album: %s", album) if 'uri' in album: tracks = self._spotify.album_tracks( album['uri']) if tracks and 'items' in tracks: LOG.debug( "Adding tracks: %s", ' '.join(track['name'] for track in tracks['items'])) uris.extend([ track['uri'] for track in tracks['items'] ]) # And now we can give it back, if we had something if len(uris) > 0: return _SpotifyServicePlayHandler(self, tokens, what, uris, score) else: # We got nothing return None
class SpotifyGateway: """Class implementing a gateway to the spotify web api. Available functions: - fetch_playlist: Fetch a Playlist namedtuple given a playlist_uri. - fetch_related_tracks: Fetch a track's related tracks by artist or album. - fetch_related_tracks_by_track: Concurrently fetch a mapping of tracks to their related tracks. - create_playlist: Create a playlist for a user[] """ @raise_spotipy_error_as_souffle_error def __init__(self, access_token: str): """C'tor""" self._access_token = access_token self._spotify = Spotify(access_token, requests_session=False) @raise_spotipy_error_as_souffle_error def fetch_playlist(self, playlist_uri: str): """Fetch a Playlist namedtuple representation of a spotify playlist given a playlist_uri.""" playlist_id = playlist_uri.split(':')[-1] name, description = fetch_playlist_metadata(playlist_id, self._access_token) track_data = fetch_playlist_track_data(playlist_id, self._access_token) tracks = [pluck_track(track_record) for track_record in track_data] return Playlist( name=name, tracks=tracks, description=description ) @raise_spotipy_error_as_souffle_error def fetch_related_tracks(self, track, related_by): """Fetch a set of Track namedtuples that are related to TRACK by RELATED_BY.""" if related_by == 'artist': related_track_data = self._spotify.artist_top_tracks(track.artist)['tracks'] related_tracks = {pluck_track(track_record, artist=track.artist) for track_record in related_track_data} elif related_by == 'album': related_track_data = self._spotify.album_tracks(track.album)['items'] related_tracks = {pluck_track(track_record, album=track.album) for track_record in related_track_data} else: raise SouffleParameterError('Invalid souffle type "{}".'.format(related_by)) return related_tracks @raise_spotipy_error_as_souffle_error def fetch_related_tracks_by_track(self, tracks, related_by): """Fetch a related_tracks_by_track mapping, where each related_tracks set is the set of tracks related to a track in TRACKS by RELATED_BY. """ with futures.ThreadPoolExecutor(max_workers=10) as executor: related_tracks_by_track = dict(executor.map( lambda track: (track, self.fetch_related_tracks(track, related_by)), tracks )) return related_tracks_by_track @raise_spotipy_error_as_souffle_error def create_playlist(self, playlist: Playlist, is_public: bool = True) -> str: """Create a new playlist for the given Playlist namedtuple. Return the uri of the new playlist. """ user_id = self.fetch_current_user_id() response = requests.post( f'{BASE}/users/{user_id}/playlists', json={ 'name': playlist.name, 'description': playlist.description, 'public': is_public }, headers={'Authorization': f'Bearer {self._access_token}'} ) response_json = response.json() playlist_id = response_json['id'] playlist_track_uris = [f'spotify:track:{track.id}' for track in playlist.tracks] response = requests.post( f'{BASE}/playlists/{playlist_id}/tracks', params={ 'uris': ','.join(playlist_track_uris) }, headers={'Authorization': f'Bearer {self._access_token}'} ) return playlist_id @raise_spotipy_error_as_souffle_error def fetch_current_user_id(self): """Fetch the current user id based on the access token provided to the SpotifyGateway constructor. Note: This should eventually return a plucked User namedtuple. """ response = self._spotify.current_user() return response['id']
# Filter releases to be newer than x. Where x currently is today. filtered_releases = [] for release in releases: release_date = datetime.strptime(release['release_date'].split(' ')[0], get_date_precision_format(release['release_date_precision'])) if release_date >= (t0-timedelta(days=1)): filtered_releases.append(release) # Extract track ids from releases and add them if they are not already in the playlist. This can happen when multiple # followed artists work on the same track. # If a track is selected to be added to the playlist, also save the artists. track_ids = [] artists = dict() followed_artists_names = [x['name'] for x in followed_artists] for release in filtered_releases: tracks = spotify.album_tracks(release['id']) for x in tracks['items']: if x['id'] not in track_ids and x['id'] not in playlist_track_ids: track_ids.append(x['id']) for artist in x['artists']: if artist['name'] in followed_artists_names: if artist['name'] not in artists: artists[artist['name']] = 1 else: artists[artist['name']] += 1 # Add tracks to playlist. n = 100 added = False for i in range(0, len(track_ids), n):
class AuthTestSpotipy(unittest.TestCase): """ These tests require client authentication - provide client credentials using the following environment variables :: 'SPOTIPY_CLIENT_ID' 'SPOTIPY_CLIENT_SECRET' """ playlist = "spotify:user:plamere:playlist:2oCEWyyAPbZp9xhVSxZavx" four_tracks = [ "spotify:track:6RtPijgfPKROxEzTHNRiDp", "spotify:track:7IHOIqZUUInxjVkko181PB", "4VrWlk8IQxevMvERoX08iC", "http://open.spotify.com/track/3cySlItpiPiIAzU3NyHCJf" ] two_tracks = [ "spotify:track:6RtPijgfPKROxEzTHNRiDp", "spotify:track:7IHOIqZUUInxjVkko181PB" ] other_tracks = [ "spotify:track:2wySlB6vMzCbQrRnNGOYKa", "spotify:track:29xKs5BAHlmlX1u4gzQAbJ", "spotify:track:1PB7gRWcvefzu7t3LJLUlf" ] bad_id = 'BAD_ID' creep_urn = 'spotify:track:3HfB5hBU0dmBt8T0iCmH42' creep_id = '3HfB5hBU0dmBt8T0iCmH42' creep_url = 'http://open.spotify.com/track/3HfB5hBU0dmBt8T0iCmH42' el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK' pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT' weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL' radiohead_urn = 'spotify:artist:4Z8W4fKeB5YxbusRsdQVPb' angeles_haydn_urn = 'spotify:album:1vAbqAeuJVWNAe7UR00bdM' @classmethod def setUpClass(self): self.spotify = Spotify( client_credentials_manager=SpotifyClientCredentials()) self.spotify.trace = False def test_audio_analysis(self): result = self.spotify.audio_analysis(self.four_tracks[0]) assert ('beats' in result) def test_audio_features(self): results = self.spotify.audio_features(self.four_tracks) self.assertTrue(len(results) == len(self.four_tracks)) for track in results: assert ('speechiness' in track) def test_audio_features_with_bad_track(self): bad_tracks = ['spotify:track:bad'] input = self.four_tracks + bad_tracks results = self.spotify.audio_features(input) self.assertTrue(len(results) == len(input)) for track in results[:-1]: if track is not None: assert ('speechiness' in track) self.assertTrue(results[-1] is None) def test_recommendations(self): results = self.spotify.recommendations(seed_tracks=self.four_tracks, min_danceability=0, max_loudness=0, target_popularity=50) self.assertTrue(len(results['tracks']) == 20) def test_artist_urn(self): artist = self.spotify.artist(self.radiohead_urn) self.assertTrue(artist['name'] == 'Radiohead') def test_artists(self): results = self.spotify.artists([self.weezer_urn, self.radiohead_urn]) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 2) def test_album_urn(self): album = self.spotify.album(self.pinkerton_urn) self.assertTrue(album['name'] == 'Pinkerton') def test_album_tracks(self): results = self.spotify.album_tracks(self.pinkerton_urn) self.assertTrue(len(results['items']) == 10) def test_album_tracks_many(self): results = self.spotify.album_tracks(self.angeles_haydn_urn) tracks = results['items'] total, received = results['total'], len(tracks) while received < total: results = self.spotify.album_tracks(self.angeles_haydn_urn, offset=received) tracks.extend(results['items']) received = len(tracks) self.assertEqual(received, total) def test_albums(self): results = self.spotify.albums( [self.pinkerton_urn, self.pablo_honey_urn]) self.assertTrue('albums' in results) self.assertTrue(len(results['albums']) == 2) def test_track_urn(self): track = self.spotify.track(self.creep_urn) self.assertTrue(track['name'] == 'Creep') def test_track_id(self): track = self.spotify.track(self.creep_id) self.assertTrue(track['name'] == 'Creep') self.assertTrue(track['popularity'] > 0) def test_track_url(self): track = self.spotify.track(self.creep_url) self.assertTrue(track['name'] == 'Creep') def test_track_bad_urn(self): try: self.spotify.track(self.el_scorcho_bad_urn) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_tracks(self): results = self.spotify.tracks([self.creep_url, self.el_scorcho_urn]) self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']) == 2) def test_artist_top_tracks(self): results = self.spotify.artist_top_tracks(self.weezer_urn) self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']) == 10) def test_artist_related_artists(self): results = self.spotify.artist_related_artists(self.weezer_urn) self.assertTrue('artists' in results) self.assertTrue(len(results['artists']) == 20) for artist in results['artists']: if artist['name'] == 'Jimmy Eat World': found = True self.assertTrue(found) def test_artist_search(self): results = self.spotify.search(q='weezer', type='artist') self.assertTrue('artists' in results) self.assertTrue(len(results['artists']['items']) > 0) self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer') def test_artist_search_with_market(self): results = self.spotify.search(q='weezer', type='artist', market='GB') self.assertTrue('artists' in results) self.assertTrue(len(results['artists']['items']) > 0) self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer') def test_artist_albums(self): results = self.spotify.artist_albums(self.weezer_urn) self.assertTrue('items' in results) self.assertTrue(len(results['items']) > 0) found = False for album in results['items']: if album['name'] == 'Hurley': found = True self.assertTrue(found) def test_search_timeout(self): client_credentials_manager = SpotifyClientCredentials() sp = spotipy.Spotify( client_credentials_manager=client_credentials_manager, requests_timeout=.01) try: sp.search(q='my*', type='track') self.assertTrue(False, 'unexpected search timeout') except requests.exceptions.Timeout: self.assertTrue(True, 'expected search timeout') def test_album_search(self): results = self.spotify.search(q='weezer pinkerton', type='album') self.assertTrue('albums' in results) self.assertTrue(len(results['albums']['items']) > 0) self.assertTrue( results['albums']['items'][0]['name'].find('Pinkerton') >= 0) def test_track_search(self): results = self.spotify.search(q='el scorcho weezer', type='track') self.assertTrue('tracks' in results) self.assertTrue(len(results['tracks']['items']) > 0) self.assertTrue(results['tracks']['items'][0]['name'] == 'El Scorcho') def test_user(self): user = self.spotify.user(user='******') self.assertTrue(user['uri'] == 'spotify:user:plamere') def test_track_bad_id(self): try: self.spotify.track(self.bad_id) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_unauthenticated_post_fails(self): with self.assertRaises(SpotifyException) as cm: self.spotify.user_playlist_create("spotify", "Best hits of the 90s") self.assertTrue(cm.exception.http_status == 401 or cm.exception.http_status == 403) def test_custom_requests_session(self): sess = requests.Session() sess.headers["user-agent"] = "spotipy-test" with_custom_session = spotipy.Spotify( client_credentials_manager=SpotifyClientCredentials(), requests_session=sess) self.assertTrue( with_custom_session.user(user="******")["uri"] == "spotify:user:akx") sess.close() def test_force_no_requests_session(self): from requests import Session with_no_session = spotipy.Spotify( client_credentials_manager=SpotifyClientCredentials(), requests_session=False) self.assertFalse(isinstance(with_no_session._session, Session)) self.assertTrue( with_no_session.user(user="******")["uri"] == "spotify:user:akx")
class Apollo(object): ''' This object is the main interface between the Google Speech transcriber and Spotify. It parses the transcribed text from Google into commands, conducts queries with the Spotipy API and interfaces with the Spotify desktop app to play music. ''' def __init__(self): # Authentication with Spotipy API self._SP_USER = "******" self._SP_CI = os.getenv('SPOTIPY_CLIENT_ID') self._SP_CS = os.getenv('SPOTIPY_CLIENT_SECRET') self._SP_CRED = oauth2.SpotifyClientCredentials( client_id=self._SP_CI, client_secret=self._SP_CS) self._SP_AUTH = self._SP_CRED.get_access_token() self._SP = Spotify(auth=self._SP_AUTH) self._SP_OAUTH_TOKEN = sp_client.get_oauth_token() self._SP_CSRF_TOKEN = sp_client.get_csrf_token() self._SP_PLAYLISTS = self._SP.user_playlists(user=self._SP_USER) # Dictionary of playlist URI's with names as keys self._SP_PLAYLIST_DICT = defaultdict(lambda: defaultdict(lambda: None)) # Number of items to return from track search self._SP_TLIMIT = 10 # Number of items to return from playlist search self._SP_PLIMIT = 100 self._TRACK_LIST = [] self.createPlaylistDict() #@classmethod def createPlaylistDict(self): # Populates the playlist dict for item in self._SP_PLAYLISTS.items()[0][1]: name = item['name'].lower() self._SP_PLAYLIST_DICT[name]['uri'] = item['uri'] self._SP_PLAYLIST_DICT[name]['id'] = item['id'] #@staticmethod def parseCommand(self, phrase): ''' Parses transcribed phrse from Google Speech into command keywords. Curently supports 3 command classes: A: - "Play/Pause" - "Skip" B: - "Play <songname>" - "Play <songname> by <artist>" - "Play playlist <playlistname>" C: - "Add this song to playlist <playlistname>" @param phrase: transcribed phrase from Google Speech API ''' cmd = defaultdict(lambda: None) if (not len(phrase)): asay("No command received") return None phrase = phrase.lower().strip(re.search(r'\b(spotify)\b', phrase)) if ("play" in phrase and len(phrase) <= 2): cmd['A'] = "play" elif ("pause" in phrase or "does" in phrase): cmd['A'] = "pause" elif ("skip" in phrase): cmd['A'] = "skip" elif ("previous" in phrase or "last" in phrase): cmd['A'] = "prev" elif ("play" in phrase): cmd['B'] = phrase elif ("by" in phrase): keywords = phrase.replace("play ", "").split("by") cmd['B'] = keywords elif ("play" in phrase and "playlist" in phrase): keywords = phrase.split("play ")[-1] cmd['B'] = keywords else: asay("No command received") return None if (len(cmd) >= 1): asay("Sending request to Spotify") ret = self.sendRequest(cmd) else: ret = None return ret #@classmethod def sendRequest(self, command): ''' Sends appropriate request to Spotify @param command: parsed command dict (key = ['A','B','C']) ''' if (len(command) == 0): asay("Error: Command was empty") return key = command.keys()[0] cmd = command[key] asay("key: %s\t command: %s" % (key, str(cmd))) if (key == 'A'): if (cmd == "play"): sp_client.unpause(self._SP_OAUTH_TOKEN, self._SP_CSRF_TOKEN) asay(">> Play") elif ("pause" in cmd or "does" in cmd): sp_client.pause(self._SP_OAUTH_TOKEN, self._SP_CSRF_TOKEN) asay("|| Paused") elif ("skip" in cmd or "next" in cmd): if (len(self._TRACK_LIST) == 0): asay("Empty track list! Play a song to populate tracklist") return track_number = random.randint(0, len(self._TRACK_LIST) - 1) asay("Playing track number " + str(track_number)) sp_client.play(self._SP_OAUTH_TOKEN, self._SP_CSRF_TOKEN, self._TRACK_LIST[track_number]) elif (key == 'B'): # Play playlist if ("playlist" in cmd): req_playlist = cmd.split("playlist ")[-1] playlist_uri = self._SP_PLAYLIST_DICT[req_playlist]['uri'] playlist_id = self._SP_PLAYLIST_DICT[req_playlist]['id'] if (playlist_uri == '0'): asay("Couldn't find playlist " + req_playlist) return False else: sp_client.play(self._SP_OAUTH_TOKEN, self._SP_CSRF_TOKEN, playlist_uri) playlist_tracks = self._SP.user_playlist_tracks( user=self._SP_USER, playlist_id=playlist_id) self._TRACK_LIST = [ item['track']['uri'] for item in playlist_tracks['items'] ] # Play single track else: # Artist name only if ("artist" in cmd): artist = cmd.split("artist")[-1] asay("Looking for " + str(artist)) result = self._SP.search(q=artist, type='artist', limit=self._SP_TLIMIT) if (not result): asay("Couldn't find artist " + artist) return False asay("Playing " + str(result['artists']['items'][0]['name'])) artist_uri = result['artists']['items'][0]['uri'] sp_client.play(self._SP_OAUTH_TOKEN, self._SP_CSRF_TOKEN, artist_uri) # TODO Get remaining tracks in album to enable skipping return True # Track + Artist name provided elif ("by" in cmd): cmd = cmd.replace("play ", "") track, artist = cmd.split("by") asay("Looking for " + str(artist) + ":" + str(track)) result = self._SP.search(q=track + " " + artist, type='track', limit=self._SP_TLIMIT) # Track name only else: track = cmd[0] asay("Looking for " + str(track)) result = self._SP.search(q=track, type='track', limit=self._SP_TLIMIT) try: track_uri = result['tracks']['items'][0]['uri'] sp_client.play(self._SP_OAUTH_TOKEN, self._SP_CSRF_TOKEN, track_uri) album_uri = result['tracks']['items'][0]['album']['uri'] album_tracks = self._SP.album_tracks(album_id=album_uri) self._TRACK_LIST = [ item['uri'] for item in album_tracks['items'] ] # Single record? (one song in the album): search the artist playlist if (len(self._TRACK_LIST) <= 1): artist = result['tracks']['items'][0]['artists'][0] singles = self._SP.artist_albums(artist['id']) self._TRACK_LIST = [] self._TRACK_LIST = [ item['uri'] for item in singles['items'] ] return True except KeyError or IndexError: asay("Couldn't find URI") return False