Ejemplo n.º 1
0
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]
Ejemplo n.º 2
0
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"]
Ejemplo n.º 4
0
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")]
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
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
Ejemplo n.º 8
0
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", [])
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0
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)