├── screenFrame.stl ├── spotipy ├── __init__.py ├── util.py ├── oauth2.py └── client.py └── README.md /screenFrame.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margyle/nowPlaying/HEAD/screenFrame.stl -------------------------------------------------------------------------------- /spotipy/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION='2.0.1' 2 | from .client import Spotify, SpotifyException 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nowPlaying 2 | A Spotify "Now Playing" viewer for the Raspberry Pi. 3 | 4 | As shown on the reddit thread: https://www.reddit.com/r/raspberry_pi/comments/7p1pyl/made_a_spotify_now_playing_viewer_for_my_workbench/ 5 | 6 | Code upload coming in the next 24-48 hours. 7 | 8 | ![alt tag](https://i.imgur.com/mJH1YOr.jpg "Description goes here") 9 | -------------------------------------------------------------------------------- /spotipy/util.py: -------------------------------------------------------------------------------- 1 | 2 | # shows a user's playlists (need to be authenticated via oauth) 3 | 4 | from __future__ import print_function 5 | import os 6 | from . import oauth2 7 | import spotipy 8 | 9 | def prompt_for_user_token(username, scope=None, client_id = None, 10 | client_secret = None, redirect_uri = None, cache_path = None): 11 | ''' prompts the user to login if necessary and returns 12 | the user token suitable for use with the spotipy.Spotify 13 | constructor 14 | 15 | Parameters: 16 | 17 | - username - the Spotify username 18 | - scope - the desired scope of the request 19 | - client_id - the client id of your app 20 | - client_secret - the client secret of your app 21 | - redirect_uri - the redirect URI of your app 22 | - cache_path - path to location to save tokens 23 | 24 | ''' 25 | 26 | if not client_id: 27 | client_id = os.getenv('SPOTIPY_CLIENT_ID') 28 | 29 | if not client_secret: 30 | client_secret = os.getenv('SPOTIPY_CLIENT_SECRET') 31 | 32 | if not redirect_uri: 33 | redirect_uri = os.getenv('SPOTIPY_REDIRECT_URI') 34 | 35 | if not client_id: 36 | print(''' 37 | You need to set your Spotify API credentials. You can do this by 38 | setting environment variables like so: 39 | 40 | export SPOTIPY_CLIENT_ID='your-spotify-client-id' 41 | export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret' 42 | export SPOTIPY_REDIRECT_URI='your-app-redirect-url' 43 | 44 | Get your credentials at 45 | https://developer.spotify.com/my-applications 46 | ''') 47 | raise spotipy.SpotifyException(550, -1, 'no credentials set') 48 | 49 | cache_path = cache_path or ".cache-" + username 50 | sp_oauth = oauth2.SpotifyOAuth(client_id, client_secret, redirect_uri, 51 | scope=scope, cache_path=cache_path) 52 | 53 | # try to get a valid token for this user, from the cache, 54 | # if not in the cache, the create a new (this will send 55 | # the user to a web page where they can authorize this app) 56 | 57 | token_info = sp_oauth.get_cached_token() 58 | 59 | if not token_info: 60 | print(''' 61 | 62 | User authentication requires interaction with your 63 | web browser. Once you enter your credentials and 64 | give authorization, you will be redirected to 65 | a url. Paste that url you were directed to to 66 | complete the authorization. 67 | 68 | ''') 69 | auth_url = sp_oauth.get_authorize_url() 70 | try: 71 | import webbrowser 72 | webbrowser.open(auth_url) 73 | print("Opened %s in your browser" % auth_url) 74 | except: 75 | print("Please navigate here: %s" % auth_url) 76 | 77 | print() 78 | print() 79 | try: 80 | response = raw_input("Enter the URL you were redirected to: ") 81 | except NameError: 82 | response = input("Enter the URL you were redirected to: ") 83 | 84 | print() 85 | print() 86 | 87 | code = sp_oauth.parse_response_code(response) 88 | token_info = sp_oauth.get_access_token(code) 89 | # Auth'ed API request 90 | if token_info: 91 | return token_info['access_token'] 92 | else: 93 | return None 94 | -------------------------------------------------------------------------------- /spotipy/oauth2.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import print_function 3 | import base64 4 | import requests 5 | import os 6 | import json 7 | import time 8 | import sys 9 | 10 | # Workaround to support both python 2 & 3 11 | import six 12 | import six.moves.urllib.parse as urllibparse 13 | 14 | 15 | class SpotifyOauthError(Exception): 16 | pass 17 | 18 | 19 | def _make_authorization_headers(client_id, client_secret): 20 | auth_header = base64.b64encode(six.text_type(client_id + ':' + client_secret).encode('ascii')) 21 | return {'Authorization': 'Basic %s' % auth_header.decode('ascii')} 22 | 23 | 24 | def is_token_expired(token_info): 25 | now = int(time.time()) 26 | return token_info['expires_at'] - now < 60 27 | 28 | 29 | class SpotifyClientCredentials(object): 30 | OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token' 31 | 32 | def __init__(self, client_id=None, client_secret=None, proxies=None): 33 | """ 34 | You can either provid a client_id and client_secret to the 35 | constructor or set SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET 36 | environment variables 37 | """ 38 | if not client_id: 39 | client_id = os.getenv('SPOTIPY_CLIENT_ID') 40 | 41 | if not client_secret: 42 | client_secret = os.getenv('SPOTIPY_CLIENT_SECRET') 43 | 44 | if not client_id: 45 | raise SpotifyOauthError('No client id') 46 | 47 | if not client_secret: 48 | raise SpotifyOauthError('No client secret') 49 | 50 | self.client_id = client_id 51 | self.client_secret = client_secret 52 | self.token_info = None 53 | self.proxies = proxies 54 | 55 | def get_access_token(self): 56 | """ 57 | If a valid access token is in memory, returns it 58 | Else feches a new token and returns it 59 | """ 60 | if self.token_info and not self.is_token_expired(self.token_info): 61 | return self.token_info['access_token'] 62 | 63 | token_info = self._request_access_token() 64 | token_info = self._add_custom_values_to_token_info(token_info) 65 | self.token_info = token_info 66 | return self.token_info['access_token'] 67 | 68 | def _request_access_token(self): 69 | """Gets client credentials access token """ 70 | payload = { 'grant_type': 'client_credentials'} 71 | 72 | headers = _make_authorization_headers(self.client_id, self.client_secret) 73 | 74 | response = requests.post(self.OAUTH_TOKEN_URL, data=payload, 75 | headers=headers, verify=True, proxies=self.proxies) 76 | if response.status_code != 200: 77 | raise SpotifyOauthError(response.reason) 78 | token_info = response.json() 79 | return token_info 80 | 81 | def is_token_expired(self, token_info): 82 | return is_token_expired(token_info) 83 | 84 | def _add_custom_values_to_token_info(self, token_info): 85 | """ 86 | Store some values that aren't directly provided by a Web API 87 | response. 88 | """ 89 | token_info['expires_at'] = int(time.time()) + token_info['expires_in'] 90 | return token_info 91 | 92 | 93 | class SpotifyOAuth(object): 94 | ''' 95 | Implements Authorization Code Flow for Spotify's OAuth implementation. 96 | ''' 97 | 98 | OAUTH_AUTHORIZE_URL = 'https://accounts.spotify.com/authorize' 99 | OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token' 100 | 101 | def __init__(self, client_id, client_secret, redirect_uri, 102 | state=None, scope=None, cache_path=None, proxies=None): 103 | ''' 104 | Creates a SpotifyOAuth object 105 | 106 | Parameters: 107 | - client_id - the client id of your app 108 | - client_secret - the client secret of your app 109 | - redirect_uri - the redirect URI of your app 110 | - state - security state 111 | - scope - the desired scope of the request 112 | - cache_path - path to location to save tokens 113 | ''' 114 | 115 | self.client_id = client_id 116 | self.client_secret = client_secret 117 | self.redirect_uri = redirect_uri 118 | self.state=state 119 | self.cache_path = cache_path 120 | self.scope=self._normalize_scope(scope) 121 | self.proxies = proxies 122 | 123 | def get_cached_token(self): 124 | ''' Gets a cached auth token 125 | ''' 126 | token_info = None 127 | if self.cache_path: 128 | try: 129 | f = open(self.cache_path) 130 | token_info_string = f.read() 131 | f.close() 132 | token_info = json.loads(token_info_string) 133 | 134 | # if scopes don't match, then bail 135 | if 'scope' not in token_info or not self._is_scope_subset(self.scope, token_info['scope']): 136 | return None 137 | 138 | if self.is_token_expired(token_info): 139 | token_info = self.refresh_access_token(token_info['refresh_token']) 140 | 141 | except IOError: 142 | pass 143 | return token_info 144 | 145 | def _save_token_info(self, token_info): 146 | if self.cache_path: 147 | try: 148 | f = open(self.cache_path, 'w') 149 | f.write(json.dumps(token_info)) 150 | f.close() 151 | except IOError: 152 | self._warn("couldn't write token cache to " + self.cache_path) 153 | pass 154 | 155 | def _is_scope_subset(self, needle_scope, haystack_scope): 156 | needle_scope = set(needle_scope.split()) if needle_scope else set() 157 | haystack_scope = set(haystack_scope.split()) if haystack_scope else set() 158 | return needle_scope <= haystack_scope 159 | 160 | def is_token_expired(self, token_info): 161 | return is_token_expired(token_info) 162 | 163 | def get_authorize_url(self, state=None, show_dialog=False): 164 | """ Gets the URL to use to authorize this app 165 | """ 166 | payload = {'client_id': self.client_id, 167 | 'response_type': 'code', 168 | 'redirect_uri': self.redirect_uri} 169 | if self.scope: 170 | payload['scope'] = self.scope 171 | if state is None: 172 | state = self.state 173 | if state is not None: 174 | payload['state'] = state 175 | if show_dialog: 176 | payload['show_dialog'] = True 177 | 178 | urlparams = urllibparse.urlencode(payload) 179 | 180 | return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) 181 | 182 | def parse_response_code(self, url): 183 | """ Parse the response code in the given response url 184 | 185 | Parameters: 186 | - url - the response url 187 | """ 188 | 189 | try: 190 | return url.split("?code=")[1].split("&")[0] 191 | except IndexError: 192 | return None 193 | 194 | def _make_authorization_headers(self): 195 | return _make_authorization_headers(self.client_id, self.client_secret) 196 | 197 | def get_access_token(self, code): 198 | """ Gets the access token for the app given the code 199 | 200 | Parameters: 201 | - code - the response code 202 | """ 203 | 204 | payload = {'redirect_uri': self.redirect_uri, 205 | 'code': code, 206 | 'grant_type': 'authorization_code'} 207 | if self.scope: 208 | payload['scope'] = self.scope 209 | if self.state: 210 | payload['state'] = self.state 211 | 212 | headers = self._make_authorization_headers() 213 | 214 | response = requests.post(self.OAUTH_TOKEN_URL, data=payload, 215 | headers=headers, verify=True, proxies=self.proxies) 216 | if response.status_code != 200: 217 | raise SpotifyOauthError(response.reason) 218 | token_info = response.json() 219 | token_info = self._add_custom_values_to_token_info(token_info) 220 | self._save_token_info(token_info) 221 | return token_info 222 | 223 | def _normalize_scope(self, scope): 224 | if scope: 225 | scopes = scope.split() 226 | scopes.sort() 227 | return ' '.join(scopes) 228 | else: 229 | return None 230 | 231 | def refresh_access_token(self, refresh_token): 232 | payload = { 'refresh_token': refresh_token, 233 | 'grant_type': 'refresh_token'} 234 | 235 | headers = self._make_authorization_headers() 236 | 237 | response = requests.post(self.OAUTH_TOKEN_URL, data=payload, 238 | headers=headers, proxies=self.proxies) 239 | if response.status_code != 200: 240 | if False: # debugging code 241 | print('headers', headers) 242 | print('request', response.url) 243 | self._warn("couldn't refresh token: code:%d reason:%s" \ 244 | % (response.status_code, response.reason)) 245 | return None 246 | token_info = response.json() 247 | token_info = self._add_custom_values_to_token_info(token_info) 248 | if not 'refresh_token' in token_info: 249 | token_info['refresh_token'] = refresh_token 250 | self._save_token_info(token_info) 251 | return token_info 252 | 253 | def _add_custom_values_to_token_info(self, token_info): 254 | ''' 255 | Store some values that aren't directly provided by a Web API 256 | response. 257 | ''' 258 | token_info['expires_at'] = int(time.time()) + token_info['expires_in'] 259 | token_info['scope'] = self.scope 260 | return token_info 261 | 262 | def _warn(self, msg): 263 | print('warning:' + msg, file=sys.stderr) 264 | 265 | -------------------------------------------------------------------------------- /spotipy/client.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | from __future__ import print_function 5 | import sys 6 | import requests 7 | import json 8 | import time 9 | 10 | import six 11 | 12 | """ A simple and thin Python library for the Spotify Web API 13 | """ 14 | 15 | 16 | class SpotifyException(Exception): 17 | def __init__(self, http_status, code, msg, headers=None): 18 | self.http_status = http_status 19 | self.code = code 20 | self.msg = msg 21 | # `headers` is used to support `Retry-After` in the event of a 22 | # 429 status code. 23 | if headers is None: 24 | headers = {} 25 | self.headers = headers 26 | 27 | def __str__(self): 28 | return 'http status: {0}, code:{1} - {2}'.format( 29 | self.http_status, self.code, self.msg) 30 | 31 | 32 | class Spotify(object): 33 | """ 34 | Example usage:: 35 | 36 | import spotipy 37 | 38 | urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' 39 | sp = spotipy.Spotify() 40 | 41 | sp.trace = True # turn on tracing 42 | sp.trace_out = True # turn on trace out 43 | 44 | artist = sp.artist(urn) 45 | print(artist) 46 | 47 | user = sp.user('plamere') 48 | print(user) 49 | """ 50 | 51 | trace = False # Enable tracing? 52 | trace_out = False 53 | max_get_retries = 10 54 | 55 | def __init__(self, auth=None, requests_session=True, 56 | client_credentials_manager=None, proxies=None, requests_timeout=None): 57 | """ 58 | Create a Spotify API object. 59 | 60 | :param auth: An authorization token (optional) 61 | :param requests_session: 62 | A Requests session object or a truthy value to create one. 63 | A falsy value disables sessions. 64 | It should generally be a good idea to keep sessions enabled 65 | for performance reasons (connection pooling). 66 | :param client_credentials_manager: 67 | SpotifyClientCredentials object 68 | :param proxies: 69 | Definition of proxies (optional) 70 | :param requests_timeout: 71 | Tell Requests to stop waiting for a response after a given number of seconds 72 | """ 73 | self.prefix = 'https://api.spotify.com/v1/' 74 | self._auth = auth 75 | self.client_credentials_manager = client_credentials_manager 76 | self.proxies = proxies 77 | self.requests_timeout = requests_timeout 78 | 79 | if isinstance(requests_session, requests.Session): 80 | self._session = requests_session 81 | else: 82 | if requests_session: # Build a new session. 83 | self._session = requests.Session() 84 | else: # Use the Requests API module as a "session". 85 | from requests import api 86 | self._session = api 87 | 88 | def _auth_headers(self): 89 | if self._auth: 90 | return {'Authorization': 'Bearer {0}'.format(self._auth)} 91 | elif self.client_credentials_manager: 92 | token = self.client_credentials_manager.get_access_token() 93 | return {'Authorization': 'Bearer {0}'.format(token)} 94 | else: 95 | return {} 96 | 97 | def _internal_call(self, method, url, payload, params): 98 | args = dict(params=params) 99 | args["timeout"] = self.requests_timeout 100 | if not url.startswith('http'): 101 | url = self.prefix + url 102 | headers = self._auth_headers() 103 | headers['Content-Type'] = 'application/json' 104 | 105 | if payload: 106 | args["data"] = json.dumps(payload) 107 | 108 | if self.trace_out: 109 | print(url) 110 | r = self._session.request(method, url, headers=headers, proxies=self.proxies, **args) 111 | 112 | if self.trace: # pragma: no cover 113 | print() 114 | print ('headers', headers) 115 | print ('http status', r.status_code) 116 | print(method, r.url) 117 | if payload: 118 | print("DATA", json.dumps(payload)) 119 | 120 | try: 121 | r.raise_for_status() 122 | except: 123 | if r.text and len(r.text) > 0 and r.text != 'null': 124 | raise SpotifyException(r.status_code, 125 | -1, '%s:\n %s' % (r.url, r.json()['error']['message']), 126 | headers=r.headers) 127 | else: 128 | raise SpotifyException(r.status_code, 129 | -1, '%s:\n %s' % (r.url, 'error'), headers=r.headers) 130 | finally: 131 | r.connection.close() 132 | if r.text and len(r.text) > 0 and r.text != 'null': 133 | results = r.json() 134 | if self.trace: # pragma: no cover 135 | print('RESP', results) 136 | print() 137 | return results 138 | else: 139 | return None 140 | 141 | def _get(self, url, args=None, payload=None, **kwargs): 142 | if args: 143 | kwargs.update(args) 144 | retries = self.max_get_retries 145 | delay = 1 146 | while retries > 0: 147 | try: 148 | return self._internal_call('GET', url, payload, kwargs) 149 | except SpotifyException as e: 150 | retries -= 1 151 | status = e.http_status 152 | # 429 means we hit a rate limit, backoff 153 | if status == 429 or (status >= 500 and status < 600): 154 | if retries < 0: 155 | raise 156 | else: 157 | sleep_seconds = int(e.headers.get('Retry-After', delay)) 158 | print ('retrying ...' + str(sleep_seconds) + 'secs') 159 | time.sleep(sleep_seconds + 1) 160 | delay += 1 161 | else: 162 | raise 163 | except Exception as e: 164 | raise 165 | print ('exception', str(e)) 166 | # some other exception. Requests have 167 | # been know to throw a BadStatusLine exception 168 | retries -= 1 169 | if retries >= 0: 170 | sleep_seconds = int(e.headers.get('Retry-After', delay)) 171 | print ('retrying ...' + str(delay) + 'secs') 172 | time.sleep(sleep_seconds + 1) 173 | delay += 1 174 | else: 175 | raise 176 | 177 | def _post(self, url, args=None, payload=None, **kwargs): 178 | if args: 179 | kwargs.update(args) 180 | return self._internal_call('POST', url, payload, kwargs) 181 | 182 | def _delete(self, url, args=None, payload=None, **kwargs): 183 | if args: 184 | kwargs.update(args) 185 | return self._internal_call('DELETE', url, payload, kwargs) 186 | 187 | def _put(self, url, args=None, payload=None, **kwargs): 188 | if args: 189 | kwargs.update(args) 190 | return self._internal_call('PUT', url, payload, kwargs) 191 | 192 | def next(self, result): 193 | """ returns the next result given a paged result 194 | 195 | Parameters: 196 | - result - a previously returned paged result 197 | """ 198 | if result['next']: 199 | return self._get(result['next']) 200 | else: 201 | return None 202 | 203 | def previous(self, result): 204 | """ returns the previous result given a paged result 205 | 206 | Parameters: 207 | - result - a previously returned paged result 208 | """ 209 | if result['previous']: 210 | return self._get(result['previous']) 211 | else: 212 | return None 213 | 214 | def _warn_old(self, msg): 215 | print('warning:' + msg, file=sys.stderr) 216 | 217 | def _warn(self, msg, *args): 218 | print('warning:' + msg.format(*args), file=sys.stderr) 219 | 220 | def track(self, track_id): 221 | """ returns a single track given the track's ID, URI or URL 222 | 223 | Parameters: 224 | - track_id - a spotify URI, URL or ID 225 | """ 226 | 227 | trid = self._get_id('track', track_id) 228 | return self._get('tracks/' + trid) 229 | 230 | def tracks(self, tracks, market = None): 231 | """ returns a list of tracks given a list of track IDs, URIs, or URLs 232 | 233 | Parameters: 234 | - tracks - a list of spotify URIs, URLs or IDs 235 | - market - an ISO 3166-1 alpha-2 country code. 236 | """ 237 | 238 | tlist = [self._get_id('track', t) for t in tracks] 239 | return self._get('tracks/?ids=' + ','.join(tlist), market = market) 240 | 241 | def artist(self, artist_id): 242 | """ returns a single artist given the artist's ID, URI or URL 243 | 244 | Parameters: 245 | - artist_id - an artist ID, URI or URL 246 | """ 247 | 248 | trid = self._get_id('artist', artist_id) 249 | return self._get('artists/' + trid) 250 | 251 | def artists(self, artists): 252 | """ returns a list of artists given the artist IDs, URIs, or URLs 253 | 254 | Parameters: 255 | - artists - a list of artist IDs, URIs or URLs 256 | """ 257 | 258 | tlist = [self._get_id('artist', a) for a in artists] 259 | return self._get('artists/?ids=' + ','.join(tlist)) 260 | 261 | def artist_albums(self, artist_id, album_type=None, country=None, limit=20, 262 | offset=0): 263 | """ Get Spotify catalog information about an artist's albums 264 | 265 | Parameters: 266 | - artist_id - the artist ID, URI or URL 267 | - album_type - 'album', 'single', 'appears_on', 'compilation' 268 | - country - limit the response to one particular country. 269 | - limit - the number of albums to return 270 | - offset - the index of the first album to return 271 | """ 272 | 273 | trid = self._get_id('artist', artist_id) 274 | return self._get('artists/' + trid + '/albums', album_type=album_type, 275 | country=country, limit=limit, offset=offset) 276 | 277 | def artist_top_tracks(self, artist_id, country='US'): 278 | """ Get Spotify catalog information about an artist's top 10 tracks 279 | by country. 280 | 281 | Parameters: 282 | - artist_id - the artist ID, URI or URL 283 | - country - limit the response to one particular country. 284 | """ 285 | 286 | trid = self._get_id('artist', artist_id) 287 | return self._get('artists/' + trid + '/top-tracks', country=country) 288 | 289 | def artist_related_artists(self, artist_id): 290 | """ Get Spotify catalog information about artists similar to an 291 | identified artist. Similarity is based on analysis of the 292 | Spotify community's listening history. 293 | 294 | Parameters: 295 | - artist_id - the artist ID, URI or URL 296 | """ 297 | trid = self._get_id('artist', artist_id) 298 | return self._get('artists/' + trid + '/related-artists') 299 | 300 | def album(self, album_id): 301 | """ returns a single album given the album's ID, URIs or URL 302 | 303 | Parameters: 304 | - album_id - the album ID, URI or URL 305 | """ 306 | 307 | trid = self._get_id('album', album_id) 308 | return self._get('albums/' + trid) 309 | 310 | def album_tracks(self, album_id, limit=50, offset=0): 311 | """ Get Spotify catalog information about an album's tracks 312 | 313 | Parameters: 314 | - album_id - the album ID, URI or URL 315 | - limit - the number of items to return 316 | - offset - the index of the first item to return 317 | """ 318 | 319 | trid = self._get_id('album', album_id) 320 | return self._get('albums/' + trid + '/tracks/', limit=limit, 321 | offset=offset) 322 | 323 | def albums(self, albums): 324 | """ returns a list of albums given the album IDs, URIs, or URLs 325 | 326 | Parameters: 327 | - albums - a list of album IDs, URIs or URLs 328 | """ 329 | 330 | tlist = [self._get_id('album', a) for a in albums] 331 | return self._get('albums/?ids=' + ','.join(tlist)) 332 | 333 | def search(self, q, limit=10, offset=0, type='track', market=None): 334 | """ searches for an item 335 | 336 | Parameters: 337 | - q - the search query 338 | - limit - the number of items to return 339 | - offset - the index of the first item to return 340 | - type - the type of item to return. One of 'artist', 'album', 341 | 'track' or 'playlist' 342 | - market - An ISO 3166-1 alpha-2 country code or the string from_token. 343 | """ 344 | return self._get('search', q=q, limit=limit, offset=offset, type=type, market=market) 345 | 346 | def user(self, user): 347 | """ Gets basic profile information about a Spotify User 348 | 349 | Parameters: 350 | - user - the id of the usr 351 | """ 352 | return self._get('users/' + user) 353 | 354 | def current_user_playlists(self, limit=50, offset=0): 355 | """ Get current user playlists without required getting his profile 356 | Parameters: 357 | - limit - the number of items to return 358 | - offset - the index of the first item to return 359 | """ 360 | return self._get("me/playlists", limit=limit, offset=offset) 361 | 362 | def user_playlists(self, user, limit=50, offset=0): 363 | """ Gets playlists of a user 364 | 365 | Parameters: 366 | - user - the id of the usr 367 | - limit - the number of items to return 368 | - offset - the index of the first item to return 369 | """ 370 | return self._get("users/%s/playlists" % user, limit=limit, 371 | offset=offset) 372 | 373 | def user_playlist(self, user, playlist_id=None, fields=None): 374 | """ Gets playlist of a user 375 | Parameters: 376 | - user - the id of the user 377 | - playlist_id - the id of the playlist 378 | - fields - which fields to return 379 | """ 380 | if playlist_id is None: 381 | return self._get("users/%s/starred" % (user), fields=fields) 382 | plid = self._get_id('playlist', playlist_id) 383 | return self._get("users/%s/playlists/%s" % (user, plid), fields=fields) 384 | 385 | def user_playlist_tracks(self, user, playlist_id=None, fields=None, 386 | limit=100, offset=0, market=None): 387 | """ Get full details of the tracks of a playlist owned by a user. 388 | 389 | Parameters: 390 | - user - the id of the user 391 | - playlist_id - the id of the playlist 392 | - fields - which fields to return 393 | - limit - the maximum number of tracks to return 394 | - offset - the index of the first track to return 395 | - market - an ISO 3166-1 alpha-2 country code. 396 | """ 397 | plid = self._get_id('playlist', playlist_id) 398 | return self._get("users/%s/playlists/%s/tracks" % (user, plid), 399 | limit=limit, offset=offset, fields=fields, 400 | market=market) 401 | 402 | 403 | def user_playlist_create(self, user, name, public=True, description=''): 404 | """ Creates a playlist for a user 405 | 406 | Parameters: 407 | - user - the id of the user 408 | - name - the name of the playlist 409 | - public - is the created playlist public 410 | - description - the description of the playlist 411 | """ 412 | data = {'name': name, 'public': public, 'description': description} 413 | 414 | 415 | return self._post("users/%s/playlists" % (user,), payload=data) 416 | 417 | def user_playlist_change_details( 418 | self, user, playlist_id, name=None, public=None, 419 | collaborative=None, description=None): 420 | """ Changes a playlist's name and/or public/private state 421 | 422 | Parameters: 423 | - user - the id of the user 424 | - playlist_id - the id of the playlist 425 | - name - optional name of the playlist 426 | - public - optional is the playlist public 427 | - collaborative - optional is the playlist collaborative 428 | - description - optional description of the playlist 429 | """ 430 | 431 | data = {} 432 | if isinstance(name, six.string_types): 433 | data['name'] = name 434 | if isinstance(public, bool): 435 | data['public'] = public 436 | if isinstance(collaborative, bool): 437 | data['collaborative'] = collaborative 438 | if isinstance(description, six.string_types): 439 | data['description'] = description 440 | return self._put("users/%s/playlists/%s" % (user, playlist_id), 441 | payload=data) 442 | 443 | def user_playlist_unfollow(self, user, playlist_id): 444 | """ Unfollows (deletes) a playlist for a user 445 | 446 | Parameters: 447 | - user - the id of the user 448 | - name - the name of the playlist 449 | """ 450 | return self._delete("users/%s/playlists/%s/followers" % (user, playlist_id)) 451 | 452 | def user_playlist_add_tracks(self, user, playlist_id, tracks, 453 | position=None): 454 | """ Adds tracks to a playlist 455 | 456 | Parameters: 457 | - user - the id of the user 458 | - playlist_id - the id of the playlist 459 | - tracks - a list of track URIs, URLs or IDs 460 | - position - the position to add the tracks 461 | """ 462 | plid = self._get_id('playlist', playlist_id) 463 | ftracks = [self._get_uri('track', tid) for tid in tracks] 464 | return self._post("users/%s/playlists/%s/tracks" % (user, plid), 465 | payload=ftracks, position=position) 466 | 467 | def user_playlist_replace_tracks(self, user, playlist_id, tracks): 468 | """ Replace all tracks in a playlist 469 | 470 | Parameters: 471 | - user - the id of the user 472 | - playlist_id - the id of the playlist 473 | - tracks - the list of track ids to add to the playlist 474 | """ 475 | plid = self._get_id('playlist', playlist_id) 476 | ftracks = [self._get_uri('track', tid) for tid in tracks] 477 | payload = {"uris": ftracks} 478 | return self._put("users/%s/playlists/%s/tracks" % (user, plid), 479 | payload=payload) 480 | 481 | def user_playlist_reorder_tracks( 482 | self, user, playlist_id, range_start, insert_before, 483 | range_length=1, snapshot_id=None): 484 | """ Reorder tracks in a playlist 485 | 486 | Parameters: 487 | - user - the id of the user 488 | - playlist_id - the id of the playlist 489 | - range_start - the position of the first track to be reordered 490 | - range_length - optional the number of tracks to be reordered (default: 1) 491 | - insert_before - the position where the tracks should be inserted 492 | - snapshot_id - optional playlist's snapshot ID 493 | """ 494 | plid = self._get_id('playlist', playlist_id) 495 | payload = {"range_start": range_start, 496 | "range_length": range_length, 497 | "insert_before": insert_before} 498 | if snapshot_id: 499 | payload["snapshot_id"] = snapshot_id 500 | return self._put("users/%s/playlists/%s/tracks" % (user, plid), 501 | payload=payload) 502 | 503 | def user_playlist_remove_all_occurrences_of_tracks( 504 | self, user, playlist_id, tracks, snapshot_id=None): 505 | """ Removes all occurrences of the given tracks from the given playlist 506 | 507 | Parameters: 508 | - user - the id of the user 509 | - playlist_id - the id of the playlist 510 | - tracks - the list of track ids to add to the playlist 511 | - snapshot_id - optional id of the playlist snapshot 512 | 513 | """ 514 | 515 | plid = self._get_id('playlist', playlist_id) 516 | ftracks = [self._get_uri('track', tid) for tid in tracks] 517 | payload = {"tracks": [{"uri": track} for track in ftracks]} 518 | if snapshot_id: 519 | payload["snapshot_id"] = snapshot_id 520 | return self._delete("users/%s/playlists/%s/tracks" % (user, plid), 521 | payload=payload) 522 | 523 | def user_playlist_remove_specific_occurrences_of_tracks( 524 | self, user, playlist_id, tracks, snapshot_id=None): 525 | """ Removes all occurrences of the given tracks from the given playlist 526 | 527 | Parameters: 528 | - user - the id of the user 529 | - playlist_id - the id of the playlist 530 | - tracks - an array of objects containing Spotify URIs of the tracks to remove with their current positions in the playlist. For example: 531 | [ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] }, 532 | { "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ] 533 | - snapshot_id - optional id of the playlist snapshot 534 | """ 535 | 536 | plid = self._get_id('playlist', playlist_id) 537 | ftracks = [] 538 | for tr in tracks: 539 | ftracks.append({ 540 | "uri": self._get_uri("track", tr["uri"]), 541 | "positions": tr["positions"], 542 | }) 543 | payload = {"tracks": ftracks} 544 | if snapshot_id: 545 | payload["snapshot_id"] = snapshot_id 546 | return self._delete("users/%s/playlists/%s/tracks" % (user, plid), 547 | payload=payload) 548 | 549 | def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): 550 | """ 551 | Add the current authenticated user as a follower of a playlist. 552 | 553 | Parameters: 554 | - playlist_owner_id - the user id of the playlist owner 555 | - playlist_id - the id of the playlist 556 | 557 | """ 558 | return self._put("users/{}/playlists/{}/followers".format(playlist_owner_id, playlist_id)) 559 | 560 | def user_playlist_is_following(self, playlist_owner_id, playlist_id, user_ids): 561 | """ 562 | Check to see if the given users are following the given playlist 563 | 564 | Parameters: 565 | - playlist_owner_id - the user id of the playlist owner 566 | - playlist_id - the id of the playlist 567 | - user_ids - the ids of the users that you want to check to see if they follow the playlist. Maximum: 5 ids. 568 | 569 | """ 570 | return self._get("users/{}/playlists/{}/followers/contains?ids={}".format(playlist_owner_id, playlist_id, ','.join(user_ids))) 571 | 572 | def me(self): 573 | """ Get detailed profile information about the current user. 574 | An alias for the 'current_user' method. 575 | """ 576 | return self._get('me/') 577 | 578 | def current_user(self): 579 | """ Get detailed profile information about the current user. 580 | An alias for the 'me' method. 581 | """ 582 | return self.me() 583 | 584 | def current_user_playing_track(self): 585 | ''' Get information about the current users currently playing track. 586 | ''' 587 | return self._get('me/player/currently-playing') 588 | 589 | def current_user_saved_albums(self, limit=20, offset=0): 590 | """ Gets a list of the albums saved in the current authorized user's 591 | "Your Music" library 592 | 593 | Parameters: 594 | - limit - the number of albums to return 595 | - offset - the index of the first album to return 596 | 597 | """ 598 | return self._get('me/albums', limit=limit, offset=offset) 599 | 600 | def current_user_saved_tracks(self, limit=20, offset=0): 601 | """ Gets a list of the tracks saved in the current authorized user's 602 | "Your Music" library 603 | 604 | Parameters: 605 | - limit - the number of tracks to return 606 | - offset - the index of the first track to return 607 | 608 | """ 609 | return self._get('me/tracks', limit=limit, offset=offset) 610 | 611 | def current_user_followed_artists(self, limit=20, after=None): 612 | """ Gets a list of the artists followed by the current authorized user 613 | 614 | Parameters: 615 | - limit - the number of tracks to return 616 | - after - ghe last artist ID retrieved from the previous request 617 | 618 | """ 619 | return self._get('me/following', type='artist', limit=limit, 620 | after=after) 621 | 622 | def current_user_saved_tracks_delete(self, tracks=None): 623 | """ Remove one or more tracks from the current user's 624 | "Your Music" library. 625 | 626 | Parameters: 627 | - tracks - a list of track URIs, URLs or IDs 628 | """ 629 | tlist = [] 630 | if tracks is not None: 631 | tlist = [self._get_id('track', t) for t in tracks] 632 | return self._delete('me/tracks/?ids=' + ','.join(tlist)) 633 | 634 | def current_user_saved_tracks_contains(self, tracks=None): 635 | """ Check if one or more tracks is already saved in 636 | the current Spotify user’s “Your Music” library. 637 | 638 | Parameters: 639 | - tracks - a list of track URIs, URLs or IDs 640 | """ 641 | tlist = [] 642 | if tracks is not None: 643 | tlist = [self._get_id('track', t) for t in tracks] 644 | return self._get('me/tracks/contains?ids=' + ','.join(tlist)) 645 | 646 | def current_user_saved_tracks_add(self, tracks=None): 647 | """ Add one or more tracks to the current user's 648 | "Your Music" library. 649 | 650 | Parameters: 651 | - tracks - a list of track URIs, URLs or IDs 652 | """ 653 | tlist = [] 654 | if tracks is not None: 655 | tlist = [self._get_id('track', t) for t in tracks] 656 | return self._put('me/tracks/?ids=' + ','.join(tlist)) 657 | 658 | def current_user_top_artists(self, limit=20, offset=0, 659 | time_range='medium_term'): 660 | """ Get the current user's top artists 661 | 662 | Parameters: 663 | - limit - the number of entities to return 664 | - offset - the index of the first entity to return 665 | - time_range - Over what time frame are the affinities computed 666 | Valid-values: short_term, medium_term, long_term 667 | """ 668 | return self._get('me/top/artists', time_range=time_range, limit=limit, 669 | offset=offset) 670 | 671 | def current_user_top_tracks(self, limit=20, offset=0, 672 | time_range='medium_term'): 673 | """ Get the current user's top tracks 674 | 675 | Parameters: 676 | - limit - the number of entities to return 677 | - offset - the index of the first entity to return 678 | - time_range - Over what time frame are the affinities computed 679 | Valid-values: short_term, medium_term, long_term 680 | """ 681 | return self._get('me/top/tracks', time_range=time_range, limit=limit, 682 | offset=offset) 683 | 684 | def current_user_recently_played(self, limit=50): 685 | ''' Get the current user's recently played tracks 686 | 687 | Parameters: 688 | - limit - the number of entities to return 689 | ''' 690 | return self._get('me/player/recently-played', limit=limit) 691 | 692 | def current_user_saved_albums_add(self, albums=[]): 693 | """ Add one or more albums to the current user's 694 | "Your Music" library. 695 | Parameters: 696 | - albums - a list of album URIs, URLs or IDs 697 | """ 698 | alist = [self._get_id('album', a) for a in albums] 699 | r = self._put('me/albums?ids=' + ','.join(alist)) 700 | return r 701 | 702 | def user_follow_artists(self, ids=[]): 703 | ''' Follow one or more artists 704 | Parameters: 705 | - ids - a list of artist IDs 706 | ''' 707 | return self._put('me/following?type=artist&ids=' + ','.join(ids)) 708 | 709 | def user_follow_users(self, ids=[]): 710 | ''' Follow one or more users 711 | Parameters: 712 | - ids - a list of user IDs 713 | ''' 714 | return self._put('me/following?type=user&ids=' + ','.join(ids)) 715 | 716 | def featured_playlists(self, locale=None, country=None, timestamp=None, 717 | limit=20, offset=0): 718 | """ Get a list of Spotify featured playlists 719 | 720 | Parameters: 721 | - locale - The desired language, consisting of a lowercase ISO 722 | 639 language code and an uppercase ISO 3166-1 alpha-2 country 723 | code, joined by an underscore. 724 | 725 | - country - An ISO 3166-1 alpha-2 country code. 726 | 727 | - timestamp - A timestamp in ISO 8601 format: 728 | yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user's 729 | local time to get results tailored for that specific date and 730 | time in the day 731 | 732 | - limit - The maximum number of items to return. Default: 20. 733 | Minimum: 1. Maximum: 50 734 | 735 | - offset - The index of the first item to return. Default: 0 736 | (the first object). Use with limit to get the next set of 737 | items. 738 | """ 739 | return self._get('browse/featured-playlists', locale=locale, 740 | country=country, timestamp=timestamp, limit=limit, 741 | offset=offset) 742 | 743 | def new_releases(self, country=None, limit=20, offset=0): 744 | """ Get a list of new album releases featured in Spotify 745 | 746 | Parameters: 747 | - country - An ISO 3166-1 alpha-2 country code. 748 | 749 | - limit - The maximum number of items to return. Default: 20. 750 | Minimum: 1. Maximum: 50 751 | 752 | - offset - The index of the first item to return. Default: 0 753 | (the first object). Use with limit to get the next set of 754 | items. 755 | """ 756 | return self._get('browse/new-releases', country=country, limit=limit, 757 | offset=offset) 758 | 759 | def categories(self, country=None, locale=None, limit=20, offset=0): 760 | """ Get a list of new album releases featured in Spotify 761 | 762 | Parameters: 763 | - country - An ISO 3166-1 alpha-2 country code. 764 | - locale - The desired language, consisting of an ISO 639 765 | language code and an ISO 3166-1 alpha-2 country code, joined 766 | by an underscore. 767 | 768 | - limit - The maximum number of items to return. Default: 20. 769 | Minimum: 1. Maximum: 50 770 | 771 | - offset - The index of the first item to return. Default: 0 772 | (the first object). Use with limit to get the next set of 773 | items. 774 | """ 775 | return self._get('browse/categories', country=country, locale=locale, 776 | limit=limit, offset=offset) 777 | 778 | def category_playlists(self, category_id=None, country=None, limit=20, 779 | offset=0): 780 | """ Get a list of new album releases featured in Spotify 781 | 782 | Parameters: 783 | - category_id - The Spotify category ID for the category. 784 | 785 | - country - An ISO 3166-1 alpha-2 country code. 786 | 787 | - limit - The maximum number of items to return. Default: 20. 788 | Minimum: 1. Maximum: 50 789 | 790 | - offset - The index of the first item to return. Default: 0 791 | (the first object). Use with limit to get the next set of 792 | items. 793 | """ 794 | return self._get('browse/categories/' + category_id + '/playlists', 795 | country=country, limit=limit, offset=offset) 796 | 797 | def recommendations(self, seed_artists=None, seed_genres=None, 798 | seed_tracks=None, limit=20, country=None, **kwargs): 799 | """ Get a list of recommended tracks for one to five seeds. 800 | 801 | Parameters: 802 | - seed_artists - a list of artist IDs, URIs or URLs 803 | 804 | - seed_tracks - a list of artist IDs, URIs or URLs 805 | 806 | - seed_genres - a list of genre names. Available genres for 807 | recommendations can be found by calling recommendation_genre_seeds 808 | 809 | - country - An ISO 3166-1 alpha-2 country code. If provided, all 810 | results will be playable in this country. 811 | 812 | - limit - The maximum number of items to return. Default: 20. 813 | Minimum: 1. Maximum: 100 814 | 815 | - min/max/target_ - For the tuneable track attributes listed 816 | in the documentation, these values provide filters and targeting on 817 | results. 818 | """ 819 | params = dict(limit=limit) 820 | if seed_artists: 821 | params['seed_artists'] = ','.join( 822 | [self._get_id('artist', a) for a in seed_artists]) 823 | if seed_genres: 824 | params['seed_genres'] = ','.join(seed_genres) 825 | if seed_tracks: 826 | params['seed_tracks'] = ','.join( 827 | [self._get_id('track', t) for t in seed_tracks]) 828 | if country: 829 | params['market'] = country 830 | 831 | for attribute in ["acousticness", "danceability", "duration_ms", 832 | "energy", "instrumentalness", "key", "liveness", 833 | "loudness", "mode", "popularity", "speechiness", 834 | "tempo", "time_signature", "valence"]: 835 | for prefix in ["min_", "max_", "target_"]: 836 | param = prefix + attribute 837 | if param in kwargs: 838 | params[param] = kwargs[param] 839 | return self._get('recommendations', **params) 840 | 841 | def recommendation_genre_seeds(self): 842 | """ Get a list of genres available for the recommendations function. 843 | """ 844 | return self._get('recommendations/available-genre-seeds') 845 | 846 | def audio_analysis(self, track_id): 847 | """ Get audio analysis for a track based upon its Spotify ID 848 | Parameters: 849 | - track_id - a track URI, URL or ID 850 | """ 851 | trid = self._get_id('track', track_id) 852 | return self._get('audio-analysis/' + trid) 853 | 854 | def audio_features(self, tracks=[]): 855 | """ Get audio features for one or multiple tracks based upon their Spotify IDs 856 | Parameters: 857 | - tracks - a list of track URIs, URLs or IDs, maximum: 50 ids 858 | """ 859 | if isinstance(tracks, str): 860 | trackid = self._get_id('track', tracks) 861 | results = self._get('audio-features/?ids=' + trackid) 862 | else: 863 | tlist = [self._get_id('track', t) for t in tracks] 864 | results = self._get('audio-features/?ids=' + ','.join(tlist)) 865 | # the response has changed, look for the new style first, and if 866 | # its not there, fallback on the old style 867 | if 'audio_features' in results: 868 | return results['audio_features'] 869 | else: 870 | return results 871 | 872 | def audio_analysis(self, id): 873 | """ Get audio analysis for a track based upon its Spotify ID 874 | Parameters: 875 | - id - a track URIs, URLs or IDs 876 | """ 877 | id = self._get_id('track', id) 878 | return self._get('audio-analysis/'+id) 879 | 880 | def devices(self): 881 | ''' Get a list of user's available devices. 882 | ''' 883 | return self._get("me/player/devices") 884 | 885 | def current_playback(self, market = None): 886 | ''' Get information about user's current playback. 887 | 888 | Parameters: 889 | - market - an ISO 3166-1 alpha-2 country code. 890 | ''' 891 | return self._get("me/player", market = market) 892 | 893 | def currently_playing(self, market = None): 894 | ''' Get user's currently playing track. 895 | 896 | Parameters: 897 | - market - an ISO 3166-1 alpha-2 country code. 898 | ''' 899 | return self._get("me/player/currently-playing", market = market) 900 | 901 | def transfer_playback(self, device_id, force_play = True): 902 | ''' Transfer playback to another device. 903 | Note that the API accepts a list of device ids, but only 904 | actually supports one. 905 | 906 | Parameters: 907 | - device_id - transfer playback to this device 908 | - force_play - true: after transfer, play. false: 909 | keep current state. 910 | ''' 911 | data = { 912 | 'device_ids': [device_id], 913 | 'play': force_play 914 | } 915 | return self._put("me/player", payload=data) 916 | 917 | def start_playback(self, device_id = None, context_uri = None, uris = None, offset = None): 918 | ''' Start or resume user's playback. 919 | 920 | Provide a `context_uri` to start playback or a album, 921 | artist, or playlist. 922 | 923 | Provide a `uris` list to start playback of one or more 924 | tracks. 925 | 926 | Provide `offset` as {"position": } or {"uri": ""} 927 | to start playback at a particular offset. 928 | 929 | Parameters: 930 | - device_id - device target for playback 931 | - context_uri - spotify context uri to play 932 | - uris - spotify track uris 933 | - offset - offset into context by index or track 934 | ''' 935 | if context_uri is not None and uris is not None: 936 | self._warn('specify either context uri or uris, not both') 937 | return 938 | if uris is not None and not isinstance(uris, list): 939 | self._warn('uris must be a list') 940 | return 941 | data = {} 942 | if context_uri is not None: 943 | data['context_uri'] = context_uri 944 | if uris is not None: 945 | data['uris'] = uris 946 | if offset is not None: 947 | data['offset'] = offset 948 | return self._put(self._append_device_id("me/player/play", device_id), payload=data) 949 | 950 | def pause_playback(self, device_id = None): 951 | ''' Pause user's playback. 952 | 953 | Parameters: 954 | - device_id - device target for playback 955 | ''' 956 | return self._put(self._append_device_id("me/player/pause", device_id)) 957 | 958 | def next_track(self, device_id = None): 959 | ''' Skip user's playback to next track. 960 | 961 | Parameters: 962 | - device_id - device target for playback 963 | ''' 964 | return self._post(self._append_device_id("me/player/next", device_id)) 965 | 966 | def previous_track(self, device_id = None): 967 | ''' Skip user's playback to previous track. 968 | 969 | Parameters: 970 | - device_id - device target for playback 971 | ''' 972 | return self._post(self._append_device_id("me/player/previous", device_id)) 973 | 974 | def seek_track(self, position_ms, device_id = None): 975 | ''' Seek to position in current track. 976 | 977 | Parameters: 978 | - position_ms - position in milliseconds to seek to 979 | - device_id - device target for playback 980 | ''' 981 | if not isinstance(position_ms, int): 982 | self._warn('position_ms must be an integer') 983 | return 984 | return self._put(self._append_device_id("me/player/seek?position_ms=%s" % position_ms, device_id)) 985 | 986 | def repeat(self, state, device_id = None): 987 | ''' Set repeat mode for playback. 988 | 989 | Parameters: 990 | - state - `track`, `context`, or `off` 991 | - device_id - device target for playback 992 | ''' 993 | if state not in ['track', 'context', 'off']: 994 | self._warn('invalid state') 995 | return 996 | self._put(self._append_device_id("me/player/repeat?state=%s" % state, device_id)) 997 | 998 | def volume(self, volume_percent, device_id = None): 999 | ''' Set playback volume. 1000 | 1001 | Parameters: 1002 | - volume_percent - volume between 0 and 100 1003 | - device_id - device target for playback 1004 | ''' 1005 | if not isinstance(volume_percent, int): 1006 | self._warn('volume must be an integer') 1007 | return 1008 | if volume_percent < 0 or volume_percent > 100: 1009 | self._warn('volume must be between 0 and 100, inclusive') 1010 | return 1011 | self._put(self._append_device_id("me/player/volume?volume_percent=%s" % volume_percent, device_id)) 1012 | 1013 | def shuffle(self, state, device_id = None): 1014 | ''' Toggle playback shuffling. 1015 | 1016 | Parameters: 1017 | - state - true or false 1018 | - device_id - device target for playback 1019 | ''' 1020 | if not isinstance(state, bool): 1021 | self._warn('state must be a boolean') 1022 | return 1023 | state = str(state).lower() 1024 | self._put(self._append_device_id("me/player/shuffle?state=%s" % state, device_id)) 1025 | 1026 | def _append_device_id(self, path, device_id): 1027 | ''' Append device ID to API path. 1028 | 1029 | Parameters: 1030 | - device_id - device id to append 1031 | ''' 1032 | if device_id: 1033 | if '?' in path: 1034 | path += "&device_id=%s" % device_id 1035 | else: 1036 | path += "?device_id=%s" % device_id 1037 | return path 1038 | 1039 | def _get_id(self, type, id): 1040 | fields = id.split(':') 1041 | if len(fields) >= 3: 1042 | if type != fields[-2]: 1043 | self._warn('expected id of type %s but found type %s %s', 1044 | type, fields[-2], id) 1045 | return fields[-1] 1046 | fields = id.split('/') 1047 | if len(fields) >= 3: 1048 | itype = fields[-2] 1049 | if type != itype: 1050 | self._warn('expected id of type %s but found type %s %s', 1051 | type, itype, id) 1052 | return fields[-1] 1053 | return id 1054 | 1055 | def _get_uri(self, type, id): 1056 | return 'spotify:' + type + ":" + self._get_id(type, id) 1057 | --------------------------------------------------------------------------------