diff --git a/librespot/core.py b/librespot/core.py index fd13f70..f7d602d 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -94,9 +94,8 @@ class ApiClient(Closeable): 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 @@ -105,8 +104,7 @@ 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 @@ -130,13 +128,11 @@ class ApiClient(Closeable): """ 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: """ :param connection_id: str: @@ -154,16 +150,11 @@ class ApiClient(Closeable): ) 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: """ @@ -171,9 +162,9 @@ class ApiClient(Closeable): :param track: TrackId: """ - 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: @@ -188,9 +179,9 @@ class ApiClient(Closeable): :param episode: EpisodeId: """ - 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: @@ -205,9 +196,9 @@ class ApiClient(Closeable): :param album: AlbumId: """ - 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 @@ -223,9 +214,9 @@ class ApiClient(Closeable): :param artist: ArtistId: """ - 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: @@ -240,9 +231,9 @@ class ApiClient(Closeable): :param show: ShowId: """ - 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: @@ -251,15 +242,16 @@ class ApiClient(Closeable): proto.ParseFromString(body) return proto - def get_playlist(self, _id: PlaylistId) -> Playlist4External.SelectedListContent: + def get_playlist(self, + _id: PlaylistId) -> Playlist4External.SelectedListContent: """ :param _id: PlaylistId: """ - 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: @@ -278,7 +270,8 @@ class ApiClient(Closeable): def __client_token(self): proto_req = ClientToken.ClientTokenRequest( - request_type=ClientToken.ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST, + request_type=ClientToken.ClientTokenRequestType. + REQUEST_CLIENT_DATA_REQUEST, client_data=ClientToken.ClientDataRequest( client_id=MercuryRequests.keymaster_client_id, client_version=Version.version_name, @@ -293,8 +286,7 @@ class ApiClient(Closeable): something7=332, something8=33404, something10=True, - ), - ), + ), ), ), ), ) @@ -345,7 +337,8 @@ 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( @@ -414,7 +407,8 @@ 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: """ :param listener: MessageListener: @@ -424,8 +418,7 @@ class DealerClient(Closeable): 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() @@ -439,8 +432,7 @@ 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() @@ -469,7 +461,8 @@ 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: """ @@ -527,10 +520,8 @@ 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: @@ -544,8 +535,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: @@ -597,7 +588,8 @@ 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 @@ -621,8 +613,7 @@ 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): @@ -644,7 +635,8 @@ 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): """ @@ -654,13 +646,10 @@ class DealerClient(Closeable): """ 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(): """ """ @@ -680,9 +669,11 @@ 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): """ """ @@ -695,12 +686,11 @@ class DealerClient(Closeable): :param 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): """ """ @@ -727,21 +717,19 @@ 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]): """ :param event_or_builder: typing.Union[GenericEvent: @@ -788,6 +776,7 @@ class EventService(Closeable): class GenericEvent: """ """ + def build(self) -> EventService.EventBuilder: """ """ raise NotImplementedError @@ -811,7 +800,9 @@ 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: """ :param c: int: (Default value = None) @@ -889,23 +880,21 @@ 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 = {} @@ -915,11 +904,8 @@ 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: """ """ @@ -942,7 +928,8 @@ 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 :param credential: Spotify account login information @@ -965,13 +952,11 @@ 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: """ """ @@ -1000,9 +985,8 @@ class Session(Closeable, MessageListener, SubListener): def close(self) -> None: """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() @@ -1028,7 +1012,8 @@ 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: """Connect to the Spotify Server""" @@ -1038,12 +1023,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() @@ -1057,25 +1042,25 @@ 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 @@ -1093,14 +1078,11 @@ 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_proto.SerializeToString()) self.connection.write_int(4 + len(client_response_plaintext_bytes)) self.connection.write(client_response_plaintext_bytes) self.connection.flush() @@ -1108,7 +1090,8 @@ 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) @@ -1169,9 +1152,8 @@ 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: """ @@ -1180,11 +1162,8 @@ class Session(Closeable, MessageListener, SubListener): :param fallback: str: (Default value = None) """ - 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: """ """ @@ -1200,7 +1179,8 @@ 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): """ :param uri: str: @@ -1226,7 +1206,8 @@ 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: """ """ @@ -1238,8 +1219,7 @@ 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( @@ -1249,9 +1229,8 @@ 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: """ """ @@ -1298,9 +1277,9 @@ class Session(Closeable, MessageListener, SubListener): """ """ 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: @@ -1319,8 +1298,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() @@ -1329,12 +1308,11 @@ 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 @@ -1342,19 +1320,19 @@ 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") + 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() + 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( { @@ -1432,8 +1410,7 @@ 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: """ :param device_type: Connect.DeviceType: @@ -1502,12 +1479,13 @@ class Session(Closeable, MessageListener, SubListener): """ 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: """ :param device_id: str: @@ -1519,9 +1497,11 @@ class Session(Closeable, MessageListener, SubListener): 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" @@ -1539,8 +1519,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) @@ -1578,7 +1558,8 @@ 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"]), ) @@ -1586,7 +1567,8 @@ 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 :param stored_credentials: str: (Default value = None) @@ -1604,7 +1586,8 @@ 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"]), ) @@ -1722,7 +1705,8 @@ 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 @@ -1762,8 +1746,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 :param cache_enabled: bool: @@ -1773,7 +1757,8 @@ 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 :param cache_dir: str: @@ -1784,8 +1769,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 :param do_cache_clean_up: bool: @@ -1796,8 +1781,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 :param store_credentials: bool: @@ -1808,7 +1793,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 @@ -1820,7 +1805,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 @@ -1975,9 +1960,8 @@ 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: """ """ @@ -2004,21 +1988,18 @@ 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: @@ -2026,19 +2007,16 @@ 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 @@ -2046,49 +2024,47 @@ 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: @@ -2112,17 +2088,15 @@ 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) class SearchException(Exception): """ """ + def __init__(self, status_code: int): super().__init__("Search failed with code {}.".format(status_code)) @@ -2195,7 +2169,8 @@ 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: """ :param image_size: str: @@ -2243,8 +2218,7 @@ 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]: """ :param scopes: typing.List[str]: @@ -2279,19 +2253,15 @@ 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 @@ -2310,9 +2280,9 @@ class TokenProvider: 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: """