diff --git a/librespot/core.py b/librespot/core.py index 4639f00..ebe01a4 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -12,7 +12,15 @@ from librespot.cache import CacheManager from librespot.crypto import CipherPair, DiffieHellman, Packet from librespot.mercury import MercuryClient, MercuryRequests, RawMercuryRequest from librespot.metadata import AlbumId, ArtistId, EpisodeId, ShowId, TrackId, PlaylistId -from librespot.proto import Authentication_pb2 as Authentication, ClientToken_pb2 as ClientToken, Connect_pb2 as Connect, Connectivity_pb2 as Connectivity, Keyexchange_pb2 as Keyexchange, Metadata_pb2 as Metadata, Playlist4External_pb2 as Playlist4External +from librespot.proto import ( + Authentication_pb2 as Authentication, + ClientToken_pb2 as ClientToken, + Connect_pb2 as Connect, + Connectivity_pb2 as Connectivity, + Keyexchange_pb2 as Keyexchange, + Metadata_pb2 as Metadata, + Playlist4External_pb2 as Playlist4External, +) from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate from librespot.structure import Closeable, MessageListener, RequestListener, SubListener import base64 @@ -48,14 +56,18 @@ class ApiClient(Closeable): self.__base_url = "https://{}".format(ApResolver.get_random_spclient()) def build_request( - self, method: str, suffix: str, - headers: typing.Union[None, typing.Dict[str, str]], - body: typing.Union[None, bytes]) -> requests.PreparedRequest: + self, + method: str, + suffix: str, + headers: typing.Union[None, typing.Dict[str, str]], + body: typing.Union[None, bytes], + ) -> requests.PreparedRequest: if self.__client_token_str is None: resp = self.__client_token() self.__client_token_str = resp.granted_token.token - self.logger.debug("Updated client token: {}".format( - self.__client_token_str)) + self.logger.debug( + "Updated client token: {}".format(self.__client_token_str) + ) request = requests.PreparedRequest() request.method = method @@ -64,41 +76,53 @@ class ApiClient(Closeable): if headers is not None: request.headers = headers request.headers["Authorization"] = "Bearer {}".format( - self.__session.tokens().get("playlist-read")) + self.__session.tokens().get("playlist-read") + ) request.headers["client-token"] = self.__client_token_str request.url = self.__base_url + suffix return request - def send(self, method: str, suffix: str, - headers: typing.Union[None, typing.Dict[str, str]], - body: typing.Union[None, bytes]) -> requests.Response: + def send( + self, + method: str, + suffix: str, + headers: typing.Union[None, typing.Dict[str, str]], + body: typing.Union[None, bytes], + ) -> requests.Response: response = self.__session.client().send( - self.build_request(method, suffix, headers, body)) + self.build_request(method, suffix, headers, body) + ) return response - def put_connect_state(self, connection_id: str, - proto: Connect.PutStateRequest) -> None: + def put_connect_state( + self, connection_id: str, proto: Connect.PutStateRequest + ) -> None: response = self.send( "PUT", "/connect-state/v1/devices/{}".format(self.__session.device_id()), { "Content-Type": "application/protobuf", - "X-Spotify-Connection-Id": connection_id + "X-Spotify-Connection-Id": connection_id, }, proto.SerializeToString(), ) if response.status_code == 413: self.logger.warning( - "PUT state payload is too large: {} bytes uncompressed.". - format(len(proto.SerializeToString()))) + "PUT state payload is too large: {} bytes uncompressed.".format( + len(proto.SerializeToString()) + ) + ) elif response.status_code != 200: - self.logger.warning("PUT state returned {}. headers: {}".format( - response.status_code, response.headers)) + self.logger.warning( + "PUT state returned {}. headers: {}".format( + response.status_code, response.headers + ) + ) def get_metadata_4_track(self, track: TrackId) -> Metadata.Track: - response = self.send("GET", - "/metadata/4/track/{}".format(track.hex_id()), - None, None) + response = self.send( + "GET", "/metadata/4/track/{}".format(track.hex_id()), None, None + ) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -108,9 +132,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_episode(self, episode: EpisodeId) -> Metadata.Episode: - response = self.send("GET", - "/metadata/4/episode/{}".format(episode.hex_id()), - None, None) + response = self.send( + "GET", "/metadata/4/episode/{}".format(episode.hex_id()), None, None + ) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -120,9 +144,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_album(self, album: AlbumId) -> Metadata.Album: - response = self.send("GET", - "/metadata/4/album/{}".format(album.hex_id()), - None, None) + response = self.send( + "GET", "/metadata/4/album/{}".format(album.hex_id()), None, None + ) ApiClient.StatusCodeException.check_status(response) body = response.content @@ -133,9 +157,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_artist(self, artist: ArtistId) -> Metadata.Artist: - response = self.send("GET", - "/metadata/4/artist/{}".format(artist.hex_id()), - None, None) + response = self.send( + "GET", "/metadata/4/artist/{}".format(artist.hex_id()), None, None + ) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -145,9 +169,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_show(self, show: ShowId) -> Metadata.Show: - response = self.send("GET", - "/metadata/4/show/{}".format(show.hex_id()), None, - None) + response = self.send( + "GET", "/metadata/4/show/{}".format(show.hex_id()), None, None + ) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -157,9 +181,9 @@ class ApiClient(Closeable): return proto def get_playlist(self, _id: PlaylistId) -> Playlist4External.SelectedListContent: - response = self.send("GET", - "/playlist/v2/playlist/{}".format(_id.id()), None, - None) + response = self.send( + "GET", "/playlist/v2/playlist/{}".format(_id.id()), None, None + ) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -194,12 +218,14 @@ class ApiClient(Closeable): ), ) - resp = requests.post("https://clienttoken.spotify.com/v1/clienttoken", - proto_req.SerializeToString(), - headers={ - "Accept": "application/x-protobuf", - "Content-Encoding": "", - }) + resp = requests.post( + "https://clienttoken.spotify.com/v1/clienttoken", + proto_req.SerializeToString(), + headers={ + "Accept": "application/x-protobuf", + "Content-Encoding": "", + }, + ) ApiClient.StatusCodeException.check_status(resp) @@ -232,12 +258,12 @@ class ApResolver: Returns: The resulting object will be returned """ - response = requests.get("{}?type={}".format(ApResolver.base_url, - service_type)) + response = requests.get("{}?type={}".format(ApResolver.base_url, service_type)) if response.status_code != 200: if response.status_code == 502: raise RuntimeError( - f"ApResolve request failed with the following return value: {response.content}. Servers might be down!") + f"ApResolve request failed with the following return value: {response.content}. Servers might be down!" + ) return response.json() @staticmethod @@ -298,12 +324,12 @@ class DealerClient(Closeable): def __init__(self, session: Session): self.__session = session - def add_message_listener(self, listener: MessageListener, - uris: list[str]) -> None: + def add_message_listener(self, listener: MessageListener, uris: list[str]) -> None: with self.__message_listeners_lock: if listener in self.__message_listeners: raise TypeError( - "A listener for {} has already been added.".format(uris)) + "A listener for {} has already been added.".format(uris) + ) self.__message_listeners[listener] = uris self.__message_listeners_lock.notify_all() @@ -311,7 +337,8 @@ class DealerClient(Closeable): with self.__request_listeners_lock: if uri in self.__request_listeners: raise TypeError( - "A listener for '{}' has already been added.".format(uri)) + "A listener for '{}' has already been added.".format(uri) + ) self.__request_listeners[uri] = listener self.__request_listeners_lock.notify_all() @@ -320,9 +347,13 @@ class DealerClient(Closeable): def connect(self) -> None: self.__connection = DealerClient.ConnectionHolder( - self.__session, self, "wss://{}/?access_token={}".format( + self.__session, + self, + "wss://{}/?access_token={}".format( ApResolver.get_random_dealer(), - self.__session.tokens().get("playlist-read"))) + self.__session.tokens().get("playlist-read"), + ), + ) def connection_invalided(self) -> None: self.__connection = None @@ -332,8 +363,7 @@ class DealerClient(Closeable): self.__last_scheduled_reconnection = None self.connect() - self.__last_scheduled_reconnection = self.__scheduler.enter( - 10, 1, anonymous) + self.__last_scheduled_reconnection = self.__scheduler.enter(10, 1, anonymous) def handle_message(self, obj: typing.Any) -> None: uri = obj.get("uri") @@ -380,8 +410,10 @@ class DealerClient(Closeable): sender = payload.get("sent_by_device_id") command = payload.get("command") self.logger.debug( - "Received request. [mid: {}, key: {}, pid: {}, sender: {}, command: {}]" - .format(mid, key, pid, sender, command)) + "Received request. [mid: {}, key: {}, pid: {}, sender: {}, command: {}]".format( + mid, key, pid, sender, command + ) + ) interesting = False with self.__request_listeners_lock: for mid_prefix in self.__request_listeners: @@ -394,8 +426,8 @@ class DealerClient(Closeable): if self.__connection is not None: self.__connection.send_reply(key, result) self.logger.warning( - "Handled request. [key: {}, result: {}]".format( - key, result)) + "Handled request. [key: {}, result: {}]".format(key, result) + ) self.__worker.submit(anonymous) if not interesting: @@ -435,8 +467,7 @@ class DealerClient(Closeable): __url: str __ws: websocket.WebSocketApp - def __init__(self, session: Session, dealer_client: DealerClient, - url: str): + def __init__(self, session: Session, dealer_client: DealerClient, url: str): self.__session = session self.__dealer_client = dealer_client self.__url = url @@ -453,7 +484,8 @@ class DealerClient(Closeable): if self.__closed: return self.__dealer_client.logger.warning( - "An exception occurred. Reconnecting...") + "An exception occurred. Reconnecting..." + ) self.close() def on_message(self, ws: websocket.WebSocketApp, text: str): @@ -469,16 +501,18 @@ class DealerClient(Closeable): elif typ == MessageType.PING: pass else: - raise RuntimeError("Unknown message type for {}".format( - typ.value)) + raise RuntimeError("Unknown message type for {}".format(typ.value)) def on_open(self, ws: websocket.WebSocketApp): if self.__closed: self.__dealer_client.logger.fatal( - "I wonder what happened here... Terminating. [closed: {}]". - format(self.__closed)) + "I wonder what happened here... Terminating. [closed: {}]".format( + self.__closed + ) + ) self.__dealer_client.logger.debug( - "Dealer connected! [url: {}]".format(self.__url)) + "Dealer connected! [url: {}]".format(self.__url) + ) def anonymous(): self.send_ping() @@ -496,20 +530,20 @@ class DealerClient(Closeable): self.__received_pong = False self.__scheduler.enter(3, 1, anonymous2) - self.__last_scheduled_ping = self.__scheduler.enter( - 30, 1, anonymous) + self.__last_scheduled_ping = self.__scheduler.enter(30, 1, anonymous) - self.__last_scheduled_ping = self.__scheduler.enter( - 30, 1, anonymous) + self.__last_scheduled_ping = self.__scheduler.enter(30, 1, anonymous) def send_ping(self): - self.__ws.send("{\"type\":\"ping\"}") + self.__ws.send('{"type":"ping"}') def send_reply(self, key: str, result: DealerClient.RequestResult): - success = "true" if result == DealerClient.RequestResult.SUCCESS else "false" + success = ( + "true" if result == DealerClient.RequestResult.SUCCESS else "false" + ) self.__ws.send( - "{\"type\":\"reply\",\"key\":\"%s\",\"payload\":{\"success\":%s}" - % (key, success)) + '{"type":"reply","key":"%s","payload":{"success":%s}' % (key, success) + ) class RequestResult(enum.Enum): UNKNOWN_SEND_COMMAND_RESULT = 0 @@ -534,19 +568,21 @@ class EventService(Closeable): try: body = event_builder.to_array() resp = self.__session.mercury().send_sync( - RawMercuryRequest.Builder().set_uri( - "hm://event-service/v1/events").set_method("POST"). - add_user_field("Accept-Language", "en").add_user_field( - "X-ClientTimeStamp", - int(time.time() * 1000)).add_payload_part(body).build()) - self.logger.debug("Event sent. body: {}, result: {}".format( - body, resp.status_code)) + RawMercuryRequest.Builder() + .set_uri("hm://event-service/v1/events") + .set_method("POST") + .add_user_field("Accept-Language", "en") + .add_user_field("X-ClientTimeStamp", int(time.time() * 1000)) + .add_payload_part(body) + .build() + ) + self.logger.debug( + "Event sent. body: {}, result: {}".format(body, resp.status_code) + ) except IOError as ex: - self.logger.error("Failed sending event: {} {}".format( - event_builder, ex)) + self.logger.error("Failed sending event: {} {}".format(event_builder, ex)) - def send_event(self, event_or_builder: typing.Union[GenericEvent, - EventBuilder]): + def send_event(self, event_or_builder: typing.Union[GenericEvent, EventBuilder]): if type(event_or_builder) is EventService.GenericEvent: builder = event_or_builder.build() elif type(event_or_builder) is EventService.EventBuilder: @@ -595,9 +631,7 @@ class EventService(Closeable): s = "" self.body.write(s.encode()) - def append(self, - c: int = None, - s: str = None) -> EventService.EventBuilder: + def append(self, c: int = None, s: str = None) -> EventService.EventBuilder: if c is None and s is None or c is not None and s is not None: raise TypeError() if c is not None: @@ -661,21 +695,23 @@ class Session(Closeable, MessageListener, SubListener): __mercury_client: MercuryClient __receiver: typing.Union[Receiver, None] = None __search: typing.Union[SearchManager, None] - __server_key = b"\xac\xe0F\x0b\xff\xc20\xaf\xf4k\xfe\xc3\xbf\xbf\x86=" \ - b"\xa1\x91\xc6\xcc3l\x93\xa1O\xb3\xb0\x16\x12\xac\xacj" \ - b"\xf1\x80\xe7\xf6\x14\xd9B\x9d\xbe.4fC\xe3b\xd22z\x1a" \ - b"\r\x92;\xae\xdd\x14\x02\xb1\x81U\x05a\x04\xd5,\x96\xa4" \ - b"L\x1e\xcc\x02J\xd4\xb2\x0c\x00\x1f\x17\xed\xc2/\xc45" \ - b"!\xc8\xf0\xcb\xae\xd2\xad\xd7+\x0f\x9d\xb3\xc52\x1a*" \ - b"\xfeY\xf3Z\r\xach\xf1\xfab\x1e\xfb,\x8d\x0c\xb79-\x92" \ - b"G\xe3\xd75\x1am\xbd$\xc2\xae%[\x88\xff\xabs)\x8a\x0b" \ - b"\xcc\xcd\x0cXg1\x89\xe8\xbd4\x80xJ_\xc9k\x89\x9d\x95k" \ - b"\xfc\x86\xd7O3\xa6x\x17\x96\xc9\xc3-\r2\xa5\xab\xcd\x05'" \ - b"\xe2\xf7\x10\xa3\x96\x13\xc4/\x99\xc0'\xbf\xed\x04\x9c" \ - b"<'X\x04\xb6\xb2\x19\xf9\xc1/\x02\xe9Hc\xec\xa1\xb6B\xa0" \ - b"\x9dH%\xf8\xb3\x9d\xd0\xe8j\xf9HM\xa1\xc2\xba\x860B\xea" \ - b"\x9d\xb3\x08l\x19\x0eH\xb3\x9df\xeb\x00\x06\xa2Z\xee\xa1" \ - b"\x1b\x13\x87<\xd7\x19\xe6U\xbd" + __server_key = ( + b"\xac\xe0F\x0b\xff\xc20\xaf\xf4k\xfe\xc3\xbf\xbf\x86=" + b"\xa1\x91\xc6\xcc3l\x93\xa1O\xb3\xb0\x16\x12\xac\xacj" + b"\xf1\x80\xe7\xf6\x14\xd9B\x9d\xbe.4fC\xe3b\xd22z\x1a" + b"\r\x92;\xae\xdd\x14\x02\xb1\x81U\x05a\x04\xd5,\x96\xa4" + b"L\x1e\xcc\x02J\xd4\xb2\x0c\x00\x1f\x17\xed\xc2/\xc45" + b"!\xc8\xf0\xcb\xae\xd2\xad\xd7+\x0f\x9d\xb3\xc52\x1a*" + b"\xfeY\xf3Z\r\xach\xf1\xfab\x1e\xfb,\x8d\x0c\xb79-\x92" + b"G\xe3\xd75\x1am\xbd$\xc2\xae%[\x88\xff\xabs)\x8a\x0b" + b"\xcc\xcd\x0cXg1\x89\xe8\xbd4\x80xJ_\xc9k\x89\x9d\x95k" + b"\xfc\x86\xd7O3\xa6x\x17\x96\xc9\xc3-\r2\xa5\xab\xcd\x05'" + b"\xe2\xf7\x10\xa3\x96\x13\xc4/\x99\xc0'\xbf\xed\x04\x9c" + b"<'X\x04\xb6\xb2\x19\xf9\xc1/\x02\xe9Hc\xec\xa1\xb6B\xa0" + b"\x9dH%\xf8\xb3\x9d\xd0\xe8j\xf9HM\xa1\xc2\xba\x860B\xea" + b"\x9d\xb3\x08l\x19\x0eH\xb3\x9df\xeb\x00\x06\xa2Z\xee\xa1" + b"\x1b\x13\x87<\xd7\x19\xe6U\xbd" + ) __stored_str: str = "" __token_provider: typing.Union[TokenProvider, None] __user_attributes = {} @@ -685,8 +721,11 @@ class Session(Closeable, MessageListener, SubListener): self.connection = Session.ConnectionHolder.create(address, None) self.__inner = inner self.__keys = DiffieHellman() - self.logger.info("Created new session! device_id: {}, ap: {}".format( - inner.device_id, address)) + self.logger.info( + "Created new session! device_id: {}, ap: {}".format( + inner.device_id, address + ) + ) def api(self) -> ApiClient: self.__wait_auth_lock() @@ -706,8 +745,7 @@ class Session(Closeable, MessageListener, SubListener): raise RuntimeError("Session isn't authenticated!") return self.__audio_key_manager - def authenticate(self, - credential: Authentication.LoginCredentials) -> None: + def authenticate(self, credential: Authentication.LoginCredentials) -> None: """ Log in to Spotify Args: @@ -729,11 +767,13 @@ class Session(Closeable, MessageListener, SubListener): self.__auth_lock_bool = False self.__auth_lock.notify_all() self.dealer().connect() - self.logger.info("Authenticated as {}!".format( - self.__ap_welcome.canonical_username)) + self.logger.info( + "Authenticated as {}!".format(self.__ap_welcome.canonical_username) + ) self.mercury().interested_in("spotify:user:attributes:update", self) self.dealer().add_message_listener( - self, ["hm://connect-state/v1/connect/logout"]) + self, ["hm://connect-state/v1/connect/logout"] + ) def cache(self) -> CacheManager: self.__wait_auth_lock() @@ -760,8 +800,9 @@ class Session(Closeable, MessageListener, SubListener): """ Close instance """ - self.logger.info("Closing session. device_id: {}".format( - self.__inner.device_id)) + self.logger.info( + "Closing session. device_id: {}".format(self.__inner.device_id) + ) self.__closing = True if self.__dealer_client is not None: self.__dealer_client.close() @@ -787,8 +828,7 @@ class Session(Closeable, MessageListener, SubListener): self.__ap_welcome = None self.cipher_pair = None self.__closed = True - self.logger.info("Closed session. device_id: {}".format( - self.__inner.device_id)) + self.logger.info("Closed session. device_id: {}".format(self.__inner.device_id)) def connect(self) -> None: """ @@ -800,12 +840,12 @@ class Session(Closeable, MessageListener, SubListener): client_hello_proto = Keyexchange.ClientHello( build_info=Version.standard_build_info(), client_nonce=nonce, - cryptosuites_supported=[ - Keyexchange.Cryptosuite.CRYPTO_SUITE_SHANNON - ], + cryptosuites_supported=[Keyexchange.Cryptosuite.CRYPTO_SUITE_SHANNON], login_crypto_hello=Keyexchange.LoginCryptoHelloUnion( diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanHello( - gc=self.__keys.public_key_bytes(), server_keys_known=1), ), + gc=self.__keys.public_key_bytes(), server_keys_known=1 + ), + ), padding=b"\x1e", ) client_hello_bytes = client_hello_proto.SerializeToString() @@ -819,24 +859,26 @@ class Session(Closeable, MessageListener, SubListener): # Read APResponseMessage ap_response_message_length = self.connection.read_int() acc.write_int(ap_response_message_length) - ap_response_message_bytes = self.connection.read( - ap_response_message_length - 4) + ap_response_message_bytes = self.connection.read(ap_response_message_length - 4) acc.write(ap_response_message_bytes) ap_response_message_proto = Keyexchange.APResponseMessage() ap_response_message_proto.ParseFromString(ap_response_message_bytes) shared_key = util.int_to_bytes( self.__keys.compute_shared_key( - ap_response_message_proto.challenge.login_crypto_challenge. - diffie_hellman.gs)) + ap_response_message_proto.challenge.login_crypto_challenge.diffie_hellman.gs + ) + ) # Check gs_signature rsa = RSA.construct((int.from_bytes(self.__server_key, "big"), 65537)) pkcs1_v1_5 = PKCS1_v1_5.new(rsa) sha1 = SHA1.new() - sha1.update(ap_response_message_proto.challenge.login_crypto_challenge. - diffie_hellman.gs) + sha1.update( + ap_response_message_proto.challenge.login_crypto_challenge.diffie_hellman.gs + ) if not pkcs1_v1_5.verify( - sha1, ap_response_message_proto.challenge. - login_crypto_challenge.diffie_hellman.gs_signature): + sha1, + ap_response_message_proto.challenge.login_crypto_challenge.diffie_hellman.gs_signature, + ): raise RuntimeError("Failed signature check!") # Solve challenge buffer = io.BytesIO() @@ -853,10 +895,13 @@ class Session(Closeable, MessageListener, SubListener): crypto_response=Keyexchange.CryptoResponseUnion(), login_crypto_response=Keyexchange.LoginCryptoResponseUnion( diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse( - hmac=challenge)), + hmac=challenge + ) + ), pow_response=Keyexchange.PoWResponseUnion(), ) - client_response_plaintext_bytes = client_response_plaintext_proto.SerializeToString( + client_response_plaintext_bytes = ( + client_response_plaintext_proto.SerializeToString() ) self.connection.write_int(4 + len(client_response_plaintext_bytes)) self.connection.write(client_response_plaintext_bytes) @@ -865,8 +910,7 @@ class Session(Closeable, MessageListener, SubListener): self.connection.set_timeout(1) scrap = self.connection.read(4) if len(scrap) == 4: - payload = self.connection.read( - struct.unpack(">i", scrap)[0] - 4) + payload = self.connection.read(struct.unpack(">i", scrap)[0] - 4) failed = Keyexchange.APResponseMessage() failed.ParseFromString(payload) raise RuntimeError(failed) @@ -912,12 +956,16 @@ class Session(Closeable, MessageListener, SubListener): attributes_update.ParseFromString(resp.payload) for pair in attributes_update.pairs_list: self.__user_attributes[pair.key] = pair.value - self.logger.info("Updated user attribute: {} -> {}".format( - pair.key, pair.value)) + self.logger.info( + "Updated user attribute: {} -> {}".format(pair.key, pair.value) + ) def get_user_attribute(self, key: str, fallback: str = None) -> str: - return self.__user_attributes.get(key) if self.__user_attributes.get( - key) is not None else fallback + return ( + self.__user_attributes.get(key) + if self.__user_attributes.get(key) is not None + else fallback + ) def is_valid(self) -> bool: if self.__closed: @@ -931,8 +979,7 @@ class Session(Closeable, MessageListener, SubListener): raise RuntimeError("Session isn't authenticated!") return self.__mercury_client - def on_message(self, uri: str, headers: typing.Dict[str, str], - payload: bytes): + def on_message(self, uri: str, headers: typing.Dict[str, str], payload: bytes): if uri == "hm://connect-state/v1/connect/logout": self.close() @@ -950,8 +997,7 @@ class Session(Closeable, MessageListener, SubListener): return for i in range(len(product)): self.__user_attributes[product[i].tag] = product[i].text - self.logger.debug("Parsed product info: {}".format( - self.__user_attributes)) + self.logger.debug("Parsed product info: {}".format(self.__user_attributes)) def preferred_locale(self) -> str: return self.__inner.preferred_locale @@ -964,7 +1010,8 @@ class Session(Closeable, MessageListener, SubListener): self.connection.close() self.__receiver.stop() self.connection = Session.ConnectionHolder.create( - ApResolver.get_random_accesspoint(), self.__inner.conf) + ApResolver.get_random_accesspoint(), self.__inner.conf + ) self.connect() self.__authenticate_partial( Authentication.LoginCredentials( @@ -974,8 +1021,9 @@ class Session(Closeable, MessageListener, SubListener): ), True, ) - self.logger.info("Re-authenticated as {}!".format( - self.__ap_welcome.canonical_username)) + self.logger.info( + "Re-authenticated as {}!".format(self.__ap_welcome.canonical_username) + ) def reconnecting(self) -> bool: return not self.__closing and not self.__closed and self.connection is None @@ -1015,9 +1063,9 @@ class Session(Closeable, MessageListener, SubListener): def stored(self): return self.__stored_str - def __authenticate_partial(self, - credential: Authentication.LoginCredentials, - remove_lock: bool) -> None: + def __authenticate_partial( + self, credential: Authentication.LoginCredentials, remove_lock: bool + ) -> None: """ Login to Spotify Args: @@ -1036,8 +1084,8 @@ class Session(Closeable, MessageListener, SubListener): version_string=Version.version_string(), ) self.__send_unchecked( - Packet.Type.login, - client_response_encrypted_proto.SerializeToString()) + Packet.Type.login, client_response_encrypted_proto.SerializeToString() + ) packet = self.cipher_pair.receive_encoded(self.connection) if packet.is_cmd(Packet.Type.ap_welcome): self.__ap_welcome = Authentication.APWelcome() @@ -1046,11 +1094,12 @@ class Session(Closeable, MessageListener, SubListener): bytes0x0f = Random.get_random_bytes(0x14) self.__send_unchecked(Packet.Type.unknown_0x0f, bytes0x0f) preferred_locale = io.BytesIO() - preferred_locale.write(b"\x00\x00\x10\x00\x02preferred-locale" + - self.__inner.preferred_locale.encode()) + preferred_locale.write( + b"\x00\x00\x10\x00\x02preferred-locale" + + self.__inner.preferred_locale.encode() + ) preferred_locale.seek(0) - self.__send_unchecked(Packet.Type.preferred_locale, - preferred_locale.read()) + self.__send_unchecked(Packet.Type.preferred_locale, preferred_locale.read()) if remove_lock: with self.__auth_lock: self.__auth_lock_bool = False @@ -1058,22 +1107,28 @@ class Session(Closeable, MessageListener, SubListener): if self.__inner.conf.store_credentials: reusable = self.__ap_welcome.reusable_auth_credentials reusable_type = Authentication.AuthenticationType.Name( - self.__ap_welcome.reusable_auth_credentials_type) + self.__ap_welcome.reusable_auth_credentials_type + ) if self.__inner.conf.stored_credentials_file is None: - raise TypeError( - "The file path to be saved is not specified") - self.__stored_str = base64.b64encode(json.dumps({ - "username": self.__ap_welcome.canonical_username, - "credentials": base64.b64encode(reusable).decode(), - "type": reusable_type, - }).encode()).decode() + raise TypeError("The file path to be saved is not specified") + self.__stored_str = base64.b64encode( + json.dumps( + { + "username": self.__ap_welcome.canonical_username, + "credentials": base64.b64encode(reusable).decode(), + "type": reusable_type, + } + ).encode() + ).decode() with open(self.__inner.conf.stored_credentials_file, "w") as f: json.dump( { "username": self.__ap_welcome.canonical_username, "credentials": base64.b64encode(reusable).decode(), "type": reusable_type, - }, f) + }, + f, + ) elif packet.is_cmd(Packet.Type.auth_failure): ap_login_failed = Keyexchange.APLoginFailed() @@ -1126,7 +1181,8 @@ class Session(Closeable, MessageListener, SubListener): return self def set_device_type( - self, device_type: Connect.DeviceType) -> Session.AbsBuilder: + self, device_type: Connect.DeviceType + ) -> Session.AbsBuilder: self.device_type = device_type return self @@ -1178,22 +1234,19 @@ class Session(Closeable, MessageListener, SubListener): def blob(self, username: str, blob: bytes) -> Session.Builder: if self.device_id is None: raise TypeError("You must specify the device ID first.") - self.login_credentials = self.decrypt_blob(self.device_id, - username, blob) + self.login_credentials = self.decrypt_blob(self.device_id, username, blob) return self def decrypt_blob( - self, device_id: str, username: str, - encrypted_blob: bytes) -> Authentication.LoginCredentials: + self, device_id: str, username: str, encrypted_blob: bytes + ) -> Authentication.LoginCredentials: encrypted_blob = base64.b64decode(encrypted_blob) sha1 = SHA1.new() sha1.update(device_id.encode()) secret = sha1.digest() - base_key = PBKDF2(secret, - username.encode(), - 20, - 0x100, - hmac_hash_module=SHA1) + base_key = PBKDF2( + secret, username.encode(), 20, 0x100, hmac_hash_module=SHA1 + ) sha1 = SHA1.new() sha1.update(base_key) key = sha1.digest() + b"\x00\x00\x00\x14" @@ -1211,8 +1264,8 @@ class Session(Closeable, MessageListener, SubListener): type_ = Authentication.AuthenticationType.Name(type_int) if type_ is None: raise IOError( - TypeError( - "Unknown AuthenticationType: {}".format(type_int))) + TypeError("Unknown AuthenticationType: {}".format(type_int)) + ) blob.read(1) l = self.read_blob_int(blob) auth_data = blob.read(l) @@ -1227,7 +1280,7 @@ class Session(Closeable, MessageListener, SubListener): if (int(lo[0]) & 0x80) == 0: return int(lo[0]) hi = buffer.read(1) - return int(lo[0]) & 0x7f | int(hi[0]) << 7 + return int(lo[0]) & 0x7F | int(hi[0]) << 7 def stored(self, stored_credentials_str: str): """ @@ -1246,8 +1299,7 @@ class Session(Closeable, MessageListener, SubListener): else: try: self.login_credentials = Authentication.LoginCredentials( - typ=Authentication.AuthenticationType.Value( - obj["type"]), + typ=Authentication.AuthenticationType.Value(obj["type"]), username=obj["username"], auth_data=base64.b64decode(obj["credentials"]), ) @@ -1255,8 +1307,7 @@ class Session(Closeable, MessageListener, SubListener): pass return self - def stored_file(self, - stored_credentials: str = None) -> Session.Builder: + def stored_file(self, stored_credentials: str = None) -> Session.Builder: """ Create credential from stored file Args: @@ -1275,8 +1326,7 @@ class Session(Closeable, MessageListener, SubListener): else: try: self.login_credentials = Authentication.LoginCredentials( - typ=Authentication.AuthenticationType.Value( - obj["type"]), + typ=Authentication.AuthenticationType.Value(obj["type"]), username=obj["username"], auth_data=base64.b64decode(obj["credentials"]), ) @@ -1391,8 +1441,7 @@ class Session(Closeable, MessageListener, SubListener): # Stored credentials store_credentials: bool = True - stored_credentials_file: str = os.path.join( - os.getcwd(), "credentials.json") + stored_credentials_file: str = os.path.join(os.getcwd(), "credentials.json") # Fetching retry_on_chunk_error: bool = True @@ -1432,8 +1481,8 @@ class Session(Closeable, MessageListener, SubListener): # return self def set_cache_enabled( - self, - cache_enabled: bool) -> Session.Configuration.Builder: + self, cache_enabled: bool + ) -> Session.Configuration.Builder: """ Set cache_enabled Args: @@ -1444,8 +1493,7 @@ class Session(Closeable, MessageListener, SubListener): self.cache_enabled = cache_enabled return self - def set_cache_dir(self, - cache_dir: str) -> Session.Configuration.Builder: + def set_cache_dir(self, cache_dir: str) -> Session.Configuration.Builder: """ Set cache_dir Args: @@ -1457,8 +1505,8 @@ class Session(Closeable, MessageListener, SubListener): return self def set_do_cache_clean_up( - self, - do_cache_clean_up: bool) -> Session.Configuration.Builder: + self, do_cache_clean_up: bool + ) -> Session.Configuration.Builder: """ Set do_cache_clean_up Args: @@ -1470,8 +1518,8 @@ class Session(Closeable, MessageListener, SubListener): return self def set_store_credentials( - self, - store_credentials: bool) -> Session.Configuration.Builder: + self, store_credentials: bool + ) -> Session.Configuration.Builder: """ Set store_credentials Args: @@ -1483,7 +1531,7 @@ class Session(Closeable, MessageListener, SubListener): return self def set_stored_credential_file( - self, stored_credential_file: str + self, stored_credential_file: str ) -> Session.Configuration.Builder: """ Set stored_credential_file @@ -1496,7 +1544,7 @@ class Session(Closeable, MessageListener, SubListener): return self def set_retry_on_chunk_error( - self, retry_on_chunk_error: bool + self, retry_on_chunk_error: bool ) -> Session.Configuration.Builder: """ Set retry_on_chunk_error @@ -1539,8 +1587,7 @@ class Session(Closeable, MessageListener, SubListener): self.__socket = sock @staticmethod - def create(address: str, conf) \ - -> Session.ConnectionHolder: + def create(address: str, conf) -> Session.ConnectionHolder: """ Create the ConnectionHolder instance Args: @@ -1649,8 +1696,9 @@ class Session(Closeable, MessageListener, SubListener): self.conf = conf self.device_type = device_type self.device_name = device_name - self.device_id = (device_id if device_id is not None else - util.random_hex_string(40)) + self.device_id = ( + device_id if device_id is not None else util.random_hex_string(40) + ) class Receiver: __session: Session @@ -1677,18 +1725,21 @@ class Session(Closeable, MessageListener, SubListener): cmd: bytes try: packet = self.__session.cipher_pair.receive_encoded( - self.__session.connection) + self.__session.connection + ) cmd = Packet.Type.parse(packet.cmd) if cmd is None: self.__session.logger.info( - "Skipping unknown command cmd: 0x{}, payload: {}". - format(util.bytes_to_hex(packet.cmd), - packet.payload)) + "Skipping unknown command cmd: 0x{}, payload: {}".format( + util.bytes_to_hex(packet.cmd), packet.payload + ) + ) continue except (RuntimeError, ConnectionResetError) as ex: if self.__running: self.__session.logger.fatal( - "Failed reading packet! {}".format(ex)) + "Failed reading packet! {}".format(ex) + ) self.__session.reconnect() break if not self.__running: @@ -1696,15 +1747,18 @@ class Session(Closeable, MessageListener, SubListener): if cmd == Packet.Type.ping: if self.__session.scheduled_reconnect is not None: self.__session.scheduler.cancel( - self.__session.scheduled_reconnect) + self.__session.scheduled_reconnect + ) def anonymous(): self.__session.logger.warning( - "Socket timed out. Reconnecting...") + "Socket timed out. Reconnecting..." + ) self.__session.reconnect() self.__session.scheduled_reconnect = self.__session.scheduler.enter( - 2 * 60 + 5, 1, anonymous) + 2 * 60 + 5, 1, anonymous + ) self.__session.send(Packet.Type.pong, packet.payload) elif cmd == Packet.Type.pong_ack: continue @@ -1712,43 +1766,48 @@ class Session(Closeable, MessageListener, SubListener): self.__session.__country_code = packet.payload.decode() self.__session.logger.info( "Received country_code: {}".format( - self.__session.__country_code)) + self.__session.__country_code + ) + ) elif cmd == Packet.Type.license_version: license_version = io.BytesIO(packet.payload) - license_id = struct.unpack(">h", - license_version.read(2))[0] + license_id = struct.unpack(">h", license_version.read(2))[0] if license_id != 0: buffer = license_version.read() self.__session.logger.info( "Received license_version: {}, {}".format( - license_id, buffer.decode())) + license_id, buffer.decode() + ) + ) else: self.__session.logger.info( - "Received license_version: {}".format(license_id)) + "Received license_version: {}".format(license_id) + ) elif cmd == Packet.Type.unknown_0x10: - self.__session.logger.debug("Received 0x10: {}".format( - util.bytes_to_hex(packet.payload))) + self.__session.logger.debug( + "Received 0x10: {}".format(util.bytes_to_hex(packet.payload)) + ) elif cmd in [ - Packet.Type.mercury_sub, Packet.Type.mercury_unsub, - Packet.Type.mercury_event, Packet.Type.mercury_req + Packet.Type.mercury_sub, + Packet.Type.mercury_unsub, + Packet.Type.mercury_event, + Packet.Type.mercury_req, ]: self.__session.mercury().dispatch(packet) elif cmd in [Packet.Type.aes_key, Packet.Type.aes_key_error]: self.__session.audio_key().dispatch(packet) - elif cmd in [ - Packet.Type.channel_error, Packet.Type.stream_chunk_res - ]: + elif cmd in [Packet.Type.channel_error, Packet.Type.stream_chunk_res]: self.__session.channel().dispatch(packet) elif cmd == Packet.Type.product_info: self.__session.parse_product_info(packet.payload) else: - self.__session.logger.info("Skipping {}".format( - util.bytes_to_hex(cmd))) + self.__session.logger.info( + "Skipping {}".format(util.bytes_to_hex(cmd)) + ) class SpotifyAuthenticationException(Exception): def __init__(self, login_failed: Keyexchange.APLoginFailed): - super().__init__( - Keyexchange.ErrorCode.Name(login_failed.error_code)) + super().__init__(Keyexchange.ErrorCode.Name(login_failed.error_code)) class SearchManager: @@ -1766,8 +1825,11 @@ class SearchManager: if request.get_locale() == "": request.set_locale(self.__session.preferred_locale()) response = self.__session.mercury().send_sync( - RawMercuryRequest.new_builder().set_method("GET").set_uri( - request.build_url()).build()) + RawMercuryRequest.new_builder() + .set_method("GET") + .set_uri(request.build_url()) + .build() + ) if response.status_code != 200: raise SearchManager.SearchException(response.status_code) return json.loads(response.payload) @@ -1827,8 +1889,7 @@ class SearchManager: self.__country = country return self - def set_image_size(self, - image_size: str) -> SearchManager.SearchRequest: + def set_image_size(self, image_size: str) -> SearchManager.SearchRequest: self.__image_size = image_size return self @@ -1855,7 +1916,8 @@ class TokenProvider: self._session = session def find_token_with_all_scopes( - self, scopes: typing.List[str]) -> typing.Union[StoredToken, None]: + self, scopes: typing.List[str] + ) -> typing.Union[StoredToken, None]: for token in self.__tokens: if token.has_scopes(scopes): return token @@ -1875,15 +1937,19 @@ class TokenProvider: else: return token self.logger.debug( - "Token expired or not suitable, requesting again. scopes: {}, old_token: {}" - .format(scopes, token)) + "Token expired or not suitable, requesting again. scopes: {}, old_token: {}".format( + scopes, token + ) + ) response = self._session.mercury().send_sync_json( - MercuryRequests.request_token(self._session.device_id(), - ",".join(scopes))) + MercuryRequests.request_token(self._session.device_id(), ",".join(scopes)) + ) token = TokenProvider.StoredToken(response) self.logger.debug( "Updated token successfully! scopes: {}, new_token: {}".format( - scopes, token)) + scopes, token + ) + ) self.__tokens.append(token) return token @@ -1900,10 +1966,9 @@ class TokenProvider: self.scopes = obj["scope"] def expired(self) -> bool: - return ( - self.timestamp + - (self.expires_in - TokenProvider.token_expire_threshold) * 1000 - < int(time.time_ns() / 1000)) + return self.timestamp + ( + self.expires_in - TokenProvider.token_expire_threshold + ) * 1000 < int(time.time_ns() / 1000) def has_scope(self, scope: str) -> bool: for s in self.scopes: