Esempio n. 1
0
 def test_request_user_token(self):
     c = Credentials('id', 'secret', 'uri')
     send = MagicMock(return_value=mock_response())
     with patch(cred_module + '.send', send):
         c.request_user_token('code')
         send.assert_called_once()
     c.close()
Esempio n. 2
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