def get_target_playlist(date: dt, client: spotipy.Spotify, user: str) -> str: """ ASSUMPTIONS: a user has no duplicate playlist names In the case that a user has a duplicate playlist name, the script will modify the one 'lower' in the user's playlist library Solution: no intuitive workaround """ # december of 2019 looks for playlist "winter 2020" target_playlist_name = get_current_season(date) + " " + str( date.year if date.month != 12 else date.year + 1) chunk, offset = 50, 0 all_playlists = {} # Case 1: Playlist is cached and playlist is current season # Good, use it # Case 2: Playlist is cached but playlist is out of season # Make a new playlist and cache it # Case 3: Playlist isnt cached # Look for it playlist_id = "" try: playlist_id = database.get_field(user, "last_playlist") # case 1 if playlist_id != '' and client.playlist( playlist_id)["name"] == target_playlist_name: return playlist_id # case 2 else: resp = client.user_playlist_create( client.me()['id'], target_playlist_name, public=False, description= 'AUTOMATED PLAYLIST - https://github.com/turrence/spotify-new-music-sorter' ) return resp['id'] except KeyError as e: # case 3: do nothing, it's not cached, hopefully this is rare pass while True: playlist_info = client.current_user_playlists(chunk, offset) for item in playlist_info['items']: all_playlists[item['name']] = item['id'] if len(all_playlists) >= playlist_info['total']: break else: offset += chunk if target_playlist_name not in all_playlists: resp = client.user_playlist_create( client.me()['id'], target_playlist_name, public=False, description= 'AUTOMATED PLAYLIST - https://github.com/turrence/spotify-new-music-sorter' ) return resp['id'] else: return all_playlists[target_playlist_name]
def main(): # Create the library folder, if it doesn't already exist library_folder = Path(__file__).parent / LIBRARY_FOLDER library_folder.mkdir(exist_ok=True) print("Retrieving auth token") token = auth_server.get_token() print("Starting export") sp = Spotify(auth=token) pl_folder = library_folder / "playlists" pl_folder.mkdir(exist_ok=True) backup_fnames = set() for pl in retrieve_all_items(sp, sp.current_user_playlists()): name = pl['name'] # Replace any characters that can't go in a filename plname = pl['name'].replace('/', '_').replace('\\', '_')\ .replace('"', '^').replace("'", '^') \ .replace(':', '=').replace('?', '_') \ .replace('|', '-').replace('*', '+') \ .replace('<', '[').replace('>', ']') # Remove any Unicode characters from filename, if the filesystem # doesn't support them, or if option is enabled if not supports_unicode_filenames or RESTRICT_FILENAME: plname = unicodedata.normalize('NFKD', plname) # Hack to make sure plname is a string, not bytes plname = plname.encode(encoding='ascii', errors='ignore') \ .decode(encoding='ascii').strip() backup_fname = f"{plname}_{pl['id']}.json" backup_fnames.add(backup_fname) backup_fpath = pl_folder / backup_fname # Check if there already is a current backup if backup_fpath.resolve().exists(): text = backup_fpath.read_text() stored_playlist = json.loads(text) if stored_playlist.get('snapshot_id') == pl['snapshot_id']: print(f'Playlist backup is up-to-date: {name}') continue print(f'Retrieving playlist: {name}') playlist = sp.playlist(playlist_id=pl['id'], fields=PLAYLIST_FIELDS) if 'tracks' in playlist and 'items' in playlist[ 'tracks'] and 'next' in playlist['tracks']: playlist['tracks']['items'] = retrieve_all_items( sp, playlist['tracks']) backup_fpath.write_text(json.dumps(playlist)) # Move deleted playlists elsewhere deleted_fpath = pl_folder / 'deleted' deleted_fpath.mkdir(exist_ok=True) all_fnames = set(p.name for p in pl_folder.glob('*.json')) deleted_fnames = all_fnames - backup_fnames for fname in deleted_fnames: print(f'Move playlist backup to deleted folder: {fname}') (pl_folder / fname).replace(deleted_fpath / fname) backup_library(sp, library_folder)
def create_playlist(sp: spotipy.Spotify, tracks: list, playlist_name: str): """creates a playlist or tracks from tracks_uri on the users account of length amount""" print('...creating playlist') user_id = sp.current_user()["id"] playlist_id = sp.user_playlist_create(user_id, playlist_name)["id"] random.shuffle(tracks) tracks_uri = [] for track in tracks: tracks_uri.append(track) sp.user_playlist_add_tracks(user_id, playlist_id, tracks_uri) print('playlist, {}, has been generated.'.format(playlist_name)) return sp.playlist(playlist_id)["external_urls"]["spotify"]
def get_spotify_songs(playlist_link): config = get_config() try: client = Spotify(client_credentials_manager=SpotifyClientCredentials( config.get("spotify-id"), config.get("spotify-secret-id"))) playlist = client.playlist(playlist_link) except SpotifyOauthError: print("\n" + paint("Error! ", Color.RED) + "Please run the configuration once.") print("Run " + paint("saberio --config", Color.BLUE)) exit(-1) return [( sanitize_song_name(song.get("track").get("name")), song.get("track").get("artists")[0].get("name"), ) for song in playlist.get("tracks").get("items")]
async def spotify(self, ctx: Context, url: str = None, type_: str = None): if not url: return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: Missing Spotify link or ID' )) elif not type_: try: type_ = url.split('&')[0].split('?')[0].split('/')[3] except IndexError: pass if type_ == 'user': return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: User profiles are not supported', description='...yet?' )) elif type_ not in ['track', 'album', 'artist', 'playlist']: return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: What is this?', description='Is it `track`, `album`, `artist` or `playlist`?' )) if url.startswith(('http://open.spotify.com', 'https://open.spotify.com')): url = url.split('?')[0].split('/')[-1] type_ = type_.lower() try: sp = Spotify(auth_manager=SpotifyClientCredentials( client_id=spotify_client_id(), client_secret=spotify_client_secret() )) except SpotifyOauthError: sp = None if not sp: return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: Unable to connect to Spotify!' )) result = error_code = None em = SuccessEmbed( author=ctx.author ).set_author( name=f'{ctx.author.display_name} shared a{"n" if type_[0] == "a" else ""} {type_}:', icon_url=ctx.author.avatar_url ) if type_ == 'track': try: result = sp.track(url) except SpotifyException as e: error_code = int(e.http_status) elif type_ == 'album': try: result = sp.album(url) except SpotifyException as e: error_code = int(e.http_status) elif type_ == 'playlist': try: result = sp.playlist(url) except SpotifyException as e: error_code = int(e.http_status) elif type_ == 'artist': try: result = sp.artist(url) except SpotifyException as e: error_code = int(e.http_status) else: return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: Unknown object type', description='Check `>help` for valid object types.' )) if error_code: if error_code == 400: d = 'Invalid ID or URL.' elif error_code == 429: d = 'Unable to do that now, please try again in 5 minutes.' elif str(error_code).startswith('5'): d = 'Spotify is not responding.' else: d = 'Unknown error. Please try again in a few minutes and please make sure URL or ID is valid.' return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: An error occurred!', description=d )) elif not result: return await ctx.send(embed=ErrorEmbed( author=ctx.author, title=':x: Unable to find anything on Spotify', description='Probably URL/ID is wrong.' )) title = result['name'] # Artists if type_ not in ['artist', 'playlist']: artists = list(map(lambda x: [x['name'], x['external_urls']['spotify']], result['artists'])) elif type_ in ['playlist']: artists = [[result['owner']['display_name'], result['owner']['external_urls']['spotify']]] else: artists = None # Released if type_ == 'track': released = result['album']['release_date'] elif type_ == 'album': released = result['release_date'] else: released = None # Genres if type_ in ['artist', 'album']: genres = ', '.join(result['genres']) or 'Not specified' else: genres = None ex_url = result['external_urls']['spotify'] thumbnail = result['album']['images'][0]['url'] if type_ == 'track' else result['images'][0]['url'] # Title if title: em.add_field( name='Name' if type_ in ['artist'] else 'Title', value=title ) # Author / Artist(s) if artists: em.add_field( name='Author' if type_ == 'playlist' else 'Artist' if len(artists) == 1 else 'Artists', value=', '.join(map(lambda x: f'[{x[0]}]({x[1]} "Check it on Spotify")', artists)) ) # Followers if type_ in ['artist', 'playlist']: em.add_field( name='Followers', value=result['followers']['total'] ) # Album if type_ == 'track': em.add_field( name='Album', value=f'[{result["name"]}]({result["album"]["external_urls"]["spotify"]} "Check it on Spotify")' ) # Released if released: em.add_field( name='Released', value=released ) # Tracks if type_ in ['playlist', 'album']: em.add_field( name='Tracks', value=str(result['tracks']['total']) ) # Genres if genres: em.add_field( name='Genres', value=genres ) # Popularity if type_ in ['track', 'artist', 'album']: em.add_field( name='Popularity', value=str(result['popularity']) ) # Label elif type_ == 'album': em.add_field( name='Label', value=result['label'] ) # Spotify link if ex_url: em.add_field( name='Spotify', value=ex_url, inline=False ) # YouTube link if type_ == 'track': # Lookup YouTube query = '{} {}'.format(result['name'], ' '.join(map(lambda x: x['name'], result['artists']))) yt = SearchVideos( query, mode='dict', max_results=1 ).result() # noinspection PyTypeChecker yt = yt['search_result'][0]['link'] if yt else None em.add_field( name='YouTube', value=yt, inline=False ) # Thumbnail if thumbnail: em.set_thumbnail( url=thumbnail ) await ctx.send(embed=em) try: await ctx.message.delete() except Forbidden or NotFound or HTTPException: pass
class AuthTestSpotipy(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' """ playlist = "spotify:user:plamere:playlist:2oCEWyyAPbZp9xhVSxZavx" playlist_new_id = "spotify:playlist:7GlxpQjjxRjmbb3RP2rDqI" 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"] album_ids = ["spotify:album:6kL09DaURb7rAoqqaA51KU", "spotify:album:6RTzC0rDbvagTSJLlY7AKl"] bad_id = 'BAD_ID' @classmethod def setUpClass(self): if sys.version_info >= (3, 2): # >= Python3.2 only warnings.filterwarnings( "ignore", category=ResourceWarning, # noqa message="unclosed.*<ssl.SSLSocket.*>") 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 = ( 'playlist-modify-public ' 'user-library-read ' 'user-follow-read ' 'user-library-modify ' 'user-read-private ' 'user-top-read ' 'user-follow-modify ' 'user-read-recently-played ' 'ugc-image-upload' ) self.token = prompt_for_user_token(self.username, scope=self.scope) self.spotify = Spotify(auth=self.token) # Helper def get_or_create_spotify_playlist(self, playlist_name): playlists = self.spotify.user_playlists(self.username) while playlists: for item in playlists['items']: if item['name'] == playlist_name: return item playlists = self.spotify.next(playlists) return self.spotify.user_playlist_create( self.username, playlist_name) # Helper def get_as_base64(self, url): import base64 return base64.b64encode(requests.get(url).content).decode("utf-8") def test_track_bad_id(self): try: self.spotify.track(self.bad_id) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_basic_user_profile(self): user = self.spotify.user(self.username) self.assertTrue(user['id'] == self.username.lower()) def test_current_user(self): user = self.spotify.current_user() self.assertTrue(user['id'] == self.username.lower()) def test_me(self): user = self.spotify.me() self.assertTrue(user['id'] == self.username.lower()) def test_user_playlists(self): playlists = self.spotify.user_playlists(self.username, limit=5) self.assertTrue('items' in playlists) self.assertTrue(len(playlists['items']) == 5) def test_user_playlist_tracks(self): playlists = self.spotify.user_playlists(self.username, limit=5) self.assertTrue('items' in playlists) for playlist in playlists['items']: user = playlist['owner']['id'] pid = playlist['id'] results = self.spotify.user_playlist_tracks(user, pid) self.assertTrue(len(results['items']) >= 0) def test_current_user_saved_albums(self): # List albums = self.spotify.current_user_saved_albums() self.assertTrue(len(albums['items']) > 1) # Add self.spotify.current_user_saved_albums_add(self.album_ids) # Contains self.assertTrue( self.spotify.current_user_saved_albums_contains( self.album_ids) == [ True, True]) # Remove self.spotify.current_user_saved_albums_delete(self.album_ids) albums = self.spotify.current_user_saved_albums() self.assertTrue(len(albums['items']) > 1) def test_current_user_playlists(self): playlists = self.spotify.current_user_playlists(limit=10) self.assertTrue('items' in playlists) self.assertTrue(len(playlists['items']) == 10) def test_user_playlist_follow(self): self.spotify.user_playlist_follow_playlist( 'plamere', '4erXB04MxwRAVqcUEpu30O') follows = self.spotify.user_playlist_is_following( 'plamere', '4erXB04MxwRAVqcUEpu30O', [ self.spotify.current_user()['id']]) self.assertTrue(len(follows) == 1, 'proper follows length') self.assertTrue(follows[0], 'is following') self.spotify.user_playlist_unfollow( 'plamere', '4erXB04MxwRAVqcUEpu30O') follows = self.spotify.user_playlist_is_following( 'plamere', '4erXB04MxwRAVqcUEpu30O', [ self.spotify.current_user()['id']]) self.assertTrue(len(follows) == 1, 'proper follows length') self.assertFalse(follows[0], 'is no longer following') def test_current_user_saved_tracks(self): tracks = self.spotify.current_user_saved_tracks() self.assertTrue(len(tracks['items']) > 0) def test_current_user_save_and_unsave_tracks(self): tracks = self.spotify.current_user_saved_tracks() total = tracks['total'] self.spotify.current_user_saved_tracks_add(self.four_tracks) tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] self.assertTrue(new_total - total == len(self.four_tracks)) tracks = self.spotify.current_user_saved_tracks_delete( self.four_tracks) tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] self.assertTrue(new_total == total) def test_categories(self): response = self.spotify.categories() self.assertTrue(len(response['categories']) > 0) def test_category_playlists(self): response = self.spotify.categories() for cat in response['categories']['items']: cat_id = cat['id'] response = self.spotify.category_playlists(category_id=cat_id) if len(response['playlists']["items"]) > 0: break self.assertTrue(True) def test_new_releases(self): response = self.spotify.new_releases() self.assertTrue(len(response['albums']) > 0) def test_featured_releases(self): response = self.spotify.featured_playlists() self.assertTrue(len(response['playlists']) > 0) def test_current_user_follows(self): response = self.spotify.current_user_followed_artists() artists = response['artists'] self.assertTrue(len(artists['items']) > 0) def test_current_user_top_tracks(self): response = self.spotify.current_user_top_tracks() items = response['items'] self.assertTrue(len(items) > 0) def test_current_user_top_artists(self): response = self.spotify.current_user_top_artists() items = response['items'] self.assertTrue(len(items) > 0) def test_current_user_recently_played(self): # No cursor res = self.spotify.current_user_recently_played() self.assertTrue(len(res['items']) <= 50) played_at = res['items'][0]['played_at'] # Using `before` gives tracks played before res = self.spotify.current_user_recently_played( before=res['cursors']['after']) self.assertTrue(len(res['items']) <= 50) self.assertTrue(res['items'][0]['played_at'] < played_at) played_at = res['items'][0]['played_at'] # Using `after` gives tracks played after res = self.spotify.current_user_recently_played( after=res['cursors']['before']) self.assertTrue(len(res['items']) <= 50) self.assertTrue(res['items'][0]['played_at'] > played_at) def test_user_playlist_ops(self): sp = self.spotify # create empty playlist playlist = self.get_or_create_spotify_playlist( 'spotipy-testing-playlist-1') playlist_id = playlist['id'] # remove all tracks from it sp.user_playlist_replace_tracks( self.username, playlist_id, []) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 0) self.assertTrue(len(playlist['tracks']['items']) == 0) # add tracks to it sp.user_playlist_add_tracks( self.username, playlist_id, self.four_tracks) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 4) self.assertTrue(len(playlist['tracks']['items']) == 4) # remove two tracks from it sp.user_playlist_remove_all_occurrences_of_tracks(self.username, playlist_id, self.two_tracks) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 2) self.assertTrue(len(playlist['tracks']['items']) == 2) # replace with 3 other tracks sp.user_playlist_replace_tracks(self.username, playlist_id, self.other_tracks) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 3) self.assertTrue(len(playlist['tracks']['items']) == 3) def test_playlist(self): # New playlist ID pl = self.spotify.playlist(self.playlist_new_id) self.assertTrue(pl["tracks"]["total"] > 0) # Old playlist ID pl = self.spotify.playlist(self.playlist) self.assertTrue(pl["tracks"]["total"] > 0) def test_playlist_tracks(self): # New playlist ID pl = self.spotify.playlist_tracks(self.playlist_new_id, limit=2) self.assertTrue(len(pl["items"]) == 2) self.assertTrue(pl["total"] > 0) # Old playlist ID pl = self.spotify.playlist_tracks(self.playlist, limit=2) self.assertTrue(len(pl["items"]) == 2) self.assertTrue(pl["total"] > 0) def test_playlist_upload_cover_image(self): pl1 = self.get_or_create_spotify_playlist('spotipy-testing-playlist-1') plid = pl1['uri'] old_b64 = pl1['images'][0]['url'] # Upload random dog image r = requests.get('https://dog.ceo/api/breeds/image/random') dog_base64 = self.get_as_base64(r.json()['message']) self.spotify.playlist_upload_cover_image(plid, dog_base64) # Image must be different pl1 = self.spotify.playlist(plid) new_b64 = self.get_as_base64(pl1['images'][0]['url']) self.assertTrue(old_b64 != new_b64) def test_playlist_cover_image(self): pl = self.get_or_create_spotify_playlist('spotipy-testing-playlist-1') plid = pl['uri'] res = self.spotify.playlist_cover_image(plid) self.assertTrue(len(res) > 0) first_image = res[0] self.assertTrue('width' in first_image) self.assertTrue('height' in first_image) self.assertTrue('url' in first_image) def test_user_follows_and_unfollows_artist(self): # Initially follows 1 artist res = self.spotify.current_user_followed_artists() self.assertTrue(res['artists']['total'] == 1) # Follow 2 more artists artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"] self.spotify.user_follow_artists(artists) res = self.spotify.current_user_followed_artists() self.assertTrue(res['artists']['total'] == 3) # Unfollow these 2 artists self.spotify.user_unfollow_artists(artists) res = self.spotify.current_user_followed_artists() self.assertTrue(res['artists']['total'] == 1) def test_user_follows_and_unfollows_user(self): # TODO improve after implementing `me/following/contains` users = ["11111204", "xlqeojt6n7on0j7coh9go8ifd"] # Follow 2 more users self.spotify.user_follow_users(users) # Unfollow these 2 users self.spotify.user_unfollow_users(users) def test_deprecated_starred(self): pl = self.spotify.user_playlist(self.username) self.assertTrue(pl["tracks"] is None) self.assertTrue(pl["owner"] is None) def test_deprecated_user_playlist(self): # Test without user due to change from # https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ pl = self.spotify.user_playlist(None, self.playlist) self.assertTrue(pl["tracks"]["total"] > 0) def test_deprecated_user_playlis(self): # Test without user due to change from # https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ pl = self.spotify.user_playlist_tracks(None, self.playlist, limit=2) self.assertTrue(len(pl["items"]) == 2) self.assertTrue(pl["total"] > 0)
class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" def __init__( self, session: OAuth2Session, spotify: Spotify, me: dict, user_id: str, name: str, ): """Initialize.""" self._id = user_id self._me = me self._name = f"Spotify {name}" self._session = session self._spotify = spotify self._scope_ok = set( session.token["scope"].split(" ")) == set(SPOTIFY_SCOPES) self._currently_playing: Optional[dict] = {} self._devices: Optional[List[dict]] = [] self._playlist: Optional[dict] = None self._spotify: Spotify = None self.player_available = False @property def name(self) -> str: """Return the name.""" return self._name @property def icon(self) -> str: """Return the icon.""" return ICON @property def available(self) -> bool: """Return True if entity is available.""" return self.player_available @property def unique_id(self) -> str: """Return the unique ID.""" return self._id @property def device_info(self) -> Dict[str, Any]: """Return device information about this entity.""" if self._me is not None: model = self._me["product"] return { "identifiers": {(DOMAIN, self._id)}, "manufacturer": "Spotify AB", "model": f"Spotify {model}".rstrip(), "name": self._name, } @property def state(self) -> Optional[str]: """Return the playback state.""" if not self._currently_playing: return STATE_IDLE if self._currently_playing["is_playing"]: return STATE_PLAYING return STATE_PAUSED @property def volume_level(self) -> Optional[float]: """Return the device volume.""" return self._currently_playing.get("device", {}).get( "volume_percent", 0) / 100 @property def media_content_id(self) -> Optional[str]: """Return the media URL.""" item = self._currently_playing.get("item") or {} return item.get("uri") @property def media_content_type(self) -> Optional[str]: """Return the media type.""" return MEDIA_TYPE_MUSIC @property def media_duration(self) -> Optional[int]: """Duration of current playing media in seconds.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["duration_ms"] / 1000 @property def media_position(self) -> Optional[str]: """Position of current playing media in seconds.""" if not self._currently_playing: return None return self._currently_playing["progress_ms"] / 1000 @property def media_position_updated_at(self) -> Optional[dt.datetime]: """When was the position of the current playing media valid.""" if not self._currently_playing: return None return utc_from_timestamp(self._currently_playing["timestamp"] / 1000) @property def media_image_url(self) -> Optional[str]: """Return the media image URL.""" if (self._currently_playing.get("item") is None or not self._currently_playing["item"]["album"]["images"]): return None return fetch_image_url(self._currently_playing["item"]["album"]) @property def media_image_remotely_accessible(self) -> bool: """If the image url is remotely accessible.""" return False @property def media_title(self) -> Optional[str]: """Return the media title.""" item = self._currently_playing.get("item") or {} return item.get("name") @property def media_artist(self) -> Optional[str]: """Return the media artist.""" if self._currently_playing.get("item") is None: return None return ", ".join([ artist["name"] for artist in self._currently_playing["item"]["artists"] ]) @property def media_album_name(self) -> Optional[str]: """Return the media album.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["album"]["name"] @property def media_track(self) -> Optional[int]: """Track number of current playing media, music track only.""" item = self._currently_playing.get("item") or {} return item.get("track_number") @property def media_playlist(self): """Title of Playlist currently playing.""" if self._playlist is None: return None return self._playlist["name"] @property def source(self) -> Optional[str]: """Return the current playback device.""" return self._currently_playing.get("device", {}).get("name") @property def source_list(self) -> Optional[List[str]]: """Return a list of source devices.""" if not self._devices: return None return [device["name"] for device in self._devices] @property def shuffle(self) -> bool: """Shuffling state.""" return bool(self._currently_playing.get("shuffle_state")) @property def supported_features(self) -> int: """Return the media player features that are supported.""" if self._me["product"] != "premium": return 0 return SUPPORT_SPOTIFY @spotify_exception_handler def set_volume_level(self, volume: int) -> None: """Set the volume level.""" self._spotify.volume(int(volume * 100)) @spotify_exception_handler def media_play(self) -> None: """Start or resume playback.""" self._spotify.start_playback() @spotify_exception_handler def media_pause(self) -> None: """Pause playback.""" self._spotify.pause_playback() @spotify_exception_handler def media_previous_track(self) -> None: """Skip to previous track.""" self._spotify.previous_track() @spotify_exception_handler def media_next_track(self) -> None: """Skip to next track.""" self._spotify.next_track() @spotify_exception_handler def media_seek(self, position): """Send seek command.""" self._spotify.seek_track(int(position * 1000)) @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" kwargs = {} # Spotify can't handle URI's with query strings or anchors # Yet, they do generate those types of URI in their official clients. media_id = str(URL(media_id).with_query(None).with_fragment(None)) if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC): kwargs["uris"] = [media_id] elif media_type in PLAYABLE_MEDIA_TYPES: kwargs["context_uri"] = media_id else: _LOGGER.error("Media type %s is not supported", media_type) return if not self._currently_playing.get("device") and self._devices: kwargs["device_id"] = self._devices[0].get("id") self._spotify.start_playback(**kwargs) @spotify_exception_handler def select_source(self, source: str) -> None: """Select playback device.""" for device in self._devices: if device["name"] == source: self._spotify.transfer_playback(device["id"], self.state == STATE_PLAYING) return @spotify_exception_handler def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" self._spotify.shuffle(shuffle) @spotify_exception_handler def update(self) -> None: """Update state and attributes.""" if not self.enabled: return if not self._session.valid_token or self._spotify is None: run_coroutine_threadsafe(self._session.async_ensure_token_valid(), self.hass.loop).result() self._spotify = Spotify(auth=self._session.token["access_token"]) current = self._spotify.current_playback() self._currently_playing = current or {} self._playlist = None context = self._currently_playing.get("context") if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: self._playlist = self._spotify.playlist(current["context"]["uri"]) devices = self._spotify.devices() or {} self._devices = devices.get("devices", []) async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" if not self._scope_ok: raise NotImplementedError if media_content_type in [None, "library"]: return await self.hass.async_add_executor_job(library_payload) payload = { "media_content_type": media_content_type, "media_content_id": media_content_id, } response = await self.hass.async_add_executor_job( build_item_response, self._spotify, self._me, payload) if response is None: raise BrowseError( f"Media not found: {media_content_type} / {media_content_id}") return response
class SpotifyMediaPlayer(MediaPlayerDevice): """Representation of a Spotify controller.""" def __init__(self, session, spotify: Spotify, me: dict, user_id: str, name: str): """Initialize.""" self._id = user_id self._me = me self._name = f"Spotify {name}" self._session = session self._spotify = spotify self._currently_playing: Optional[dict] = {} self._devices: Optional[List[dict]] = [] self._playlist: Optional[dict] = None self._spotify: Spotify = None self.player_available = False @property def name(self) -> str: """Return the name.""" return self._name @property def icon(self) -> str: """Return the icon.""" return ICON @property def available(self) -> bool: """Return True if entity is available.""" return self.player_available @property def unique_id(self) -> str: """Return the unique ID.""" return self._id @property def device_info(self) -> Dict[str, Any]: """Return device information about this entity.""" if self._me is not None: model = self._me["product"] return { "identifiers": {(DOMAIN, self._id)}, "manufacturer": "Spotify AB", "model": f"Spotify {model}".rstrip(), "name": self._name, } @property def state(self) -> Optional[str]: """Return the playback state.""" if not self._currently_playing: return STATE_IDLE if self._currently_playing["is_playing"]: return STATE_PLAYING return STATE_PAUSED @property def volume_level(self) -> Optional[float]: """Return the device volume.""" return self._currently_playing.get("device", {}).get( "volume_percent", 0) / 100 @property def media_content_id(self) -> Optional[str]: """Return the media URL.""" item = self._currently_playing.get("item") or {} return item.get("name") @property def media_content_type(self) -> Optional[str]: """Return the media type.""" return MEDIA_TYPE_MUSIC @property def media_duration(self) -> Optional[int]: """Duration of current playing media in seconds.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["duration_ms"] / 1000 @property def media_position(self) -> Optional[str]: """Position of current playing media in seconds.""" if not self._currently_playing: return None return self._currently_playing["progress_ms"] / 1000 @property def media_position_updated_at(self) -> Optional[dt.datetime]: """When was the position of the current playing media valid.""" if not self._currently_playing: return None return utc_from_timestamp(self._currently_playing["timestamp"] / 1000) @property def media_image_url(self) -> Optional[str]: """Return the media image URL.""" if (self._currently_playing.get("item") is None or not self._currently_playing["item"]["album"]["images"]): return None return self._currently_playing["item"]["album"]["images"][0]["url"] @property def media_image_remotely_accessible(self) -> bool: """If the image url is remotely accessible.""" return False @property def media_title(self) -> Optional[str]: """Return the media title.""" item = self._currently_playing.get("item") or {} return item.get("name") @property def media_artist(self) -> Optional[str]: """Return the media artist.""" if self._currently_playing.get("item") is None: return None return ", ".join([ artist["name"] for artist in self._currently_playing["item"]["artists"] ]) @property def media_album_name(self) -> Optional[str]: """Return the media album.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["album"]["name"] @property def media_track(self) -> Optional[int]: """Track number of current playing media, music track only.""" item = self._currently_playing.get("item") or {} return item.get("track_number") @property def media_playlist(self): """Title of Playlist currently playing.""" if self._playlist is None: return None return self._playlist["name"] @property def source(self) -> Optional[str]: """Return the current playback device.""" return self._currently_playing.get("device", {}).get("name") @property def source_list(self) -> Optional[List[str]]: """Return a list of source devices.""" if not self._devices: return None return [device["name"] for device in self._devices] @property def shuffle(self) -> bool: """Shuffling state.""" return bool(self._currently_playing.get("shuffle_state")) @property def supported_features(self) -> int: """Return the media player features that are supported.""" if self._me["product"] != "premium": return 0 return SUPPORT_SPOTIFY @spotify_exception_handler def set_volume_level(self, volume: int) -> None: """Set the volume level.""" self._spotify.volume(int(volume * 100)) @spotify_exception_handler def media_play(self) -> None: """Start or resume playback.""" self._spotify.start_playback() @spotify_exception_handler def media_pause(self) -> None: """Pause playback.""" self._spotify.pause_playback() @spotify_exception_handler def media_previous_track(self) -> None: """Skip to previous track.""" self._spotify.previous_track() @spotify_exception_handler def media_next_track(self) -> None: """Skip to next track.""" self._spotify.next_track() @spotify_exception_handler def media_seek(self, position): """Send seek command.""" self._spotify.seek_track(int(position * 1000)) @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" kwargs = {} if media_type == MEDIA_TYPE_MUSIC: kwargs["uris"] = [media_id] elif media_type == MEDIA_TYPE_PLAYLIST: kwargs["context_uri"] = media_id else: _LOGGER.error("Media type %s is not supported", media_type) return self._spotify.start_playback(**kwargs) @spotify_exception_handler def select_source(self, source: str) -> None: """Select playback device.""" for device in self._devices: if device["name"] == source: self._spotify.transfer_playback(device["id"], self.state == STATE_PLAYING) return @spotify_exception_handler def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" self._spotify.shuffle(shuffle) @spotify_exception_handler def update(self) -> None: """Update state and attributes.""" if not self.enabled: return if not self._session.valid_token or self._spotify is None: run_coroutine_threadsafe(self._session.async_ensure_token_valid(), self.hass.loop).result() self._spotify = Spotify(auth=self._session.token["access_token"]) current = self._spotify.current_playback() self._currently_playing = current or {} self._playlist = None context = self._currently_playing.get("context") if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: self._playlist = self._spotify.playlist(current["context"]["uri"]) devices = self._spotify.devices() or {} self._devices = devices.get("devices", [])
class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC _attr_media_image_remotely_accessible = False def __init__( self, session: OAuth2Session, spotify: Spotify, me: dict, # pylint: disable=invalid-name user_id: str, name: str, ) -> None: """Initialize.""" self._id = user_id self._me = me self._name = f"Spotify {name}" self._session = session self._spotify = spotify self._scope_ok = set( session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES) self._currently_playing: dict | None = {} self._devices: list[dict] | None = [] self._playlist: dict | None = None self._attr_name = self._name self._attr_unique_id = user_id self._attr_last_selected = None self._attributes = {} @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" model = "Spotify Free" if self._me is not None: product = self._me["product"] model = f"Spotify {product}" return DeviceInfo( identifiers={(DOMAIN, self._id)}, manufacturer="Spotify AB", model=model, name=self._name, entry_type=DeviceEntryType.SERVICE, configuration_url="https://open.spotify.com", ) @property def state(self) -> str | None: """Return the playback state.""" if not self._currently_playing: return STATE_IDLE if self._currently_playing["is_playing"]: return STATE_PLAYING return STATE_PAUSED @property def volume_level(self) -> float | None: """Return the device volume.""" return self._currently_playing.get("device", {}).get( "volume_percent", 0) / 100 @property def media_content_id(self) -> str | None: """Return the media URL.""" item = self._currently_playing.get("item") or {} return item.get("uri") @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["duration_ms"] / 1000 @property def media_position(self) -> str | None: """Position of current playing media in seconds.""" if not self._currently_playing: return None return self._currently_playing["progress_ms"] / 1000 @property def media_position_updated_at(self) -> dt.datetime | None: """When was the position of the current playing media valid.""" if not self._currently_playing: return None return utc_from_timestamp(self._currently_playing["timestamp"] / 1000) @property def media_image_url(self) -> str | None: """Return the media image URL.""" if (self._currently_playing.get("item") is None or not self._currently_playing["item"]["album"]["images"]): return None return fetch_image_url(self._currently_playing["item"]["album"]) @property def media_title(self) -> str | None: """Return the media title.""" item = self._currently_playing.get("item") or {} return item.get("name") @property def media_artist(self) -> str | None: """Return the media artist.""" if self._currently_playing.get("item") is None: return None return ", ".join( artist["name"] for artist in self._currently_playing["item"]["artists"]) @property def media_album_name(self) -> str | None: """Return the media album.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["album"]["name"] @property def media_track(self) -> int | None: """Track number of current playing media, music track only.""" item = self._currently_playing.get("item") or {} return item.get("track_number") @property def media_playlist(self): """Title of Playlist currently playing.""" if self._playlist is None: return None return self._playlist["name"] @property def source(self) -> str | None: """Return the current playback device.""" return self._currently_playing.get("device", {}).get("name") @property def source_list(self) -> list[str] | None: """Return a list of source devices.""" if not self._devices: return None return [device["name"] for device in self._devices] @property def shuffle(self) -> bool: """Shuffling state.""" return bool(self._currently_playing.get("shuffle_state")) @property def repeat(self) -> str | None: """Return current repeat mode.""" repeat_state = self._currently_playing.get("repeat_state") return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) @property def supported_features(self) -> int: """Return the media player features that are supported.""" if self._me["product"] != "premium": return 0 return SUPPORT_SPOTIFY @property def extra_state_attributes(self): """Return entity specific state attributes.""" return self._attributes @spotify_exception_handler def set_volume_level(self, volume: int) -> None: """Set the volume level.""" self._spotify.volume(int(volume * 100)) @spotify_exception_handler def media_play(self) -> None: """Start or resume playback.""" self._spotify.start_playback() @spotify_exception_handler def media_pause(self) -> None: """Pause playback.""" self._spotify.pause_playback() @spotify_exception_handler def media_previous_track(self) -> None: """Skip to previous track.""" self._spotify.previous_track() @spotify_exception_handler def media_next_track(self) -> None: """Skip to next track.""" self._spotify.next_track() @spotify_exception_handler def media_seek(self, position): """Send seek command.""" self._spotify.seek_track(int(position * 1000)) @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" kwargs = {} # Spotify can't handle URI's with query strings or anchors # Yet, they do generate those types of URI in their official clients. media_id = str(URL(media_id).with_query(None).with_fragment(None)) self._attr_last_selected = media_id self._attributes[ATTR_LAST_SELECTED] = self._attr_last_selected self.update() if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC): kwargs["uris"] = [media_id] elif media_type in PLAYABLE_MEDIA_TYPES: kwargs["context_uri"] = media_id else: _LOGGER.error("Media type %s is not supported", media_type) return if not self._currently_playing.get("device") and self._devices: kwargs["device_id"] = self._devices[0].get("id") self._spotify.start_playback(**kwargs) self.update() @spotify_exception_handler def select_source(self, source: str) -> None: """Select playback device.""" for device in self._devices: if device["name"] == source: self._spotify.transfer_playback(device["id"], self.state == STATE_PLAYING) return @spotify_exception_handler def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" self._spotify.shuffle(shuffle) @spotify_exception_handler def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: raise ValueError(f"Unsupported repeat mode: {repeat}") self._spotify.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) @spotify_exception_handler def update(self) -> None: """Update state and attributes.""" self._attributes[ATTR_LAST_SELECTED] = self._attr_last_selected if not self.enabled: return if not self._session.valid_token or self._spotify is None: run_coroutine_threadsafe(self._session.async_ensure_token_valid(), self.hass.loop).result() self._spotify = Spotify(auth=self._session.token["access_token"]) current = self._spotify.current_playback() self._currently_playing = current or {} self._playlist = None context = self._currently_playing.get("context") if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: self._playlist = self._spotify.playlist(current["context"]["uri"]) devices = self._spotify.devices() or {} self._devices = devices.get("devices", []) async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" if not self._scope_ok: _LOGGER.debug( "Spotify scopes are not set correctly, this can impact features such as media browsing" ) raise NotImplementedError if media_content_type in (None, "library"): return await self.hass.async_add_executor_job(library_payload) payload = { "media_content_type": media_content_type, "media_content_id": media_content_id, } response = await self.hass.async_add_executor_job( build_item_response, self._spotify, self._me, payload) if response is None: raise BrowseError( f"Media not found: {media_content_type} / {media_content_id}") return response
def update_playlist(playlist_id, user_id, tracks, sp: Spotify): playlist_record = sp.playlist(playlist_id=playlist_id) existing_ids = [i['track']['id'] for i in playlist_record['tracks']['items']] update_tracks = [track_id for track_id in tracks if track_id not in existing_ids] sp.user_playlist_add_tracks(user=user_id, playlist_id=playlist_id, tracks=update_tracks)
class AuthTestSpotipy(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' """ playlist = "spotify:user:plamere:playlist:2oCEWyyAPbZp9xhVSxZavx" playlist_new_id = "spotify:playlist:7GlxpQjjxRjmbb3RP2rDqI" 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"] album_ids = ["spotify:album:6kL09DaURb7rAoqqaA51KU", "spotify:album:6RTzC0rDbvagTSJLlY7AKl"] 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 = ( 'playlist-modify-public ' 'user-library-read ' 'user-follow-read ' 'user-library-modify ' 'user-read-private ' 'user-top-read ' 'user-follow-modify' ) self.token = prompt_for_user_token(self.username, scope=self.scope) self.spotify = Spotify(auth=self.token) def test_track_bad_id(self): try: self.spotify.track(self.bad_id) self.assertTrue(False) except SpotifyException: self.assertTrue(True) def test_basic_user_profile(self): user = self.spotify.user(self.username) self.assertTrue(user['id'] == self.username.lower()) def test_current_user(self): user = self.spotify.current_user() self.assertTrue(user['id'] == self.username.lower()) def test_me(self): user = self.spotify.me() self.assertTrue(user['id'] == self.username.lower()) def test_user_playlists(self): playlists = self.spotify.user_playlists(self.username, limit=5) self.assertTrue('items' in playlists) self.assertTrue(len(playlists['items']) == 5) def test_user_playlist_tracks(self): playlists = self.spotify.user_playlists(self.username, limit=5) self.assertTrue('items' in playlists) for playlist in playlists['items']: user = playlist['owner']['id'] pid = playlist['id'] results = self.spotify.user_playlist_tracks(user, pid) self.assertTrue(len(results['items']) >= 0) # known API issue currently causes this test to fail # the issue is that the API doesn't currently respect the # limit parameter # def user_playlist_tracks(self, user, playlist_id=None, fields=None, # limit=100, offset=0): # self.assertTrue(len(playlists['items']) == 5) def test_current_user_saved_albums(self): # List albums = self.spotify.current_user_saved_albums() self.assertTrue(len(albums['items']) == 1) # Add self.spotify.current_user_saved_albums_add(self.album_ids) # Contains self.assertTrue( self.spotify.current_user_saved_albums_contains( self.album_ids) == [ True, True]) # Remove self.spotify.current_user_saved_albums_delete(self.album_ids) albums = self.spotify.current_user_saved_albums() self.assertTrue(len(albums['items']) == 1) def test_current_user_playlists(self): playlists = self.spotify.current_user_playlists(limit=10) self.assertTrue('items' in playlists) self.assertTrue(len(playlists['items']) == 10) def test_user_playlist_follow(self): self.spotify.user_playlist_follow_playlist( 'plamere', '4erXB04MxwRAVqcUEpu30O') follows = self.spotify.user_playlist_is_following( 'plamere', '4erXB04MxwRAVqcUEpu30O', [ self.spotify.current_user()['id']]) self.assertTrue(len(follows) == 1, 'proper follows length') self.assertTrue(follows[0], 'is following') self.spotify.user_playlist_unfollow( 'plamere', '4erXB04MxwRAVqcUEpu30O') follows = self.spotify.user_playlist_is_following( 'plamere', '4erXB04MxwRAVqcUEpu30O', [ self.spotify.current_user()['id']]) self.assertTrue(len(follows) == 1, 'proper follows length') self.assertFalse(follows[0], 'is no longer following') def test_current_user_saved_tracks(self): tracks = self.spotify.current_user_saved_tracks() self.assertTrue(len(tracks['items']) > 0) def test_current_user_save_and_unsave_tracks(self): tracks = self.spotify.current_user_saved_tracks() total = tracks['total'] self.spotify.current_user_saved_tracks_add(self.four_tracks) tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] self.assertTrue(new_total - total == len(self.four_tracks)) tracks = self.spotify.current_user_saved_tracks_delete( self.four_tracks) tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] self.assertTrue(new_total == total) def test_categories(self): response = self.spotify.categories() self.assertTrue(len(response['categories']) > 0) def test_category_playlists(self): response = self.spotify.categories() for cat in response['categories']['items']: cat_id = cat['id'] response = self.spotify.category_playlists(category_id=cat_id) if len(response['playlists']["items"]) > 0: break self.assertTrue(True) def test_new_releases(self): response = self.spotify.new_releases() self.assertTrue(len(response['albums']) > 0) def test_featured_releases(self): response = self.spotify.featured_playlists() self.assertTrue(len(response['playlists']) > 0) def test_current_user_follows(self): response = self.spotify.current_user_followed_artists() artists = response['artists'] self.assertTrue(len(artists['items']) > 0) def test_current_user_top_tracks(self): response = self.spotify.current_user_top_tracks() items = response['items'] self.assertTrue(len(items) > 0) def test_current_user_top_artists(self): response = self.spotify.current_user_top_artists() items = response['items'] self.assertTrue(len(items) > 0) def get_or_create_spotify_playlist(self, playlist_name): playlists = self.spotify.user_playlists(self.username) while playlists: for item in playlists['items']: if item['name'] == playlist_name: return item['id'] playlists = self.spotify.next(playlists) playlist = self.spotify.user_playlist_create( self.username, playlist_name) playlist_id = playlist['uri'] return playlist_id def test_user_playlist_ops(self): sp = self.spotify # create empty playlist playlist_id = self.get_or_create_spotify_playlist( 'spotipy-testing-playlist-1') # remove all tracks from it sp.user_playlist_replace_tracks( self.username, playlist_id, []) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 0) self.assertTrue(len(playlist['tracks']['items']) == 0) # add tracks to it sp.user_playlist_add_tracks( self.username, playlist_id, self.four_tracks) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 4) self.assertTrue(len(playlist['tracks']['items']) == 4) # remove two tracks from it sp.user_playlist_remove_all_occurrences_of_tracks(self.username, playlist_id, self.two_tracks) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 2) self.assertTrue(len(playlist['tracks']['items']) == 2) # replace with 3 other tracks sp.user_playlist_replace_tracks(self.username, playlist_id, self.other_tracks) playlist = sp.user_playlist(self.username, playlist_id) self.assertTrue(playlist['tracks']['total'] == 3) self.assertTrue(len(playlist['tracks']['items']) == 3) def test_playlist(self): # New playlist ID pl = self.spotify.playlist(self.playlist_new_id) self.assertTrue(pl["tracks"]["total"] > 0) # Old playlist ID pl = self.spotify.playlist(self.playlist) self.assertTrue(pl["tracks"]["total"] > 0) def test_user_follows_and_unfollows_artist(self): # Initially follows 1 artist res = self.spotify.current_user_followed_artists() self.assertTrue(res['artists']['total'] == 1) # Follow 2 more artists artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"] self.spotify.user_follow_artists(artists) res = self.spotify.current_user_followed_artists() self.assertTrue(res['artists']['total'] == 3) # Unfollow these 2 artists self.spotify.user_unfollow_artists(artists) res = self.spotify.current_user_followed_artists() self.assertTrue(res['artists']['total'] == 1) def test_user_follows_and_unfollows_user(self): # TODO improve after implementing `me/following/contains` users = ["11111204", "xlqeojt6n7on0j7coh9go8ifd"] # Follow 2 more users self.spotify.user_follow_users(users) # Unfollow these 2 users self.spotify.user_unfollow_users(users)