class _HTTP: __slots__ = ['version', 'http'] def __init__(self, version: str, pool_size: int): self.version = version self.http = PoolManager(num_pools=pool_size) def get(self, uri: str = '', params: dict = {}, headers: dict = {}) -> response: header = { 'User-Agent': 'Python3 SDK v%s' % self.version, } header.update(headers) return self.http.request_encode_url('GET', uri, fields=params, headers=header) def post(self, uri: str = '', params: dict = {}, headers: dict = {}) -> response: header = { 'User-Agent': 'Python3 SDK v%s' % self.version, } header.update(headers) return self.http.request_encode_body('POST', uri, fields=params, headers=headers, encode_multipart=False)
class Client: accounts_url = "https://accounts.spotify.com" api_url = "https://api.spotify.com/v1" redirect_uri = "http://localhost:8888" def __init__(self): if not (Config.client and Config.secret): raise Exception("Missing Spotify client credentials") self.client = PoolManager(ca_certs=where()) def response(self, response: HTTPResponse, success_code: int = 200) -> Dict[str, Any]: if response.status != success_code: raise SpotifyError(f"Failed with code {response.status}: " f"{response.reason} ({response.data})") return loads(response.data) @property def token(self) -> str: if not Config.token: self.authorize() elif Config.validity < time(): self.refresh() return Config.token def authorize(self) -> None: payload = { "client_id": Config.client, "response_type": "code", "redirect_uri": self.redirect_uri, "scope": "playlist-modify-public", } log.info("Opening browser for authorization") webbrowser.open(f"{self.accounts_url}/authorize?{urlencode(payload)}") with HTTPServer(("", 8888), HTTPRequestHandler) as HTTPRequestHandler.server: log.info("Listening for authentication callback") HTTPRequestHandler.server.serve_forever() code = HTTPRequestHandler.spotify_code self.authenticate(code) def authenticate(self, code: str) -> None: payload = { "code": code, "redirect_uri": self.redirect_uri, "grant_type": "authorization_code", "client_id": Config.client, "client_secret": Config.secret, } log.info("Authenticating client") response = self.client.request_encode_body( "POST", self.accounts_url + "/api/token", fields=payload, encode_multipart=False, ) data = self.response(response) Config.update( token=data["access_token"], validity=time() + data["expires_in"], refresh=data["refresh_token"], ) def refresh(self): payload = { "refresh_token": Config.refresh, "grant_type": "refresh_token" } keys = f"{Config.client}:{Config.secret}" encoded_keys = b64encode(keys.encode("ascii")).decode("ascii") headers = {"Authorization": f"Basic {encoded_keys}"} log.info("Refreshing token") response = self.client.request_encode_body( "POST", self.accounts_url + "/api/token", fields=payload, headers=headers, encode_multipart=False, ) data = self.response(response) Config.update(token=data["access_token"], validity=time() + data["expires_in"]) @overload def search(self, artist: str, title: str) -> Optional[SpotifyTrack]: ... @overload def search(self, artist: str, title: str, limit: int) -> Optional[List[SpotifyTrack]]: ... def search(self, artist: str, title: str, limit: int = 1): q: List[str] = [] if artist: q += [f"artist:{artist}"] if title: q += [f"track:{title}"] params = { "limit": limit, "type": "track", "q": " ".join(q), } headers = {"Authorization": f"Bearer {self.token}"} query = urlencode(params, quote_via=quote).replace("%3A", ":") log.debug(f"Searching for {query}") response = self.client.request("GET", f"{self.api_url}/search?{query}", headers=headers) data = self.response(response) items = data["tracks"]["items"] if items: tracks = [ SpotifyTrack( uri=track["id"], title=track["name"], artist=track["artists"][0]["name"], album=track["album"]["name"], ) for track in items ] return tracks[0] if limit == 1 else tracks return None def replace(self, playlist: str, tracks: List[SpotifyTrack]) -> None: headers = { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json", } url = self.api_url + f"/playlists/{playlist}/tracks" body = {"uris": [f"spotify:track:{track.uri}" for track in tracks]} log.info("Updating playlist") response = self.client.request("PUT", url, headers=headers, body=dumps(body)) self.response(response, 201)
class Spotify: accounts_url = 'https://accounts.spotify.com' api_url = 'https://api.spotify.com/v1' redirect_uri = 'http://localhost:8888' def __init__(self): if not (Config.client and Config.secret): raise Exception('Missing Spotify client credentials') self.client = PoolManager(ca_certs=where()) def response(self, response: HTTPResponse, success_code: int = 200) -> Dict[str, Any]: if response.status != success_code: raise SpotifyError(f'Failed with code {response.status}: ' f'{response.reason} ({response.data})') return loads(response.data) @property def token(self) -> str: if not Config.token: self.authorize() elif Config.validity < time(): self.refresh() return Config.token def authorize(self) -> None: payload = { 'client_id': Config.client, 'response_type': 'code', 'redirect_uri': self.redirect_uri, 'scope': 'playlist-modify-public', } log.info('Opening browser for authorization') webbrowser.open(f"{self.accounts_url}/authorize?{urlencode(payload)}") with HTTPServer(("", 8888), HTTPRequestHandler) as HTTPRequestHandler.server: log.info("Listening for authentication callback") HTTPRequestHandler.server.serve_forever() code = HTTPRequestHandler.spotify_code self.authenticate(code) def authenticate(self, code: str) -> None: payload = { 'code': code, 'redirect_uri': self.redirect_uri, 'grant_type': 'authorization_code', 'client_id': Config.client, 'client_secret': Config.secret, } log.info('Authenticating client') response = self.client.request_encode_body( 'POST', self.accounts_url + '/api/token', fields=payload, encode_multipart=False) data = self.response(response) Config.update( token=data['access_token'], validity=time() + data['expires_in'], refresh=data['refresh_token']) def refresh(self): payload = { 'refresh_token': Config.refresh, 'grant_type': 'refresh_token' } keys = f"{Config.client}:{Config.secret}" encoded_keys = b64encode(keys.encode("ascii")).decode("ascii") headers = {'Authorization': f'Basic {encoded_keys}'} log.info('Refreshing token') response = self.client.request_encode_body( 'POST', self.accounts_url + '/api/token', fields=payload, headers=headers, encode_multipart=False) data = self.response(response) Config.update( token=data['access_token'], validity=time() + data['expires_in']) @overload def search(self, artist: str, title: str) -> Optional[SpotifyTrack]: ... @overload def search(self, artist: str, title: str, limit: int) -> Optional[List[SpotifyTrack]]: ... def search(self, artist: str, title: str, limit: int = 1 ): q: List[str] = [] if artist: q += [f"artist:{artist}"] if title: q += [f"track:{title}"] params = { 'limit': limit, 'type': 'track', 'q': ' '.join(q), } headers = {'Authorization': f"Bearer {self.token}"} query = urlencode(params, quote_via=quote).replace('%3A', ':') log.debug(f"Searching for {query}") response = self.client.request( 'GET', f"{self.api_url}/search?{query}", headers=headers) data = self.response(response) items = data['tracks']['items'] if items: tracks = [ SpotifyTrack( uri=track['id'], title=track['name'], artist=track['artists'][0]['name'], album=track['album']['name']) for track in items ] return tracks[0] if limit == 1 else tracks return None def replace(self, playlist: str, tracks: List[SpotifyTrack]) -> None: headers = { 'Authorization': f"Bearer {self.token}", 'Content-Type': 'application/json' } url = self.api_url + f'/playlists/{playlist}/tracks' body = {'uris': [f"spotify:track:{track.uri}" for track in tracks]} log.info('Updating playlist') response = self.client.request( 'PUT', url, headers=headers, body=dumps(body)) self.response(response, 201)