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'})
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
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