예제 #1
0
def pause_song():
    pause_req = request.get_json(force=True)
    spotify_id = pause_req['spotify_id']
    dev_id = pause_req['device_id']

    if spotify_id:
        db = Database(Config.DATABASE_PATH)
        token = db.get_user_token(spotify_id)
        spotify = Spotify(auth=token)
        print('PAUSING SONG')
        pause_data = spotify.pause_playback(device_id=dev_id)
        return create_response(pause_data)

    return jsonify({'Error': 'Invalid or missing spotify id'})
예제 #2
0
class SpotifyService(MusicService):
    """
    Spotify client music service.
    """
    def __init__(self,
                 state,
                 client_id=None,
                 client_secret=None,
                 redirect_uri=None,
                 device_name=None):
        """
        @see Service.__init__()

        For the real meanings of the kwargs, read the Spotipy documentation:
            https://spotipy.readthedocs.io
        You can also set them as the environment variables::
          * ``SPOTIPY_CLIENT_ID``
          * ``SPOTIPY_CLIENT_SECRET``
          * ``SPOTIPY_REDIRECT_URI``

        :type  client_id: str
        :param client_id:
            The Spotify client ID.
        :type  client_secret: str
        :param client_secret:
            The Spotify client secret.
        :type  redirect_uri: str
        :param redirect_uri:
            The Spotify redirect URI.
        :type  device_name: str
        :param device_name:
            The name of the device to control, if not the default one.
        """
        super(SpotifyService, self).__init__("SpotifyService", state,
                                             "Spotify")

        self._client_id = client_id
        self._client_secret = client_secret
        self._redirect_uri = redirect_uri
        self._device_name = device_name
        self._device_id = None
        self._spotify = None
        self._volume = None

    def set_volume(self, volume):
        """
        @see MusicService.set_volume()
        """
        if MIN_VOLUME <= volume <= MAX_VOLUME:
            self._volume = volume
            self._spotify.volume(100.0 * (volume - MIN_VOLUME) /
                                 (MAX_VOLUME - MIN_VOLUME),
                                 device_id=self._device_id)
        else:
            raise ValueError("Bad volume: %s", volume)

    def get_volume():
        """
        @see MusicService.get_volume()
        """
        return self._volume

    def is_playing(self):
        """
        Whether the player is playing.

        :rtype: bool
        :return:
           Whether the player is playing.
        """
        try:
            cur = self._spotify.currently_playing()
            return cur['is_playing']
        except:
            return False

    def play(self, uris):
        """
        Set the list of URIs playing.
        """
        LOG.info("Playing: %s", ' '.join(uris))
        self._spotify.start_playback(uris=uris, device_id=self._device_id)

    def pause(self):
        """
        Pause any currently playing music.
        """
        self._spotify.pause_playback(device_id=self._device_id)

    def unpause(self):
        """
        Resume any currently paused music.
        """
        self._spotify.start_playback(device_id=self._device_id)

    def _start(self):
        """
        @see Startable._start()
        """
        # This is what we need to be able to do
        scope = ','.join(('user-library-read', 'user-read-playback-state',
                          'user-modify-playback-state'))

        # Create the authorization manager, and then use that to create the
        # client
        auth_manager = SpotifyOAuth(client_id=self._client_id,
                                    client_secret=self._client_secret,
                                    redirect_uri=self._redirect_uri,
                                    scope=scope)
        self._spotify = Spotify(auth_manager=auth_manager)

        # See what devices we have to hand
        try:
            if self._device_name is not None:
                LOG.info("Looking for device named '%s'", self._device_name)

            devices = self._spotify.devices()
            for device in devices['devices']:
                # Say what we see
                name = device['name']
                id_ = device['id']
                type_ = device['type']
                active = device['is_active']
                vol = device['volume_percent']
                LOG.info("Found %sactive %s device: '%s'",
                         '' if active else 'in', type_, name)

                # See if we're looking for a specific device, if not just snoop
                # the volume from the first active one
                if self._device_name is not None:
                    if name == self._device_name:
                        LOG.info("Matched '%s' to ID '%s'", name, id_)
                        self._device_id = id_
                        self._volume = vol / 100.0 * MAX_VOLUME
                else:
                    if active and self._volume is None:
                        self._volume = vol / 100.0 * MAX_VOLUME

        except Exception as e:
            LOG.warning("Unable to determine active Spoify devices: %s", e)

        # If we were looking for a specific device then make sure that we found
        # it in the list
        if self._device_name is not None and self._device_id is None:
            raise ValueError("Failed to find device with name '%s'" %
                             (self._device_name, ))

    def _stop(self):
        """
        @see Startable._stop()
        """
        try:
            self._spotify.pause_playback(device_id=self._device_id)
        except:
            # Best effort
            pass

    def _match_artist(self, artist):
        """
        @see MusicService._match_artist()
        """
        artist = ' '.join(artist).lower()
        LOG.debug("Matching artist '%s'", artist)
        result = self._spotify.search(artist, type='artist')
        if 'artists' in result and 'items' in result['artists']:
            items = result['artists']['items']
            LOG.debug("Checking %d results", len(items))
            for item in items:
                name = item.get('name', '').lower()
                LOG.debug("Matching against '%s'", name)
                if fuzz.ratio(name, artist) > 80:
                    return True
        return False

    def _get_stop_handler(self, tokens):
        """
        @see MusicService._get_stop_handler()
        """
        return _SpotifyServicePauseHandler(self, tokens)

    def _get_play_handler(self, tokens):
        """
        @see MusicService._get_play_handler()
        """
        return _SpotifyServiceUnpauseHandler(self, tokens)

    def _get_toggle_pause_handler(self, tokens):
        """
        @see MusicService._get_toggle_pause_handler()
        """
        return _SpotifyServiceTogglePauseHandler(self, tokens)

    def _get_handler_for(self, tokens, platform_match, genre, artist,
                         song_or_album):
        """
        @see MusicService._get_handler_for()
        """
        # Do nothing if we have no name
        if song_or_album is None or len(song_or_album) == 0:
            return None

        # Normalise to strings
        name = ' '.join(song_or_album).lower()
        if artist is None or len(artist) == 0:
            artist = None
        else:
            artist = ' '.join(artist).lower()

        # We will put all the track URIs in here
        uris = []

        # Search by track name then album name, these are essentially the same
        # logic
        for which in ('track', 'album'):
            LOG.info("Looking for '%s'%s as a %s", name,
                     " by '%s'" % artist if artist else '', which)

            # This is the key in the results
            plural = which + 's'

            # Try using the song_or_album as the name
            result = self._spotify.search(name, type=which)
            if not result:
                LOG.info("No results")
                continue

            # Did we get back any tracks
            if plural not in result:
                LOG.error("%s was not in result keys: %s", plural,
                          result.keys())
                continue

            # We got some results back, let's assign scores to them all
            results = result[plural]
            matches = []
            for item in results.get('items', []):
                # It must have a uri
                if 'uri' not in item and item['uri']:
                    LOG.error("No URI in %s", item)

                # Look at all the candidate entries
                if 'name' in item:
                    # See if this is better than any existing match
                    name_score = fuzz.ratio(name, item['name'].lower())
                    LOG.debug("'%s' matches '%s' with score %d", item['name'],
                              name, name_score)

                    # Check to make sure that we have an artist match as well
                    if artist is None:
                        # Treat as a wildcard
                        artist_score = 100
                    else:
                        artist_score = 0
                        for entry in item.get('artists', []):
                            score = fuzz.ratio(artist,
                                               entry.get('name', '').lower())
                            LOG.debug("Artist match score for '%s' was %d",
                                      entry.get('name', ''), score)
                            if score > artist_score:
                                artist_score = score
                    LOG.debug("Artist match score was %d", artist_score)

                    # Only consider cases where the scores look "good enough"
                    if name_score > 75 and artist_score > 75:
                        LOG.debug("Adding match")
                        matches.append((item, name_score, artist_score))

            # Anything?
            if len(matches) > 0:
                LOG.debug("Got %d matches", len(matches))

                # Order them accordingly
                matches.sort(key=lambda e: (e[1], e[2]))

                # Now, pick the top one
                best = matches[0]
                item = best[0]
                LOG.debug("Best match was: %s", item)

                # Extract the info
                item_name = item.get('name', None) or name
                artists = item.get('artists', [])
                artist_name = (artists[0].get('name', None)
                               if len(artists) > 0 else None) or artist

                # Description of what we are playing
                what = item_name if item_name else name
                if artist_name:
                    what += " by " + artist_name
                what += " on Spotify"

                # The score is the geometric value of the two
                score = sqrt(best[1] * best[1] + best[2] * best[2]) / 100.0

                # The should be here
                assert 'uri' in item, "Missing URI in %s" % (item, )
                uri = item['uri']

                # If we are an album then grab the track URIs
                if which == 'album':
                    tracks = self._spotify.album_tracks(uri)
                    if tracks and 'items' in tracks:
                        uris = [track['uri'] for track in tracks['items']]
                else:
                    # Just the track
                    uris = [uri]

                # And we're done
                break

        # Otherwise assume that it's an artist
        if len(uris) == 0 and artist is None:
            LOG.info("Looking for '%s' as an artist", name)
            result = self._spotify.search(name, type='artist')
            LOG.debug("Got: %s", result)

            if result and 'artists' in result and 'items' in result['artists']:
                items = sorted(result['artists']['items'],
                               key=lambda entry: fuzz.ratio(
                                   name,
                                   entry.get('name', '').lower()),
                               reverse=True)

                # Look at the best one, if any
                LOG.debug("Got %d matches", len(items))
                if len(items) > 0:
                    match = items[0]
                    who = match['name']
                    what = "%s on Spotify" % (who, )
                    score = fuzz.ratio(who.lower(), name)

                    # Find all their albums
                    if 'uri' in match:
                        LOG.debug("Got match: %s", match['uri'])
                        artist_albums = self._spotify.artist_albums(
                            match['uri'])
                        for album in artist_albums.get('items', []):
                            # Append all the tracks
                            LOG.debug("Looking at album: %s", album)
                            if 'uri' in album:
                                tracks = self._spotify.album_tracks(
                                    album['uri'])
                                if tracks and 'items' in tracks:
                                    LOG.debug(
                                        "Adding tracks: %s",
                                        ' '.join(track['name']
                                                 for track in tracks['items']))
                                    uris.extend([
                                        track['uri']
                                        for track in tracks['items']
                                    ])

        # And now we can give it back, if we had something
        if len(uris) > 0:
            return _SpotifyServicePlayHandler(self, tokens, what, uris, score)
        else:
            # We got nothing
            return None
예제 #3
0
class _ThingSpotifyImpl(_ThingSpotifyDummy):
    def __init__(self, api_base_url, tok):
        super().__init__(api_base_url)
        self._sp = Spotify(auth=tok)
        self.unmuted_vol_pct = 0
        self.volume_up_pct_delta = 10

        self.status_cache_seconds = 10
        self.last_status = None
        self.last_status_t = 0

    def playpause(self):
        if self._is_active():
            self._sp.pause_playback()
        else:
            self._sp.start_playback()

    def stop(self):
        self._sp.pause_playback()

    def play_next_in_queue(self):
        self._sp.next_track()

    def play_prev_in_queue(self):
        # First 'prev' just moves playtime back to 0
        self._sp.previous_track()
        self._sp.previous_track()

    def set_playtime(self, t):
        if not self._is_active():
            return

        self._sp.seek_track(int(t) * 1000)

    def volume_up(self):
        if not self._is_active():
            return

        vol = self._get_volume_pct() + self.volume_up_pct_delta
        if vol > 100:
            vol = 100
        self.set_volume_pct(vol)

    def volume_down(self):
        if not self._is_active():
            return

        vol = self._get_volume_pct() - self.volume_up_pct_delta
        if vol < 0:
            vol = 0
        self.set_volume_pct(vol)

    def set_volume_pct(self, pct):
        if not self._is_active():
            return
        self._sp.volume(int(pct))

    def toggle_mute(self):
        if not self._is_active():
            return
        vol = self._get_volume_pct()
        if vol == 0:
            self.set_volume_pct(self.unmuted_vol_pct)
        else:
            self.unmuted_vol_pct = vol
            self.set_volume_pct(0)

    def play_in_device(self, dev_name):
        devs = self._sp.devices()['devices']
        for dev in devs:
            if dev['name'] == dev_name:
                self._sp.transfer_playback(dev['id'])
                if self.json_status(nocache=True)['player_state'] != 'Playing':
                    self.playpause()
                return

        raise KeyError("Spotify knows no device called {}".format(dev_name))

    def _get_volume_pct(self):
        l = [
            x for x in self._sp.devices()['devices'] if x['is_active'] == True
        ]
        if len(l) == 0:
            return 0
        return l[0]['volume_percent']

    def _is_active(self):
        track = self._sp.current_user_playing_track()
        return (track is not None) and track['is_playing']

    def json_status(self, nocache=False):
        global LAST_ACTIVE_DEVICE
        if nocache == False and self.last_status is not None:
            if time.time() - self.last_status_t < self.status_cache_seconds:
                logger.debug("Return Spotify status from cache")
                return self.last_status

        self.last_status_t = time.time()

        devices = self._sp.devices()['devices']

        active_dev = None
        if len(devices) > 0:
            act_devs = [x for x in devices if x['is_active'] == True]
            if len(act_devs) > 0:
                active_dev = act_devs[0]

        if active_dev is not None:
            LAST_ACTIVE_DEVICE = active_dev['name']

        vol = active_dev['volume_percent'] if active_dev is not None else 0

        track = self._sp.current_user_playing_track()
        is_active = (track is not None) and track['is_playing']

        self.last_status = {
            'name': self.get_id(),
            'uri': None,
            'active_device':
            active_dev['name'] if active_dev is not None else None,
            'last_active_device': LAST_ACTIVE_DEVICE,
            'available_devices': [x['name'] for x in devices],
            'app': None,
            'volume_pct': vol,
            'volume_muted': (vol == 0),
            'player_state': 'Playing' if is_active else 'Idle',
            'media': None,
        }

        if track is None or track['item'] is None:
            return self.last_status

        # Get all cover images sorted by image size
        imgs = []
        try:
            imgs = [(img['height'] * img['width'], img['url'])
                    for img in track['item']['album']['images']]
            imgs.sort()
        except:
            pass

        # Pick an image that's at least 300*300 (or the biggest, if all are smaller)
        selected_img = None
        for img in imgs:
            area, selected_img = img
            if area >= 90000:
                break

        self.last_status['media'] = {
            'icon': selected_img,
            'title': track['item']['name'],
            'duration': track['item']['duration_ms'] / 1000,
            'current_time': track['progress_ms'] / 1000,
            'spotify_metadata': {
                'artist':
                ', '.join(
                    [x['name'] for x in track['item']['album']['artists']]),
                'album_link':
                track['item']['album']['external_urls']['spotify'],
                'album_name':
                track['item']['album']['name'],
                'track_count':
                track['item']['album']['total_tracks'],
                'current_track':
                track['item']['track_number'],
            }
        }

        return self.last_status