Exemple #1
0
    def test_specifying_pos_args_until_limit(self, app_token):
        client = Spotify(app_token, max_limits_on=True)
        s1, = client.search('piano', ('track', ), None, None)
        with client.max_limits(False):
            s2, = client.search('piano', ('track', ), None, None)

        assert s1.limit > s2.limit
Exemple #2
0
    def __init__(self, token):
        super().__init__()

        self.sp_user = Spotify(token)

        app_name = 'SpotiCLI'
        version = '1.20.0917.dev'

        ###define app parameters
        self.app_info = f'{Fore.CYAN}{Style.BRIGHT}\n{app_name} {version}{Style.RESET_ALL}'
        self.intro = self.app_info + '\n'
        self.prompt = f'{Fore.GREEN}{Style.BRIGHT}spoticli ~$ {Style.RESET_ALL}'

        self.current_endpoint = ''
        self.api_delay = 0.3

        #hide built-in cmd2 functions. this will leave them available for use but will be hidden from tab completion (and docs)
        self.hidden_commands.append('alias')
        self.hidden_commands.append('unalias')
        self.hidden_commands.append('set')
        self.hidden_commands.append('edit')
        self.hidden_commands.append('history')
        self.hidden_commands.append('load')
        self.hidden_commands.append('macro')
        self.hidden_commands.append('py')
        self.hidden_commands.append('pyscript')
        self.hidden_commands.append('quit')
        self.hidden_commands.append('shell')
        self.hidden_commands.append('shortcuts')
        self.hidden_commands.append('_relative_load')
        self.hidden_commands.append('run_pyscript')
        self.hidden_commands.append('run_script')
        self.debug = True
Exemple #3
0
    def __init__(self, config, app=None):
        self.config = config
        self.loop = asyncio.get_event_loop()
        self.app = app
        self.device = None
        self.user_pause = False
        self.task = None
        self.active = False
        self.running = True
        self.info = {}
        self.user_info = {}
        self.token = None
        self.spotify = Spotify()
        self.last_update = None
        self.credentials = Credentials(self.config.client_id,
                                       self.config.client_secret,
                                       self.config.client_redirect_uri)
        self.sender = RetryingSender(sender=AsyncSender())

        self.playback_device_name = self.config.default_device

        self.user_info = {}
        self.now_playing_data = {}
        self.loop.run_until_complete(self.load_auth())

        self.backup_playlist = self.loop.run_until_complete(
            self.load_and_confirm('backup_playlist'))
        self.user_playlist = self.loop.run_until_complete(
            self.load_and_confirm('user_playlist'))
        self.recent_picks = self.loop.run_until_complete(
            self.load_and_confirm('recent_picks'))
        self.blacklist = self.loop.run_until_complete(self.load_blacklist())
        self.idle_timeout = 300
        self.current_track_user = None
Exemple #4
0
    def test_turning_on_max_limits_returns_more(self, app_token):
        client = Spotify(app_token)
        s1, = client.search('piano')
        with client.max_limits(True):
            s2, = client.search('piano')

        assert s1.limit < s2.limit
Exemple #5
0
    def test_turning_off_max_limits_returns_less(self, app_token):
        client = Spotify(app_token, max_limits_on=True)
        s1, = client.search('piano')
        with client.max_limits(False):
            s2, = client.search('piano')

        assert s1.limit > s2.limit
Exemple #6
0
 def __init__(self, token: RefreshingToken) -> None:
     super().__init__()
     self.artist = ""
     self.title = ""
     self.is_playing = False
     self._position = 0
     self._token = token
     self._spotify = Spotify(self._token)
     self._event_timestamp = time.time()
Exemple #7
0
def get_artists_data(spotify_client: tk.Spotify,
                     artist_ids: List[str]) -> pd.DataFrame:
    """
    Given a spotify-client and list of artist-ids, compiles a dataframe with metadata for all the artists.
    The compiled metadata contains the following fields for each artist:
    1. artist-id
    2. artist-name
    3. artist-genre - if artist has multiple generes, there will a row for the artist for each genre
    4. artist-popularity - (0-100) measure of artist's popularity
    """
    # Query Spotify for artists metadata
    artists_metadata = spotify_client.artists(
        artist_ids) if len(artist_ids) > 0 else []

    artist_dict_list = []
    for artist in artists_metadata:
        artist_dict = {}
        soundprintutils.update_dict_by_schema(artist_dict,
                                              ArtisterCommon.ARTIST_ID,
                                              artist.id)
        soundprintutils.update_dict_by_schema(artist_dict,
                                              ArtisterCommon.ARTIST_NAME,
                                              artist.name)
        soundprintutils.update_dict_by_schema(artist_dict,
                                              ArtisterCommon.ARTIST_POPULARITY,
                                              artist.popularity)

        genres = artist.genres
        artist_dict_list += soundprintutils.normalize_dict_field_list(
            artist_dict, genres, ArtisterCommon.ARTIST_GENRE)

    return pd.DataFrame(artist_dict_list, columns=ArtisterCommon.SCHEMA)
Exemple #8
0
    def test_modify_default_sender_instance(self):
        instance = MagicMock()

        from tekore import sender, Spotify
        sender.default_sender_instance = instance

        s = Spotify()
        self.assertIs(s.sender, instance)
Exemple #9
0
    def test_modify_default_sender_type(self):
        instance = MagicMock()
        type_mock = MagicMock(return_value=instance)

        from tekore import sender, Spotify
        sender.default_sender_type = type_mock

        s = Spotify()
        self.assertIs(s.sender, instance)
Exemple #10
0
    def test_instance_has_precedence_over_type(self):
        instance = MagicMock()
        type_mock = MagicMock(return_value=MagicMock())

        from tekore import sender, Spotify
        sender.default_sender_type = type_mock
        sender.default_sender_instance = instance

        s = Spotify()
        self.assertIs(s.sender, instance)
Exemple #11
0
def get_song_spotify(app_token):
    global loopnum
    global attempts
    spotify = Spotify(app_token)
    playlist = spotify.playlist(input("enter spotify playlist id"))  # Spotify pLaylist id goes here
    while_loop = True
    while while_loop:
        try:
            track = spotify.playlist_tracks(playlist.id).items[loopnum].track
            song = track.name
            artist = track.artists
            artist = artist[0]
            artist = artist.name
            full = str(song) + ' by ' + str(artist)
            print(str(loopnum + 1) + '. ' + full)
            get_song_youtube(full)

        except IndexError:
            while_loop = False
            print("Completed Successfully")
Exemple #12
0
def get_tracks_played_after(spotify_client: tk.Spotify, after_timestamp_ms: int) -> pd.DataFrame:
    """
    Using the spotify-client, this function will query Spotify Web API to get recently played tracks after the
    point of time specified by the provided timestamp.
    The results are extracted, parsed and returned in a pandas DataFrame with schema according to ListenerCommon#SCHEMA.
    The column for ListenerCommon#LISTENED_TIME is not populated for any row and is NaN
    :param spotify_client: Client with access token to query Spotify Web API
    :param after_timestamp_ms: Epoch time in milliseconds after which recently played tracks are to be queried
    :return: pandas DataFrame with relevant fields of recently played soundtracks on Spotify
    """
    df = pd.DataFrame([], columns=ListenerCommon.SCHEMA)

    response = spotify_client.playback_recently_played(limit=50, after=after_timestamp_ms)
    while len(response.items) > 0:
        df = df.append(pd.DataFrame(extract_playback_info(response.items), columns=ListenerCommon.SCHEMA))
        if response.cursors is None:
            break
        else:
            next_after_ms = int(response.cursors.after)
            response = spotify_client.playback_recently_played(limit=50, after=next_after_ms)

    return df
def get_song_spotify(app_token):
    global attempts
    spotify = Spotify(app_token)
    playlist_id_spotify = input("Enter the spotify playlist id")
    playlist = spotify.playlist_items(playlist_id_spotify, as_tracks=True)
    print(playlist)
    playlist = playlist["items"]
    print(playlist)
    try:
        i = 0
        songIds = []
        whileLoop = True

        # Gets the song ids from the returned dictionary
        while whileLoop:
            subPlaylist = playlist[i]
            subPlaylist.pop("added_at", None)
            subPlaylist.pop("added_by", None)
            subPlaylist.pop("is_local", None)
            subPlaylist.pop("primary_color", None)
            subPlaylist = subPlaylist["track"]
            subPlaylist.pop("album", None)
            subPlaylist.pop("artists", None)
            subPlaylist.pop("available_markets", None)
            subPlaylist = subPlaylist["id"]
            print(subPlaylist)
            songIds.append(subPlaylist)
            i += 1

    except IndexError:
        pass

    for i in range(len(songIds)):
        track = spotify.track(songIds[i], market=None)
        artist = track.artists
        artist = artist[0]
        print(f"{track.name} by {artist.name}")
        get_song_youtube(f"{track.name} by {artist.name}")
Exemple #14
0
    async def load_auth(self):

        try:
            token_contents = await FileUtils.get_data_from_file(
                self.config.data_directory, 'token')
            #conf=(self.config["client_id"], self.config["client_secret"], self.config["client_redirect_uri"])
            #if isfile(os.path.join(self.config['base_directory'], 'token.json')):
            #    with open(os.path.join(self.config['base_directory'], 'token.json'),'r') as jsonfile:
            #       token_contents=json.loads(jsonfile.read())
            #self.token = RefreshingToken(None, self.credentials)
            #self.token.refresh_user_token(conf,token_contents['refresh_token'])
            #logger.info('Using refresh Token: %s' % token_contents['refresh_token'])
            token = self.credentials.refresh_user_token(
                token_contents['refresh_token'])
            #logger.info('pre Token: %s' % token)
            self.token = RefreshingToken(token, self.credentials)
            self.spotify = Spotify(token=self.token,
                                   sender=self.sender,
                                   max_limits_on=True)
            #else:
            #    logger.error('!! Error token file not found and no refresh token available.')
        except:
            logger.error('.. Error loading token', exc_info=True)
Exemple #15
0
    async def set_token(self, code):
        try:
            logger.info('.. Setting token from code: %s...' % code[:10])
            self.code = code
            token = self.credentials.request_user_token(code)
            self.token = RefreshingToken(token, self.credentials)
            logger.info('.. Token is now: %s' % self.token)
            await self.save_auth(token=self.token, code=self.code)
            self.spotify = Spotify(token=self.token,
                                   sender=self.sender,
                                   max_limits_on=True)
            #await self.monitor_token()

            # This is currently removed for troubleshooting when a device gets picked
            #if self.spotify:
            #    if not self.device:
            #        await self.set_playback_device(self.config['default_device'])

            await self.update_list('update')
            await self.update_now_playing()
        except:
            logger.error('Error setting token from code %s' % code[:10],
                         exc_info=True)
Exemple #16
0
async def startup() -> None:
    global _credentials, _api
    _credentials = Credentials(
        conf.CLIENT_ID,
        conf.CLIENT_SECRET,
        conf.REDIRECT_URI,
        sender=AsyncSender(
            AsyncClient(http2=True, timeout=conf.SPOTIFY_API_TIMEOUT)),
        asynchronous=True,
    )
    _api = Spotify(
        sender=AsyncSender(
            AsyncClient(http2=True, timeout=conf.SPOTIFY_API_TIMEOUT)),
        asynchronous=True,
    )
    await database.connect()
    _logger.info("melodiam-auth started")
Exemple #17
0
def get_albums_data(spotify_client: tk.Spotify,
                    album_ids: List[str]) -> pd.DataFrame:
    """
    Given a spotify-client and list of album-ids, compiles a dataframe with metadata for all the albums.
    The compiled metadata contains the following fields for each album:
    1. album-id
    2. album-name
    3. album-type - album/single/compilation
    4. genre - if album has multiple generes, there will a row for the album for each genre
    5. album-release-date
    6. label - Music label for releasing the album
    7. album-track-count - total number of tracks in album
    8. album-popularity - (0-100) number signifying album's popularity
    """
    # Query Spotify for albums metadata
    albums_metadata = spotify_client.albums(
        album_ids) if len(album_ids) > 0 else []

    album_dict_list = []
    for album in albums_metadata:
        album_dict = {}
        soundprintutils.update_dict_by_schema(album_dict,
                                              AlbumerCommon.ALBUM_ID, album.id)
        soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.TYPE,
                                              album.album_type)
        soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.LABEL,
                                              album.label)
        soundprintutils.update_dict_by_schema(album_dict, AlbumerCommon.NAME,
                                              album.name)
        soundprintutils.update_dict_by_schema(album_dict,
                                              AlbumerCommon.POPULARITY,
                                              album.popularity)
        soundprintutils.update_dict_by_schema(
            album_dict, AlbumerCommon.RELEASE_DATE,
            pd.to_datetime(album.release_date).timestamp())
        soundprintutils.update_dict_by_schema(album_dict,
                                              AlbumerCommon.TOTAL_TRACKS,
                                              album.total_tracks)

        genres = album.genres
        album_dict_list += soundprintutils.normalize_dict_field_list(
            album_dict, genres, AlbumerCommon.GENRE)

    return pd.DataFrame(album_dict_list, columns=AlbumerCommon.SCHEMA)
Exemple #18
0
class sofa_spotify_controller(object):
    def __init__(self, config, app=None):
        self.config = config
        self.loop = asyncio.get_event_loop()
        self.app = app
        self.device = None
        self.user_pause = False
        self.task = None
        self.active = False
        self.running = True
        self.info = {}
        self.user_info = {}
        self.token = None
        self.spotify = Spotify()
        self.last_update = None
        self.credentials = Credentials(self.config.client_id,
                                       self.config.client_secret,
                                       self.config.client_redirect_uri)
        self.sender = RetryingSender(sender=AsyncSender())

        self.playback_device_name = self.config.default_device

        self.user_info = {}
        self.now_playing_data = {}
        self.loop.run_until_complete(self.load_auth())

        self.backup_playlist = self.loop.run_until_complete(
            self.load_and_confirm('backup_playlist'))
        self.user_playlist = self.loop.run_until_complete(
            self.load_and_confirm('user_playlist'))
        self.recent_picks = self.loop.run_until_complete(
            self.load_and_confirm('recent_picks'))
        self.blacklist = self.loop.run_until_complete(self.load_blacklist())
        self.idle_timeout = 300
        self.current_track_user = None

    async def start(self):
        try:
            nowplaying = await self.update_now_playing()
        except:
            logger.error('.! error starting initial nowplaying check',
                         exc_info=True)
            self.active = False

    @property
    def auth_url(self):
        try:
            return self.credentials.user_authorisation_url(
                scope=tekore.scope.every)
        except:
            logger.error('.. error retrieving authorization url',
                         exc_info=True)
        return ""

    async def load_blacklist(self):
        try:
            return await FileUtils.get_data_from_file(
                self.config.data_directory, 'blacklist')
        except:
            logger.error('!! could not load blacklist')
            return []

    async def load_and_confirm(self, list_name):
        # Load queues from disk and ensure they have a selection_tracker uuid to help deal with unique key requirements in the client
        playlist = {}
        try:
            playlist = await FileUtils.get_data_from_file(
                self.config.data_directory, list_name)
            if playlist == None:
                playlist = {'tracks': []}
            if type(playlist) == list:
                logger.error('!! Error - legacy format playlist for %s' %
                             list_name)
                playlist = {"tracks": playlist}
                await FileUtils.save_data_to_file(self.config.data_directory,
                                                  list_name, playlist)

            if 'tracks' in playlist and len(playlist['tracks']) > 0:
                for item in playlist['tracks']:
                    if 'selection_tracker' not in item:
                        item['selection_tracker'] = str(uuid.uuid4())
            else:
                playlist['tracks'] = []
        except:
            logger.error('!! error loading and checking list: %s' % listname)
        return playlist

    async def load_auth(self):

        try:
            token_contents = await FileUtils.get_data_from_file(
                self.config.data_directory, 'token')
            #conf=(self.config["client_id"], self.config["client_secret"], self.config["client_redirect_uri"])
            #if isfile(os.path.join(self.config['base_directory'], 'token.json')):
            #    with open(os.path.join(self.config['base_directory'], 'token.json'),'r') as jsonfile:
            #       token_contents=json.loads(jsonfile.read())
            #self.token = RefreshingToken(None, self.credentials)
            #self.token.refresh_user_token(conf,token_contents['refresh_token'])
            #logger.info('Using refresh Token: %s' % token_contents['refresh_token'])
            token = self.credentials.refresh_user_token(
                token_contents['refresh_token'])
            #logger.info('pre Token: %s' % token)
            self.token = RefreshingToken(token, self.credentials)
            self.spotify = Spotify(token=self.token,
                                   sender=self.sender,
                                   max_limits_on=True)
            #else:
            #    logger.error('!! Error token file not found and no refresh token available.')
        except:
            logger.error('.. Error loading token', exc_info=True)

    async def save_auth(self, token=None, code=None):

        try:
            token_data = {
                'last_code': code,
                "type": token.token_type,
                "access_token": token.access_token,
                "refresh_token": token.refresh_token,
                "expires_at": token.expires_at
            }
            logger.info('.. saving token data: %s' % token_data)
            async with aiofiles.open(
                    os.path.join(self.config.data_directory, 'token.json'),
                    'w') as f:
                await f.write(json.dumps(token_data))
        except:
            logger.error('.. Error saving token and code' %
                         (token[:10], code[:10]),
                         exc_info=True)

    async def set_token(self, code):
        try:
            logger.info('.. Setting token from code: %s...' % code[:10])
            self.code = code
            token = self.credentials.request_user_token(code)
            self.token = RefreshingToken(token, self.credentials)
            logger.info('.. Token is now: %s' % self.token)
            await self.save_auth(token=self.token, code=self.code)
            self.spotify = Spotify(token=self.token,
                                   sender=self.sender,
                                   max_limits_on=True)
            #await self.monitor_token()

            # This is currently removed for troubleshooting when a device gets picked
            #if self.spotify:
            #    if not self.device:
            #        await self.set_playback_device(self.config['default_device'])

            await self.update_list('update')
            await self.update_now_playing()
        except:
            logger.error('Error setting token from code %s' % code[:10],
                         exc_info=True)

    def authenticated(func):
        def wrapper(self):
            #logger.info('checking authentication')
            if self.token and self.spotify:
                return func(self)
            else:
                logger.info('must be authenticated before using spotify API')
                #return False
                raise AuthorizationNeeded

        return wrapper

    async def get_user(self):

        try:
            if self.token:
                #logger.info('user: %s' % await self.spotify.current_user())
                userobj = await self.spotify.current_user()
                return userobj.asbuiltin()
        except tekore.client.decor.error.Unauthorised:
            logger.error('.. Invalid access token: %s' %
                         self.token.access_token)
        except:
            logger.error('.. error getting user info', exc_info=True)
        return {}

    async def add_blacklist_term(self, term):

        if not self.blacklist:
            self.blacklist = []
        if term not in self.blacklist:
            self.blacklist.append(term)
            logger.info('.. added %s to blacklist' % term)
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'blacklist', self.blacklist)
            return True
        return False

    async def remove_blacklist_term(self, term):

        if not self.blacklist:
            self.blacklist = []
        if self.blacklist and term in self.blacklist:
            self.blacklist.remove(term)
            logger.info('.. removed %s from blacklist' % term)
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'blacklist', self.blacklist)
            return True
        return False

    async def filter_tracks_blacklist(self, tracks):

        results = []
        for track in tracks:
            artist_title = ("%s %s" % (track['artist'], track['name'])).lower()
            if any(word.lower() in artist_title for word in self.blacklist):
                logger.info('.. filtered result: %s' % artist_title)
            else:
                results.append(track)
        return results

    async def restart_local_playback_device(self):
        # This allows you to select a playback device by name
        try:
            stdoutdata = subprocess.getoutput("systemctl restart raspotify")
            logger.info('>> restart local playback device %s' % stdoutdata)
            return True
        except:
            logger.error('Error restarting local playback', exc_info=True)
        return False

    async def set_playback_device(self, name, restart=True):
        # This allows you to select a playback device by name
        try:
            # try to restart the local spotifyd since it tends to fail over time
            devs = await self.spotify.playback_devices()
            for dev in devs:
                if dev.id == name or dev.name == name:
                    logger.info('.. transferring to device %s (%s)' %
                                (dev.name, dev.id))
                    await self.spotify.playback_transfer(dev.id)
                    self.device = dev.id
                    self.playback_device_name = dev.name
                    return True

            logger.info('did not find local playback device %s.  restarting' %
                        name)

            await self.restart_local_playback_device()
            await asyncio.sleep(2)

            devs = await self.spotify.playback_devices()
            for dev in devs:
                if dev.name == name:
                    logger.info('transferring to %s' % dev.id)
                    await self.spotify.playback_transfer(dev.id)
                    self.device = dev.id
                    self.playback_device_name = dev.name
                    return True

            return False
        except:
            logger.error('Error setting playback device to %s' % name,
                         exc_info=True)

    async def check_playback_devices(self):
        # This allows you to select a playback device by name
        try:
            devs = await self.spotify.playback_devices()
            for dev in devs:
                logger.info('Device: %s' % dev)

        except:
            logger.error('Error checking playback devices', exc_info=True)
        return False

    async def check_playback_device(self):

        try:
            devs = await self.spotify.playback_devices()
            for dev in devs:
                if dev.is_active:
                    return True

        except:
            logger.error('Error checking playback device', exc_info=True)
        return False

    @authenticated
    async def get_active_playback_device(self):
        # This allows you to select a playback device by name
        try:
            devs = await self.spotify.playback_devices()
            for dev in devs:
                #logger.info('dev: %s %s' % (dev.is_active, dev))
                if dev.is_active:
                    return {
                        "name": dev.name,
                        "id": dev.id,
                        "volume": dev.volume_percent
                    }

        except:
            logger.error('Error checking playback devices', exc_info=True)
        return {}

    @authenticated
    async def get_playback_devices(self):

        try:
            result = []
            devs = await self.spotify.playback_devices()
            for dev in devs:
                result.append({
                    "name": dev.name,
                    "id": dev.id,
                    "is_active": dev.is_active,
                    "volume_percent": dev.volume_percent
                })

        except:
            logger.error('!! Error listing playback devices', exc_info=True)
        return result

    async def get_user_playlist(self, name):

        try:
            playlists = await self.spotify.followed_playlists()
            for playlist in playlists.items:
                if playlist.name == name:
                    logger.info('found playlist: %s %s' %
                                (playlist.name, playlist.owner))
                    return {"name": playlist.name, "id": playlist.id}
            return {}
        except:
            logger.error('Error searching spotify', exc_info=True)
            return {}

    async def get_playlist_source_data(self, id, list_type):
        if list_type == "playlist":
            playlist = await self.spotify.playlist(id)
            covers = await self.spotify.playlist_cover_image(playlist.id)
            if len(covers) > 0:
                cover = covers[0].url
            return {
                "name": playlist.name,
                "art": cover,
                "owner": playlist.owner.id,
                "display": playlist.name
            }
        if list_type == "radio":
            track = await self.spotify.track(id)
            return {
                "name": track.name,
                "art": track.album.images[0].url,
                "artist": track.artists[0].name,
                "album": track.album.name,
                "display":
                track.name + " - " + track.artists[0].name + " radio"
            }
        return {}

    async def get_user_playlists(self):

        try:
            display_list = []
            playlists = await self.spotify.followed_playlists()
            #playlists = self.spotify.followed_playlists()
            for playlist in playlists.items:
                #logger.info('found playlist: %s %s' % (playlist.name, playlist.owner.id))
                try:
                    cover = ""
                    covers = await self.spotify.playlist_cover_image(
                        playlist.id)
                    if len(covers) > 0:
                        cover = covers[0].url
                except concurrent.futures._base.CancelledError:
                    logger.error('Error getting cover for %s (cancelled)' %
                                 playlist.name)
                except:
                    logger.error('Error getting cover for %s' % playlist.name,
                                 exc_info=True)
                display_list.append({
                    "name": playlist.name,
                    "id": playlist.id,
                    "art": cover,
                    "owner": playlist.owner.id
                })
            return display_list
        except:
            logger.error('Error getting user playlists from spotify',
                         exc_info=True)
            return []

    async def get_playlist_tracks(self, id):

        try:
            display_list = []
            #playlist = self.spotify.playlist(id)
            tracks = await self.spotify.playlist_items(id)
            #tracks = await self.spotify.playlist_tracks(id)
            tracks = self.spotify.all_items(tracks)
            logger.info('.. Tracks: %s' % tracks)
            async for track in tracks:
                display_list.append({
                    "id": track.track.id,
                    'selection_tracker': str(uuid.uuid4()),
                    "name": track.track.name,
                    "art": track.track.album.images[0].url,
                    "artist": track.track.artists[0].name,
                    "album": track.track.album.name,
                    "url": track.track.href
                })
            return display_list
        except:
            logger.error('Error getting spotify playlist tracks',
                         exc_info=True)
            return []

    async def add_radio(self, song_id, limit=50):
        try:
            #track = await self.spotify.track(song_id)
            recommendations = await self.spotify.recommendations(
                track_ids=[song_id], limit=limit)
            #logger.info('recommendations: %s' % recommendations)
            track_list = []
            for track in recommendations.tracks:
                pltrack = {
                    "id": track.id,
                    "name": track.name,
                    "art": track.album.images[0].url,
                    "artist": track.artists[0].name,
                    "album": track.album.name,
                    "url": track.href,
                    "votes": 1,
                    "count": 0
                }
                logger.info('Adding track: %s - %s' %
                            (pltrack['artist'], pltrack['name']))
                track_list.append(pltrack)
            track_list = await self.filter_tracks_blacklist(track_list)
            self.backup_playlist = {
                "id": song_id,
                "type": "radio",
                "tracks": list(track_list)
            }
            self.backup_playlist.update(await self.get_playlist_source_data(
                song_id, "radio"))
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'backup_playlist',
                                              self.backup_playlist)
            return track_list
        except:
            logger.error('Error setting backup playlist from radio track %s' %
                         song_id,
                         exc_info=True)
            return []

    async def search(self, search, types=('track', ), limit=20):
        try:
            display_list = []
            result = await self.spotify.search(search,
                                               types=types,
                                               limit=limit)
            for track in result[0].items:
                display_list.append({
                    "id": track.id,
                    "name": track.name,
                    "art": track.album.images[0].url,
                    "artist": track.artists[0].name,
                    "album": track.album.name,
                    "url": track.href
                })
            display_list = await self.filter_tracks_blacklist(display_list)
            return display_list

        except:
            logger.error('Error searching spotify', exc_info=True)
            return []

    async def add_track_to_playlist(self, song_id, playlist_id):
        try:
            #playlist=await self.get_user_playlist("Discovered")
            #playlist_id=playlist['id']
            track = await self.spotify.track(song_id)
            track_data = self.get_track_data(track)
            await self.spotify.playlist_add(playlist_id, [track.uri])
            return track_data
        except:
            logger.error('Error adding tracks to playlist %s' % dir(track),
                         exc_info=True)
        return {}

    async def add_to_recent_picks(self, track, user=None):
        recent = []
        if 'tracks' in self.recent_picks:
            for prev_pick in self.recent_picks['tracks']:
                logger.info('prev: %s' % prev_pick)
                if prev_pick['id'] != track.id:
                    recent.append(prev_pick)

        new_track = {
            "id": track.id,
            "name": track.name,
            "art": track.album.images[0].url,
            "artist": track.artists[0].name,
            "album": track.album.name,
            "url": track.href,
            "user": user,
            "time": datetime.now().isoformat()
        }
        recent.append(new_track)
        self.recent_picks['tracks'] = recent[-25:]
        await FileUtils.save_data_to_file(self.config.data_directory,
                                          'recent_picks', {self.recent_picks})

    async def add_track(self, song_id, user=None):
        try:
            track = await self.spotify.track(song_id)
            track_json = {
                "id": track.id,
                "name": track.name,
                "art": track.album.images[0].url,
                "artist": track.artists[0].name,
                "album": track.album.name,
                "url": track.href,
                "user": user,
                "time": datetime.now().isoformat()
            }
            logger.info('Adding track for %s: %s - %s' %
                        (user, track_json['artist'], track_json['name']))
            self.user_playlist['tracks'].append(track_json)
            self.user_playlist['tracks'] = await self.filter_tracks_blacklist(
                self.user_playlist['tracks'])
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'user_playlist',
                                              self.user_playlist)
            self.loop.create_task(self.add_to_recent_picks(track, user))

            await self.update_list('update')
            track_data = await self.get_track_data(
                track)  # get json version for returning to web user
            return track_data
        except:
            logger.error('Error adding song %s' % song_id, exc_info=True)
        return {}

    async def del_track(self, song_id):
        try:
            remove_count = 0
            newlist = []
            for song in self.user_playlist['tracks']:
                if song['id'] != song_id:
                    logger.info('Adding non-delete: %s vs %s' %
                                (song['id'], song_id))
                    newlist.append(song)
                else:
                    remove_count += 1
            self.user_playlist['tracks'] = newlist
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'user_playlist',
                                              self.user_playlist)
            #self.app.saveJSON('user_playlist', self.user_playlist)

            newlist = []
            for song in self.backup_playlist['tracks']:
                if song['id'] != song_id:
                    newlist.append(song)
                else:
                    remove_count += 1
            self.backup_playlist['tracks'] = newlist
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'backup_playlist',
                                              self.backup_playlist)
            #self.app.saveJSON('backup_playlist', self.backup_playlist)
            await self.update_list('update')
            return {"removed": remove_count}
        except:
            logger.error('Error adding song %s' % song_id, exc_info=True)
            return []

    async def shuffle_backup(self):
        try:
            promoted_list = []
            working_backup = []
            ids = []
            for item in self.backup_playlist['tracks']:
                if item['id'] not in ids:
                    ids.append(item['id'])
                else:
                    logger.info('dupe track: %s' % item)
                if 'promoted' in item and item['promoted'] == True:
                    promoted_list.append(item)
                else:
                    working_backup.append(item)
            random.shuffle(working_backup)
            self.backup_playlist['tracks'] = promoted_list + working_backup
            #logger.info('.. new backup list: %s' % self.backup_playlist)
            return self.backup_playlist
        except:
            logger.error('Error shuffling backup list', exc_info=True)
            return []

    async def get_queue(self):
        return {'user': self.user_playlist, 'backup': self.backup_playlist}

    async def get_recent_picks(self):
        return {"recent": self.recent_picks}

    async def clear_queue(self):
        self.user_playlist = {}
        self.backup_playlist = {}

    async def list_next_tracks(self, maxcount=5):
        try:
            next_tracks = []
            next_tracks = self.user_playlist['tracks'][:maxcount]
            if len(next_tracks) < maxcount:
                remaining = maxcount - len(next_tracks)
                next_tracks = next_tracks + self.backup_playlist[
                    'tracks'][:remaining]
            return next_tracks
        except:
            logger.error('Error getting next tracks', exc_info=True)
        return []

    async def update_list(self, action):
        try:
            nowplaying = await self.now_playing()
            await self.app.update({'playlist': action})
            #await self.app.server.add_sse_update({'playlist':action})

        except:
            logger.error('Error updating now playing subscribers',
                         exc_info=True)
            return []

    async def get_track_data(self, track):
        try:
            #logger.info('track type: %s' % track.json())
            if not track:
                return {}
            item = getattr(track, 'item', track)

            data = {
                "id": item.id,
                "name": item.name,
                "art": item.album.images[0].url,
                "artist": item.artists[0].name,
                "album": item.album.name,
                "url": item.href,
                "is_playing": getattr(track, 'is_playing', False),
                "length": int(item.duration_ms / 1000),
                "position": int(getattr(track, 'progress_ms', 0) / 1000)
            }
            return data
        except:
            logger.error('.. error getting track data from %s' % track,
                         exc_info=True)
        return {}

    async def now_playing(self):
        try:
            nowplaying = {}
            npdata = None
            #pb=await self.spotify.playback_recently_played()
            #logger.info('test: %s' % pb)

            if await self.check_playback_device():
                npdata = await self.spotify.playback_currently_playing()
            else:
                await self.set_playback_device('jukebox')
                recent = await self.spotify.playback_recently_played(limit=1)
                npdata = recent.items[0].track
            nowplaying = await self.get_track_data(npdata)
            if self.current_track_user:
                nowplaying['user'] = self.current_track_user

        except requests.exceptions.HTTPError:
            logger.warn('.. Token may have expired: %s' % self.token)
            self.active = False
        except tekore.Unauthorised:
            logger.error(
                '!! Error - Unauthorized to Spotify - token may be missing or expired.'
            )
            self.active = False
        except:
            logger.error('Error getting now playing', exc_info=True)
            self.active = False
        return nowplaying

    async def pause(self):
        try:
            if await self.check_playback_device():
                logger.info('-> sending pause to spotify')
                await self.spotify.playback_pause()
                await self.update_now_playing()
                self.user_pause = True
                return True
        except:
            logger.error('Error pausing', exc_info=True)
        return False

    async def play(self):
        try:
            if not await self.get_active_playback_device():
                #logger.error('!! error - no playback device: %s' % await self.get_active_playback_device())
                await self.set_playback_device(self.playback_device_name)
                if not await self.get_active_playback_device():
                    return False

            logger.info(".. playing on %s" %
                        await self.get_active_playback_device())
            playing = await self.spotify.playback_currently_playing()
            if playing.item == None:
                await self.next_track()
            try:
                await self.spotify.playback_resume()
            except tekore.Forbidden:
                logger.error('!! error - could not resume playback',
                             exc_info=True)
                await self.next_track()

            self.active = True
            await self.update_now_playing()
            self.user_pause = False
            return True

            # TODO: need handler for this error:
            # tekore.client.decor.error.NotFound: Error in https://api.spotify.com/v1/me/player/play:
            # 404: Player command failed: No active device found
            # Requires an active device and the user has none.

        except:
            logger.error('Error playing', exc_info=True)
        return False

    async def set_backup_playlist(self, playlist_id):
        try:
            track_list = await self.get_playlist_tracks(playlist_id)
            for item in track_list:
                item['selection_tracker'] = str(uuid.uuid4())

            self.backup_playlist = {
                "id": playlist_id,
                "type": "playlist",
                "tracks": list(track_list)
            }
            self.backup_playlist.update(await self.get_playlist_source_data(
                playlist_id, "playlist"))
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'backup_playlist',
                                              self.backup_playlist)
            return track_list
        except:
            logger.error('Error setting backup playlist', exc_info=True)
            return []

    async def get_playlist(self, playlist_id):
        try:
            track_list = await self.get_playlist_tracks(playlist_id)
            return list(track_list)
        except:
            logger.error('Error setting backup playlist', exc_info=True)
            return []

    async def track_ready(self):
        try:
            if 'tracks' in self.user_playlist and len(
                    self.user_playlist['tracks']) > 0:
                return True
            if 'tracks' in self.backup_playlist and len(
                    self.backup_playlist['tracks']) > 0:
                return True
        except:
            logger.error('Error checking for ready track from queues',
                         exc_info=True)
        return False

    async def get_next_track(self):
        try:
            next_track = {}
            next_track = await self.pop_user_track()
            if next_track:
                self.current_track_user = next_track['user']
                logger.info('.. pulling user track: %s - %s' %
                            (next_track['artist'], next_track['name']))
            else:
                next_track = await self.pop_backup_track()
                if next_track:
                    self.current_track_user = None
                    logger.info('.. pulling backup track: %s - %s' %
                                (next_track['artist'], next_track['name']))
            return next_track
        except:
            logger.error('!! Error getting next track from queues',
                         exc_info=True)
            return {}

    async def pop_user_track(self):
        try:
            if 'tracks' in self.user_playlist and len(
                    self.user_playlist['tracks']) > 0:
                next_track = self.user_playlist['tracks'].pop(0)
                await FileUtils.save_data_to_file(self.config.data_directory,
                                                  'user_playlist',
                                                  self.user_playlist)
                return next_track
            else:
                return {}
        except:
            logger.error('Error getting track from backup playlist')
            return {}

    async def pop_backup_track(self):
        try:
            if 'tracks' in self.backup_playlist and len(
                    self.backup_playlist['tracks']) > 0:
                next_track = self.backup_playlist['tracks'].pop(0)
                await FileUtils.save_data_to_file(self.config.data_directory,
                                                  'backup_playlist',
                                                  self.backup_playlist)
                #self.app.saveJSON('backup_playlist', self.backup_playlist)
                return next_track
            else:
                return {}
        except:
            logger.error('Error getting track from backup playlist',
                         exc_info=True)
            return {}

    async def promote_backup_track(self, song_id, super_promote=False):
        try:
            newlist = []
            promoted_track = None
            promoted_count = 0
            for song in self.backup_playlist['tracks']:
                if song['id'] == song_id:
                    promoted_track = song
                else:
                    if 'promoted' in song and song['promoted'] == True:
                        promoted_count += 1
                    newlist.append(song)
            if promoted_track:
                if super_promote:
                    result = await self.add_track(promoted_track['id'])
                else:
                    promoted_track['promoted'] = True
                    if promoted_count == 0:
                        newlist.insert(0, promoted_track)
                    else:
                        newlist.insert(promoted_count, promoted_track)
            self.backup_playlist['tracks'] = newlist
            await FileUtils.save_data_to_file(self.config.data_directory,
                                              'backup_playlist',
                                              self.backup_playlist)
            await self.update_list('update')
            return {"promoted": song_id}
        except:
            logger.error('Error adding song %s' % song_id, exc_info=True)
            return []

    async def next_track(self):
        try:
            next_track = await self.get_next_track()
            if next_track:
                self.active = True
                result = await self.play_id(next_track['id'])
                await self.update_list('pop')
                await self.update_now_playing()
                return result
            else:
                logger.warning('!! No more tracks to play')
                self.active = False
        except:
            logger.error('Error trying to play', exc_info=True)
            self.active = False

    async def play_id(self, id):
        try:
            result = await self.spotify.playback_start_tracks([id])
            self.active = True
            return result
        except:
            logger.error('!! Error trying to play id %s' % id, exc_info=True)
            self.active = False
        return False

    async def seek_pos(self, position):
        try:
            logger.info('.. seeking to %s / %s' %
                        (int(position) * 1000,
                         self.now_playing_data['nowplaying']['length']))
            await self.spotify.playback_seek(int(position) * 1000)
            result = await self.update_now_playing()
            self.active = True
            return result
        except:
            logger.error('!! Error trying to seek to position %s' % position,
                         exc_info=True)
            self.active = False

    def stop(self):
        self.task.cancel()

    async def update_now_playing(self, event=None):
        try:
            try:
                iso = self.last_update.isoformat()
            except:
                iso = None
            track_change = False
            if "nowplaying" in self.now_playing_data:
                now_playing_data = dict(self.now_playing_data["nowplaying"])
            if event == None:
                now_playing_data = await self.now_playing()
                logger.info('.. Updating nowplaying data: %s' %
                            now_playing_data)
            else:
                if event['player_event'] == 'change':
                    logger.info('.. event change: %s / %s' %
                                (event, now_playing_data))
                    if event['track_id'] == event['old_track_id']:
                        track_change = True
                    elif 'track_id' in self.now_playing_data[
                            'nowplaying'] and event[
                                'track_id'] != now_playing_data['track_id']:
                        now_playing_data = await self.now_playing()
                        logger.info('.. Updating nowplaying data: %s' %
                                    now_playing_data)
                    elif event['player_event'] == "playing":
                        logger.info(
                            '.. Updating nowplaying data play state only')
                        now_playing_data['is_playing'] = True
                    elif event['player_event'] == "paused":
                        logger.info(
                            '.. Updating nowplaying data paused state only')
                        now_playing_data['is_playing'] = False
                    else:
                        now_playing_data = await self.now_playing()
                        logger.info('.. Updating nowplaying data: %s' %
                                    now_playing_data)

            idle = True

            if 'is_playing' in now_playing_data:
                if now_playing_data['is_playing']:
                    self.active = True
                    idle = False
                    self.last_update = datetime.now()
            # or now_playing_data['position']==0
            if track_change and not self.user_pause:
                if not event or event['old_track_id'] == event['track_id']:
                    logger.info(
                        '.. track ended: %s - %s' %
                        (now_playing_data['artist'], now_playing_data['name']))
                    self.active = False
                    idle = False
                    await self.next_track()
                    # If there is a next track, it should trigger the librespot reply

            if self.last_update and (datetime.now() - self.last_update
                                     ).total_seconds() < self.idle_timeout:
                idle = False

            #new_data={"idle": idle, "user_pause": self.user_pause, "last_update": iso, "nowplaying": now_playing_data, "device": await self.get_active_playback_device(), "devices": await self.get_playback_devices() }
            new_data = {
                "idle": idle,
                "user_pause": self.user_pause,
                "last_update": iso,
                "nowplaying": now_playing_data
            }

            await self.app.update(new_data)
            self.now_playing_data = new_data
            return self.now_playing_data

        except:
            logger.error('Error updating now playing subscribers',
                         exc_info=True)
            return {}

    async def control(self, command, params=None):

        # consolidating most of the media controls down to a single control function that calls the actual tekore/spotify
        # commands. Only commands that return updated now playing data should be used through control.  Other reporting
        # commands should use specific functions.

        # Each of these commands needs to be reviewed for duplication of the update_now_playing data

        if command == "nowplaying":
            pass  # no command needed, update now playing will run
        elif command == "play":
            result = await self.app.spotify_controller.play()
        elif command == "pause":
            result = await self.app.spotify_controller.pause()
        elif command == "next":
            result = await self.app.spotify_controller.next_track()
        elif command == "seek":
            result = await self.app.spotify_controller.seek_pos(params[0])
        elif command == "set_device":
            result = await self.app.spotify_controller.set_playback_device(
                params[0])
        else:
            return {"error": "invalid command: %s" % command}

        #result = await self.app.spotify_controller.update_now_playing()
        if not self.now_playing_data:
            result = await self.app.spotify_controller.update_now_playing()
        else:
            result = self.now_playing_data
        #logger.info('.. nowplaying update: %s / %s' % (command, result))
        return result
Exemple #19
0
def client():
    return Spotify('token')
Exemple #20
0
 def test_request_with_closed_client_raises(self):
     client = Spotify()
     client.close()
     with pytest.raises(RuntimeError):
         client.track('id')
Exemple #21
0
 async def test_request_with_closed_async_client_raises(self):
     client = Spotify(asynchronous=True)
     await client.close()
     with pytest.raises(RuntimeError):
         await client.track('id')
Exemple #22
0
    def test_specifying_limit_kwarg_overrides_max_limits(self, app_token):
        client = Spotify(app_token, max_limits_on=True)
        s, = client.search('piano', limit=1)

        assert s.limit == 1
Exemple #23
0
    def test_specifying_limit_pos_arg_overrides_max_limits(self, app_token):
        client = Spotify(app_token, max_limits_on=True)
        s, = client.search('piano', ('track', ), None, None, 1)

        assert s.limit == 1
Exemple #24
0
 def test_chunked_context_disables(self):
     client = Spotify(chunked_on=True)
     with client.chunked(False):
         assert client.chunked_on is False
Exemple #25
0
 def test_chunked_context_enables(self):
     client = Spotify()
     with client.chunked(True):
         assert client.chunked_on is True
Exemple #26
0
 def test_returns_model_list(self, app_token, track_ids):
     client = Spotify(app_token, chunked_on=True)
     tracks = client.tracks(track_ids)
     assert isinstance(tracks, ModelList)
Exemple #27
0
class SpotifyWebAPI(APIBase):
    player_name: str = "Spotify"
    artist: str = None
    title: str = None
    is_playing: bool = None

    def __init__(self, token: RefreshingToken) -> None:
        super().__init__()
        self.artist = ""
        self.title = ""
        self.is_playing = False
        self._position = 0
        self._token = token
        self._spotify = Spotify(self._token)
        self._event_timestamp = time.time()

    def connect_api(self) -> None:
        self._refresh_metadata()

    @property
    def position(self) -> int:
        """
        _refresh_metadata() has to be called because the song position is
        constantly changing.
        """

        self._refresh_metadata()
        return self._position

    def _refresh_metadata(self) -> None:
        """
        Refreshes the metadata of the player: artist, title, whether
        it's playing or not, and the current position.
        """

        metadata = self._spotify.playback_currently_playing()
        if metadata is None or metadata.item is None:
            raise ConnectionNotReady("No song currently playing")
        self.artist = metadata.item.artists[0].name
        self.title = metadata.item.name

        # Some local songs don't have an artist name so `split_title`
        # is called in an attempt to manually get it from the title.
        if self.artist == '':
            self.artist, self.title = split_title(self.title)

        self._position = metadata.progress_ms
        self.is_playing = metadata.is_playing

    def event_loop(self) -> None:
        """
        The event loop callback that checks if changes happen. This is called
        periodically within the Qt window.

        It checks for changes in:
            * The playback status (playing/paused) to change the player's too
            * The currently playing song: if a new song started, it's played
            * The position
        """

        # Previous properties are saved to compare them with the new ones
        # after the metadata refresh
        artist = self.artist
        title = self.title
        position = self._position
        is_playing = self.is_playing
        self._refresh_metadata()

        # First checking if a new song started, so that position or status
        # changes are related to the new song.
        if self.artist != artist or self.title != title:
            logging.info("New video detected")
            self.new_song_signal.emit(self.artist, self.title, 0)

        if self.is_playing != is_playing:
            logging.info("Status change detected")
            self.status_signal.emit(self.is_playing)

        # The position difference between calls is compared to the elapsed
        # time to know whether the position has been modified.
        # Changes will be ignored unless the position difference is
        # greater than the elapsed time (plus a margin) or if it's negative
        # (backwards).
        playback_diff = self._position - position
        calls_diff = int((time.time() - self._event_timestamp) * 1000)
        if playback_diff >= (calls_diff + 100) or playback_diff < 0:
            logging.info("Position change detected")
            self.position_signal.emit(self._position)

        # The time passed between calls is refreshed
        self._event_timestamp = time.time()
Exemple #28
0
 def test_too_many_chunked_succeeds(self, app_token, track_ids):
     client = Spotify(app_token, chunked_on=True)
     tracks = client.tracks(track_ids)
     assert len(track_ids) == len(tracks)
Exemple #29
0
 def __init__(self, api_key):
     self.api_key = api_key
     client_id, client_secret = api_key.split(':')
     self.token = request_client_token(client_id, client_secret)
     self.spotify = Spotify(self.token)
Exemple #30
0
 async def test_async_too_many_chunked_succeeds(self, app_token, track_ids):
     client = Spotify(app_token, chunked_on=True, asynchronous=True)
     tracks = await client.tracks(track_ids)
     assert len(track_ids) == len(tracks)
     await client.close()