def available_devices(self): """ Get all devices currently available. Returns: List[str]: All available device ids. Calls endpoints: - GET /v1/me/player/devices Required token scopes: - user-read-playback-state """ response_json, status_code = utils.request( self._session, request_type=const.REQUEST_GET, endpoint=Endpoints.PLAYER_AVAILABLE_DEVICES, body=None, uri_params=None) if status_code != 200: raise utils.SpotifyError(status_code, response_json) try: devices = response_json['devices'] result = [elem['id'] for elem in devices] except KeyError: raise utils.SpotifyError(KEYSTRING) return result
def _player_data(self, key, market=const.TOKEN_REGION, should_raise_error=True): """ Helper function for the getter methods. Wraps calling the player endpoint and handling a missing key. Args: key: the key to get from the currently playing context market: see the :class:`shared args documentation <Player>` should_raise_error: if False: returns None when no device available if True: raises SpotifyError when no device available Returns: None if there is no active device and should_raise_error is False The result of player_data[key] otherwise Raises: SpotifyError: if Spotify returns an error or the key isn't found NetworkError: for misc network failures Calls endpoints: - GET /v1/me/player Required token scopes: - user-read-playback-state """ response_json, status_code = utils.request( self._session, request_type=const.REQUEST_GET, endpoint=Endpoints.PLAYER_DATA, body=None, uri_params={'market': market}) # No active device # TODO: update when bug report is resolved if status_code == 204: if not should_raise_error: return None raise utils.SpotifyError('No active device', status_code, response_json) # Valid Player errors if status_code in [403, 404]: raise utils.SpotifyError(status_code, response_json) # Misc other failure if status_code != 200: raise utils.NetworkError(status_code, response_json) if key not in response_json: raise utils.SpotifyError(KEYSTRING + ': key <%s> not found' % key) return response_json[key]
def _follow_unfollow_help(self, other, request_type): """ follow and unfollow are identical, except for the request type. This function implements that functionality to remove duplicate code. """ # Validate input if not isinstance(other, list): other = [other] for elem in other: if type(elem) not in [Artist, User, Playlist]: raise TypeError(elem) # Split up input artists = utils.separate(other, Artist) users = utils.separate(other, User) playlists = utils.separate(other, Playlist) for batch in utils.create_batches(utils.map_ids(artists)): response_json, status_code = utils.request( self._session, request_type=request_type, endpoint=Endpoints.USER_FOLLOW_ARTIST_USER, body=None, uri_params={ 'type': 'artist', 'ids': batch }) if status_code != 204: raise utils.SpotifyError(status_code, response_json) for batch in utils.create_batches(utils.map_ids(users)): response_json, status_code = utils.request( self._session, request_type=request_type, endpoint=Endpoints.USER_FOLLOW_ARTIST_USER, body=None, uri_params={ 'type': 'user', 'ids': batch }) if status_code != 204: raise utils.SpotifyError(status_code, response_json) for playlist in playlists: response_json, status_code = utils.request( self._session, request_type=request_type, endpoint=Endpoints.USER_FOLLOW_PLAYLIST % playlist, body=None, uri_params=None) if status_code != 200: raise utils.SpotifyError(status_code, response_json)
def related_artists(self, search_limit=20): """ Get artists similar to this artist, as defined by Spotify. Args: search_limit (int): the maximum number of results to return. Must be between 1 and 20, inclusive (this is Spotify's limit). Returns: List[Artist]: The artists related to this artist. Calls endpoints: - GET /v1/artists/{id}/related-artists """ # This search limit is not part of the API, Spotify always returns up to # 20. # TODO: limit can't be None... # Type validation if search_limit is not None and not isinstance(search_limit, int): raise TypeError('search_limit should be None or int') # Argument validation if search_limit < 0 or search_limit > 20: raise ValueError('search_limit should be >= 0 and <= 20') # Save params for lazy loading check search_query = (search_limit) # Construct params for API call endpoint = Endpoints.ARTIST_RELATED_ARTISTS % self.spotify_id() # Lazy loading check if search_query == self._related_artists_query_params: return self._related_artists # Update stored params for lazy loading response_json, status_code = utils.request(session=self._session, request_type=\ const.REQUEST_GET, endpoint=endpoint ) if status_code != 200: raise utils.SpotifyError(status_code, response_json) if 'artists' not in response_json: raise utils.SpotifyError('Malformed response, missing key artists') result = [ Artist(self._session, x) for x in response_json.get('artists') ] self._related_artists = result self._related_artists_query_params = search_query return self._related_artists
def replace_all_tracks(self, tracks): """ Replace all of the tracks in the playlist. Args: tracks (List[Track]): The tracks to populate the playlist. Warning: All previously present tracks in the playlist will be removed. Required token scopes: - playlist-modify-public: If the playlist is public. - playlist-modify-private: If the playlist is private or collaborative. Calls endpoints: - PUT /v1/playlists/{playlist_id}/tracks """ endpoint = Endpoints.PLAYLIST_TRACKS % self.spotify_id() if not all([isinstance(track, Track) for track in tracks]): raise TypeError('All elements of tracks must be Track objects') body = {} body['uris'] = [track.uri() for track in tracks] response_json, status_code = utils.request( self._session, request_type='PUT', endpoint=endpoint, body=body ) if status_code != 201: raise utils.SpotifyError(status_code, response_json)
def get_repeat(self): """ Get the repeat state for the current playback. Uses the currently active device, if one exists. Returns: One of - sp.TRACKS - sp.CONTEXT - sp.OFF Calls endpoints: - GET /v1/me/player Required token scopes: - user-read-playback-state """ result = self._player_data('repeat_state') states = { const.TRACKS: 'track', const.CONTEXT: 'context', const.OFF: 'off' } if result not in states: raise utils.SpotifyError('Repeat state <%s> not defined' % result) return states[result]
def update_description(self, description): """ Updates the playlist description as it appears on Spotify. Args: description (str): the new new description of this playlist. Required token scopes: - playlist-modify-public: If the playlist is public. - playlist-modify-private: If the playlist is private or collaborative. Calls endpoints: - PUT /v1/playlists/{playlist_id} """ endpoint = Endpoints.PLAYLIST % self.spotify_id() if not isinstance(description, str): raise TypeError('The description must be a string') body = {} body['description'] = description response_json, status_code = utils.request( self._session, request_type='PUT', endpoint=endpoint, body=body ) if status_code != 200: raise utils.SpotifyError(status_code, response_json)
def set_playback_position(self, position, device_id=None): """ Set the current position in the currently playing track in ms. Args: position (int): the position (in ms). Must be non-negative. If greater than the len of the track, will play the next song. Returns: None Calls endpoints: - PUT /v1/me/player/seek Required token scopes: - user-modify-playback-state """ if position < 0: raise ValueError(position) uri_params = {'position_ms': position} if device_id is not None: uri_params['device_id'] = device_id response_json, status_code = utils.request( self._session, request_type=const.REQUEST_PUT, endpoint=Endpoints.PLAYER_SEEK, body=None, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def set_shuffle(self, shuffle_state, device_id=None): """ Set the shuffle state of the active device. Args: shuffle_state (bool): True to set shuffle to on, False to set shuffle to off Returns: None Calls endpoints: - PUT /v1/me/player/shuffle Required token scopes: - user-modify-playback-state """ uri_params = {'state': shuffle_state} if device_id is not None: uri_params['device_id'] = device_id response_json, status_code = utils.request( self._session, request_type=const.REQUEST_PUT, endpoint=Endpoints.PLAYER_SHUFFLE, body=None, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def set_active_device(self, device_id, force_play=const.KEEP_PLAY_STATE): """ Transfer playback to a different available device. Args: force_play: one of: - sp.FORCE_PLAY: resume playback after transfering to new device - sp.KEEP_PLAY_STATE: keep the current playback state. Returns: None Calls endpoints: - PUT /v1/me/player Required token scopes: - user-modify-playback-state """ if force_play not in [const.FORCE_PLAY, const.KEEP_PLAY_STATE]: raise ValueError(force_play) body = { 'device_ids': [device_id], 'play': force_play == const.FORCE_PLAY } response_json, status_code = utils.request( self._session, request_type=const.REQUEST_PUT, endpoint=Endpoints.PLAYER_TRANSFER, body=body, uri_params=None) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def _update_fields(self): """ Update self._raw using the User id. Calls endpoints: GET /v1/users/{id} GET /v1/me Note: This method only includes private fields if those are allowed by the token scopes and the token was created by this user. Required token scopes: user-read-email: to read the email user-read-private: to read the country and subscription """ other = self._session.current_user() if self == other: # We know other is a 'User' object, so this protected access is okay #pylint: disable=protected-access self._raw.update(other._raw) # other._raw has superset of data gotten below, skip extra api call return response_json, status_code = utils.request( session=self._session, request_type=const.REQUEST_GET, endpoint=Endpoints.USER_DATA % self.spotify_id()) if status_code != 200: raise utils.SpotifyError(status_code, response_json) self._raw.update(response_json)
def _update_fields(self): """ If field is not present, update it using the object's artist id. Raises: ValueError if artist id not present in the raw object data. Calls endpoints: - GET /v1/artists/{id} """ endpoint = Endpoints.ARTIST_DATA % self.spotify_id() response_json, status_code = utils.request( session=self._session, request_type=const.REQUEST_GET, endpoint=endpoint, ) if status_code != 200: raise utils.SpotifyError(status_code, response_json) # Updates _raw with new values. One liner : for each key in union of # keys in self._raw and response_json, takes value for key from # response_json if present, else takes value for key from self._raw. # TODO: this is weird notation, make a utility function for it. # Especially useful since it is an action necessary for many classes. self._raw = {**self._raw, **response_json}
def set_volume(self, volume, device_id=None): """ Set the current volume for the playback. Args: volume (int): volume (in percent) from 0 to 100 inclusive Returns: None Calls endpoints: - PUT /v1/me/player/volume Required token scopes: - user-modify-playback-state """ if volume < 0 or volume > 100: raise ValueError(volume) uri_params = {'volume_percent': volume} if device_id is not None: uri_params['device_id'] = device_id response_json, status_code = utils.request( self._session, request_type=const.REQUEST_PUT, endpoint=Endpoints.PLAYER_VOLUME, body=None, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def audio_analysis(self): #pylint: disable=line-too-long """ Get the audio analysis for this track. The Audio Analysis describes the track’s structure and musical content, including rhythm, pitch, and timbre. All information is precise to the audio sample. For more information on track audio analysis, see Spotify's `documentation <https://developer.spotify.com/documentation/web-api/reference/tracks/get-audio-analysis/>`__ Returns: dict: a dictionary containing the audio analysis as defined at the above link. Calls endpoints: - GET /v1/audio-analysis/{id} """ response_json, status_code = utils.request( session=self._session, request_type=const.REQUEST_GET, endpoint=Endpoints.TRACK_ANALYSIS % self.spotify_id() ) if status_code != 200: raise utils.SpotifyError(status_code, response_json) return response_json
def previous(self, device_id=None): """ Skip to the previous song in the playback. Note: Will skip to the previous song in the playback regardless of where in the current song playback is. Returns: None Calls endpoints: - POST /v1/me/player/previous Required token scopes: - user-modify-playback-state """ uri_params = None if device_id is None else {'device_id': device_id} response_json, status_code = utils.request( self._session, request_type=const.REQUEST_POST, endpoint=Endpoints.PLAYER_PREVIOUS, body=None, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def current_user(self): """ Returns: User: The user associated with the current Spotify API token. Raises: ValueError: if the Spotify API key is not valid. ValueError: if the response is empty. HTTPError: if failure or partial failure. Calls endpoints: - GET /v1/me """ # Construct params for API call endpoint = Endpoints.SEARCH_CURRENT_USER # Execute requests response_json, status_code = utils.request( session=self, request_type=const.REQUEST_GET, endpoint=endpoint) if status_code != 200: raise utils.SpotifyError(status_code, response_json) return User(self, response_json)
def resume(self, device_id=None): """ Resume the current playback. Returns: None Calls endpoints: - PUT /v1/me/player/play Required token scopes: - user-modify-playback-state Raises: SpotifyError: if playback is already playing """ uri_params = None if device_id is None else {'device_id': device_id} response_json, status_code = utils.request( self._session, request_type=const.REQUEST_PUT, endpoint=Endpoints.PLAYER_PLAY, body=None, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def update_visibility(self, visibility): """ Updates whether the playlist is public/private/collaborative. Args: visibility: One of: - sp.PUBLIC - sp.PRIVATE - sp.PRIVATE_COLLAB Required token scopes: - playlist-modify-public: If the playlist is public. - playlist-modify-private: If the playlist is private or collaborative. Calls endpoints: - PUT /v1/playlists/{playlist_id} """ endpoint = Endpoints.PLAYLIST % self.spotify_id() body = {} if visibility not in [const.PUBLIC, const.PRIVATE, const.PRIVATE_COLLAB]: raise ValueError('Invalid visibility, must be one of [sp.PUBLIC,' + 'sp.PRIVATE, sp.PRIVATE_COLLAB]') body['public'] = (visibility == const.PUBLIC) body['collaborative'] = (visibility == const.PRIVATE_COLLAB) response_json, status_code = utils.request( self._session, request_type='PUT', endpoint=endpoint, body=body ) if status_code != 200: raise utils.SpotifyError(status_code, response_json)
def play(self, item, offset=0, device_id=None): """ Change the current track and context for the player. Args: item: an instance of: - sp.Track - sp.Album - sp.Playlist - or sp.Artist. offset (int): the position in item to start playback. Ignored if item is not an Album or Playlist. 0 <= offset < len(item). Note: Playback will start at the beginning of the track. Use in combination with Player.set_playback_position to start elsewhere in the track. Returns: None Calls endpoints: - PUT /v1/me/player/play Required token scopes: - user-modify-playback-state """ # Validate inputs if type(item) not in [Track, Album, Playlist, Artist]: raise TypeError(item) if type(item) in [Album, Playlist]: if offset < 0 or offset >= len(item): raise ValueError(offset) # Build up the request uri_params = None if device_id is None else {'device_id': device_id} if isinstance(item, Track): body = {'uris': [item.uri()]} else: body = {'context_uri': item.uri()} if type(item) in [Album, Playlist]: body['offset'] = {'position': offset} response_json, status_code = utils.request( self._session, request_type=const.REQUEST_PUT, endpoint=Endpoints.PLAYER_PLAY, body=body, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def get_artists(self, artist_ids): """ Gets the artists with the given Spotify ids. Args: artist_ids (str, List[str): The Spotify artist id(s) to get. Returns: Union[Album, List[Album]]: The requested artist(s). Raises: TypeError: for invalid types in any argument. HTTPError: if failure or partial failure. Calls endpoints: - GET /v1/artists Note: the following endpoint is not used. - GET /v1/artists/{id} """ # Type validation if not isinstance(artist_ids, str) and\ not all(isinstance(x, str) for x in artist_ids): raise TypeError('artist_ids should be str or list of str') if isinstance(artist_ids, str): artist_ids = list(artist_ids) # Construct params for API call endpoint = Endpoints.SEARCH_ALBUMS uri_params = dict() # A maximum of 50 artists can be returned per API call batches = utils.create_batches(artist_ids, 50) result = list() for batch in batches: uri_params['ids'] = batch # Execute requests response_json, status_code = utils.request( session=self, request_type=const.REQUEST_GET, endpoint=endpoint, uri_params=uri_params) if status_code != 200: raise utils.SpotifyError(status_code, response_json) items = response_json['artists'] for item in items: result.append(Artist(self, item)) return result if len(result) != 1 else result[0]
def _save_remove_help(self, other, request_type): """ save and remove are identical, except for the request type and return codes. This function implements that functionality to remove duplicate code. """ # Validate input if not isinstance(other, list): other = [other] for elem in other: if type(elem) not in [Album, Track]: raise TypeError(elem) # Split up input albums = utils.separate(other, Album) tracks = utils.separate(other, Track) for batch in utils.create_batches(utils.map_ids(albums)): response_json, status_code = utils.request( self._session, request_type=request_type, endpoint=Endpoints.USER_SAVE_ALBUMS, body=None, uri_params={'ids': batch}) # All success codes are 200, except saving an album success = 201 if request_type == const.REQUEST_PUT else 200 if status_code != success: raise utils.SpotifyError(status_code, response_json) for batch in utils.create_batches(utils.map_ids(tracks)): response_json, status_code = utils.request( self._session, request_type=request_type, endpoint=Endpoints.USER_SAVE_TRACKS, body=None, uri_params={'ids': batch}) if status_code != 200: raise utils.SpotifyError(status_code, response_json)
def add_tracks(self, tracks, position=None): """ Adds one or more tracks to the playlist. Args: tracks: A Track object or list of Track objects to be added. position: An integer specifying the 0-indexed position in the playlist to insert tracks. A negative integer will be evaluated from the end of the playlist as negative indices behave in lists. This must be a valid index into a list of length len(playlist). Position can be omitted to append to the playlist instead. Required token scopes: - playlist-modify-public: If the playlist is public. - playlist-modify-private: If the playlist is private or collaborative. Calls endpoints: - POST /v1/playlists/{playlist_id}/tracks """ endpoint = Endpoints.PLAYLIST_TRACKS % self.spotify_id() body = {} uris = [] if isinstance(tracks, list): for track in tracks: if not isinstance(track, Track): raise TypeError('The elements of tracks must be Track ' + 'objects') uris.append(track.uri()) else: if not isinstance(tracks, Track): raise TypeError('The type of tracks must either be a Track ' + 'object or a list of Track objects') uris.append(tracks.uri()) body['uris'] = uris if position: if not isinstance(position, int): raise TypeError('The position must be an integer') original_position = position if position < 0: position += len(self) if position < 0 or position >= len(self): raise ValueError(f'Invalid position: {original_position}') body['position'] = position response_json, status_code = utils.request( self._session, request_type='POST', endpoint=endpoint, body=body ) if status_code != 201: raise utils.SpotifyError(status_code, response_json)
def create_playlist(self, name, visibility=const.PUBLIC, description=None): """ Create a new playlist owned by the current user. Args: name (str): The name for the new playlist. Does not need to be unique; a user may have several playlists with the same name. visibility: How other users interact with this playlist. One of: - sp.PUBLIC: publicly viewable, not collaborative - sp.PRIVATE: not publicly viewable, not collaborative - sp.PRIVATE_COLLAB: not publicly viewable, collaborative description (str): The viewable description of the playlist. Returns: Playlist: The newly created Playlist object. Note that this function modifies the user's library. Required token scopes: - playlist-modify-public - playlist-modify-private Calls endpoints: - POST /v1/users/{user_id}/playlists """ # Validate inputs if visibility not in [ const.PUBLIC, const.PRIVATE, const.PRIVATE_COLLAB ]: raise TypeError(visibility) body = { 'name': name, 'public': visibility == const.PUBLIC, 'collaborative': visibility == const.PRIVATE_COLLAB } if description is not None: body['description'] = description response_json, status_code = utils.request( self._session, request_type=const.REQUEST_POST, endpoint=Endpoints.USER_CREATE_PLAYLIST % self.spotify_id(), body=body, uri_params=None) if status_code != 201: raise utils.SpotifyError(status_code, response_json) return Playlist(self._session, response_json)
def context(self, market=const.TOKEN_REGION): """ Get the currently playing context for the playback. Uses the currently active device, if one exists. Args: market: see the :class:`shared args documentation <Player>` Returns: An Album, Artist, or Playlist if there is a context for the playback, else None if there is no context Calls endpoints: - GET /v1/me/player Required token scopes: - user-read-playback-state """ context = self._player_data('context', market) if context is None: return None # Validate context if 'uri' not in context or 'type' not in context: raise utils.SpotifyError(KEYSTRING) # uri of form 'spotify:type:id' context_id = context['uri'].split(':')[-1] # Context can only be one of Artist, Album, or Playlist if context['type'] == 'album': return self._session.get_albums(context_id) if context['type'] == 'artist': return self._session.get_artists(context_id) if context['type'] == 'playlist': return self._session.get_playlists(context_id) raise utils.SpotifyError('Unrecognized context: %s' % str(context))
def image(self): """ Get the the playlist cover image. Returns: Image: an image object if the Playlist has a cover image. None: if the Playlist has no cover image. Calls endpoints: - PUT /v1/playlists/{playlist_id} """ endpoint = Endpoints.PLAYLIST_IMAGES % self.spotify_id() response_json, status_code = utils.request( self._session, request_type='GET', endpoint=endpoint ) if status_code != 200: raise utils.SpotifyError(status_code, response_json) if len(response_json) > 1: raise utils.SpotifyError('Playlist has more than one cover image!') return None if len(response_json) == 0 else Image(response_json[0])
def enqueue(self, item, device_id=None): """ Add an item to the end of the queue. Args: item: the item to add to the queue. One of: - Album - Track - Playlist Returns: None Note: If a playlist is added, the order of added songs may be inconsistent. Note: When adding an Album or Playlist, this method can fail partway through, resulting in only some Tracks being added to the queue. Calls endpoints: - POST /v1/me/player/queue Required token scopes: - user-modify-playback-state """ if type(item) not in [Album, Track, Playlist]: raise ValueError(item) # Make into an iterable if isinstance(item, Track): item = [item] uri_params = {} if device_id is None else {'device_id': device_id} # Can only enqueue one item at a time for track in item: uri_params['uri'] = track.uri() response_json, status_code = utils.request( self._session, request_type=const.REQUEST_POST, endpoint=Endpoints.PLAYER_QUEUE, body=None, uri_params=uri_params) if status_code != 204: raise utils.SpotifyError(status_code, response_json)
def image(self): """ Get the User's profile picture. Returns: Union[Image, None]: An image object if the User has a profile picture, else None. Calls endpoints: - GET /v1/users/{id} """ result = utils.get_field(self, 'images') if len(result) > 1: raise utils.SpotifyError('User has more than one profile picture!') return None if len(result) == 0 else Image(result[0])
def get_users(self, user_ids): """ Gets the users with the given Spotify ids. Args: user_ids (str, List[str]): The Spotify user id(s) to get. Returns: Union[User, List[User]]: The requested user(s). Raises: TypeError: for invalid types in any argument. HTTPError: if failure or partial failure. Calls endpoints: - GET /v1/users/{user_id} """ # Type validation if not isinstance(user_ids, str) and\ not all(isinstance(x, str) for x in user_ids): raise TypeError('user_ids should be str or list of str') if isinstance(user_ids, str): user_ids = list('user_ids should be str') # Construct params for API call uri_params = dict() # Each API call can return at most 1 user. Therefore there is no need # to batch this query. result = list() for user_id in user_ids: # Execute requests # TODO: Partial failure - if user with user_id does not exist, # status_code is 404 response_json, status_code = utils.request( session=self, request_type=const.REQUEST_GET, endpoint=Endpoints.SEARCH_USER % user_id, uri_params=uri_params) if status_code != 200: raise utils.SpotifyError(status_code, response_json) result.append(User(self, response_json)) return result if len(result) != 1 else result[0]
def get_volume(self): """ Get the current volume for the playback. Returns: int: The volume (in percent) from 0 to 100 inclusive. Calls endpoints: - GET /v1/me/player Required token scopes: - user-read-playback-state """ device = self._player_data('device') if 'volume_percent' not in device: raise utils.SpotifyError(KEYSTRING + ': key volume_percent not found') return device['volume_percent']
def recently_played(self, limit=50): """ Args: limit (int): Max number of items to return. Must be between 1 and 50, inclusive. Returns: List[Tracks]: The user's recently played tracks. Could be empty. Required token scopes: - user-read-recently-played Calls endpoints: - GET /v1/me/player/recently-played Note: - The 'before' and 'after' functionalities are not supported. - Does not return the time the tracks were played - A track must be played for >30s to be included in the history. Tracks played while in a 'private session' are not recorded. """ # Validate arguments if limit <= 0 or limit > 50: raise ValueError(limit) # Execute requests response_json, status_code = utils.request( self._session, request_type=const.REQUEST_GET, endpoint=Endpoints.USER_RECENTLY_PLAYED, body=None, uri_params={'limit': limit}) if status_code != 200: raise utils.SpotifyError(status_code, response_json) results = [] for elem in response_json['items']: results.append(Track(self._session, elem)) return results