diff --git a/examples/player.py b/examples/player.py index 5d00a27..3f98328 100644 --- a/examples/player.py +++ b/examples/player.py @@ -33,18 +33,15 @@ def client(): return if (args[0] == "p" or args[0] == "play") and len(args) == 2: track_uri_search = re.search( - r"^spotify:track:(?P[0-9a-zA-Z]{22})$", args[1] - ) + r"^spotify:track:(?P[0-9a-zA-Z]{22})$", args[1]) track_url_search = re.search( r"^(https?://)?open.spotify.com/track/(?P[0-9a-zA-Z]{22})(\?si=.+?)?$", args[1], ) if track_uri_search is not None or track_url_search is not None: - track_id_str = ( - track_uri_search - if track_uri_search is not None - else track_url_search - ).group("TrackID") + track_id_str = (track_uri_search + if track_uri_search is not None else + track_url_search).group("TrackID") play(track_id_str) wait() if args[0] == "q" or args[0] == "quality": @@ -64,20 +61,22 @@ def client(): token = session.tokens().get("user-read-email") resp = requests.get( "https://api.spotify.com/v1/search", - {"limit": "5", "offset": "0", "q": cmd[2:], "type": "track"}, + { + "limit": "5", + "offset": "0", + "q": cmd[2:], + "type": "track" + }, headers={"Authorization": "Bearer %s" % token}, ) i = 1 tracks = resp.json()["tracks"]["items"] for track in tracks: - print( - "%d, %s | %s" - % ( - i, - track["name"], - ",".join([artist["name"] for artist in track["artists"]]), - ) - ) + print("%d, %s | %s" % ( + i, + track["name"], + ",".join([artist["name"] for artist in track["artists"]]), + )) i += 1 position = -1 while True: @@ -115,8 +114,7 @@ def login(): def play(track_id_str: str): track_id = TrackId.from_base62(track_id_str) stream = session.content_feeder().load( - track_id, VorbisOnlyAudioQuality(AudioQuality.VERY_HIGH), False, None - ) + track_id, VorbisOnlyAudioQuality(AudioQuality.VERY_HIGH), False, None) ffplay = subprocess.Popen( ["ffplay", "-"], stdin=subprocess.PIPE, @@ -132,13 +130,11 @@ def play(track_id_str: str): def splash(): - print( - "=================================\n" - "| Librespot-Python Player |\n" - "| |\n" - "| by kokarare1212 |\n" - "=================================\n\n\n" - ) + print("=================================\n" + "| Librespot-Python Player |\n" + "| |\n" + "| by kokarare1212 |\n" + "=================================\n\n\n") def main(): diff --git a/librespot/audio/AbsChunkedInputStream.py b/librespot/audio/AbsChunkedInputStream.py index c730d82..a69bd7e 100644 --- a/librespot/audio/AbsChunkedInputStream.py +++ b/librespot/audio/AbsChunkedInputStream.py @@ -22,7 +22,9 @@ class AbsChunkedInputStream(InputStream, HaltListener): _decoded_length: int = 0 def __init__(self, retry_on_chunk_error: bool): - self.retries: typing.Final[typing.List[int]] = [0 for _ in range(self.chunks())] + self.retries: typing.Final[typing.List[int]] = [ + 0 for _ in range(self.chunks()) + ] self.retry_on_chunk_error = retry_on_chunk_error def is_closed(self) -> bool: @@ -107,13 +109,10 @@ class AbsChunkedInputStream(InputStream, HaltListener): self.request_chunk_from_stream(chunk) self.requested_chunks()[chunk] = True - for i in range( - chunk + 1, min(self.chunks() - 1, chunk + self.preload_ahead) + 1 - ): - if ( - self.requested_chunks()[i] - and self.retries[i] < self.preload_chunk_retries - ): + for i in range(chunk + 1, + min(self.chunks() - 1, chunk + self.preload_ahead) + 1): + if (self.requested_chunks()[i] + and self.retries[i] < self.preload_chunk_retries): self.request_chunk_from_stream(i) self.requested_chunks()[chunk] = True @@ -147,7 +146,10 @@ class AbsChunkedInputStream(InputStream, HaltListener): self.check_availability(chunk, True, True) - def read(self, b: bytearray = None, offset: int = None, length: int = None) -> int: + def read(self, + b: bytearray = None, + offset: int = None, + length: int = None) -> int: if b is None and offset is None and length is None: return self.internal_read() if not (b is not None and offset is not None and length is not None): @@ -157,9 +159,8 @@ class AbsChunkedInputStream(InputStream, HaltListener): raise IOError("Stream is closed!") if offset < 0 or length < 0 or length > len(b) - offset: - raise IndexError( - "offset: {}, length: {}, buffer: {}".format(offset, length, len(b)) - ) + raise IndexError("offset: {}, length: {}, buffer: {}".format( + offset, length, len(b))) elif length == 0: return 0 @@ -174,7 +175,8 @@ class AbsChunkedInputStream(InputStream, HaltListener): self.check_availability(chunk, True, False) copy = min(len(self.buffer()[chunk]) - chunk_off, length - i) - b[offset + 0 : copy] = self.buffer()[chunk][chunk_off : chunk_off + copy] + b[offset + 0:copy] = self.buffer()[chunk][chunk_off:chunk_off + + copy] i += copy self._pos += copy @@ -222,5 +224,4 @@ class AbsChunkedInputStream(InputStream, HaltListener): @staticmethod def from_stream_error(stream_error: int): return AbsChunkedInputStream.ChunkException( - "Failed due to stream error, code: {}".format(stream_error) - ) + "Failed due to stream error, code: {}".format(stream_error)) diff --git a/librespot/audio/cdn/CdnFeedHelper.py b/librespot/audio/cdn/CdnFeedHelper.py index b0eca33..3a25cea 100644 --- a/librespot/audio/cdn/CdnFeedHelper.py +++ b/librespot/audio/cdn/CdnFeedHelper.py @@ -40,7 +40,8 @@ class CdnFeedHelper: streamer = session.cdn().stream_file(file, key, url, halt_listener) input_stream = streamer.stream() - normalization_data = NormalizationData.NormalizationData.read(input_stream) + normalization_data = NormalizationData.NormalizationData.read( + input_stream) if input_stream.skip(0xA7) != 0xA7: raise IOError("Couldn't skip 0xa7 bytes!") return PlayableContentFeeder.PlayableContentFeeder.LoadedStream( @@ -48,13 +49,13 @@ class CdnFeedHelper: streamer, normalization_data, PlayableContentFeeder.PlayableContentFeeder.Metrics( - file.file_id, preload, -1 if preload else audio_key_time - ), + file.file_id, preload, -1 if preload else audio_key_time), ) @staticmethod def load_episode_external( - session: Session, episode: Metadata.Episode, halt_listener: HaltListener + session: Session, episode: Metadata.Episode, + halt_listener: HaltListener ) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream: resp = session.client().head(episode.external_url) @@ -62,18 +63,17 @@ class CdnFeedHelper: CdnFeedHelper._LOGGER.warning("Couldn't resolve redirect!") url = resp.url - CdnFeedHelper._LOGGER.debug( - "Fetched external url for {}: {}".format( - Utils.Utils.bytes_to_hex(episode.gid), url - ) - ) + CdnFeedHelper._LOGGER.debug("Fetched external url for {}: {}".format( + Utils.Utils.bytes_to_hex(episode.gid), url)) - streamer = session.cdn().stream_external_episode(episode, url, halt_listener) + streamer = session.cdn().stream_external_episode( + episode, url, halt_listener) return PlayableContentFeeder.PlayableContentFeeder.LoadedStream( episode, streamer, None, - PlayableContentFeeder.PlayableContentFeeder.Metrics(None, False, -1), + PlayableContentFeeder.PlayableContentFeeder.Metrics( + None, False, -1), ) @staticmethod @@ -102,6 +102,5 @@ class CdnFeedHelper: streamer, normalization_data, PlayableContentFeeder.PlayableContentFeeder.Metrics( - file.file_id, False, audio_key_time - ), + file.file_id, False, audio_key_time), ) diff --git a/librespot/audio/cdn/CdnManager.py b/librespot/audio/cdn/CdnManager.py index 6f3228a..a33ca6b 100644 --- a/librespot/audio/cdn/CdnManager.py +++ b/librespot/audio/cdn/CdnManager.py @@ -37,9 +37,9 @@ class CdnManager: def get_head(self, file_id: bytes): resp = self._session.client().get( self._session.get_user_attribute( - "head-files-url", "https://heads-fa.spotify.com/head/{file_id}" - ).replace("{file_id}", Utils.bytes_to_hex(file_id)) - ) + "head-files-url", + "https://heads-fa.spotify.com/head/{file_id}").replace( + "{file_id}", Utils.bytes_to_hex(file_id))) if resp.status_code != 200: raise IOError("{}".format(resp.status_code)) @@ -50,9 +50,9 @@ class CdnManager: return body - def stream_external_episode( - self, episode: Metadata.Episode, external_url: str, halt_listener: HaltListener - ): + def stream_external_episode(self, episode: Metadata.Episode, + external_url: str, + halt_listener: HaltListener): return CdnManager.Streamer( self._session, StreamId(episode), @@ -84,8 +84,7 @@ class CdnManager: resp = self._session.api().send( "GET", "/storage-resolve/files/audio/interactive/{}".format( - Utils.bytes_to_hex(file_id) - ), + Utils.bytes_to_hex(file_id)), None, None, ) @@ -101,13 +100,11 @@ class CdnManager: proto.ParseFromString(body) if proto.result == StorageResolve.StorageResolveResponse.Result.CDN: url = random.choice(proto.cdnurl) - self._LOGGER.debug( - "Fetched CDN url for {}: {}".format(Utils.bytes_to_hex(file_id), url) - ) + self._LOGGER.debug("Fetched CDN url for {}: {}".format( + Utils.bytes_to_hex(file_id), url)) return url raise CdnManager.CdnException( - "Could not retrieve CDN url! result: {}".format(proto.result) - ) + "Could not retrieve CDN url! result: {}".format(proto.result)) class CdnException(Exception): pass @@ -167,8 +164,7 @@ class CdnManager: if expire_at is None: self._expiration = -1 self._cdnManager._LOGGER.warning( - "Invalid __token__ in CDN url: {}".format(url) - ) + "Invalid __token__ in CDN url: {}".format(url)) return self._expiration = expire_at * 1000 @@ -178,10 +174,8 @@ class CdnManager: except ValueError: self._expiration = -1 self._cdnManager._LOGGER.warning( - "Couldn't extract expiration, invalid parameter in CDN url: ".format( - url - ) - ) + "Couldn't extract expiration, invalid parameter in CDN url: " + .format(url)) return self._expiration = int(token_url.query[:i]) * 1000 @@ -190,8 +184,8 @@ class CdnManager: self._expiration = -1 class Streamer( - GeneralAudioStream.GeneralAudioStream, - GeneralWritableStream.GeneralWritableStream, + GeneralAudioStream.GeneralAudioStream, + GeneralWritableStream.GeneralWritableStream, ): _session: Session = None _streamId: StreamId = None @@ -224,38 +218,39 @@ class CdnManager: self._cdnUrl = cdn_url self._haltListener = halt_listener - resp = self.request(range_start=0, range_end=ChannelManager.CHUNK_SIZE - 1) + resp = self.request(range_start=0, + range_end=ChannelManager.CHUNK_SIZE - 1) content_range = resp._headers.get("Content-Range") if content_range is None: raise IOError("Missing Content-Range header!") split = Utils.split(content_range, "/") self._size = int(split[1]) - self._chunks = int(math.ceil(self._size / ChannelManager.CHUNK_SIZE)) + self._chunks = int( + math.ceil(self._size / ChannelManager.CHUNK_SIZE)) first_chunk = resp._buffer self._available = [False for _ in range(self._chunks)] self._requested = [False for _ in range(self._chunks)] self._buffer = [bytearray() for _ in range(self._chunks)] - self._internalStream = CdnManager.Streamer.InternalStream(self, False) + self._internalStream = CdnManager.Streamer.InternalStream( + self, False) self._requested[0] = True self.write_chunk(first_chunk, 0, False) - def write_chunk(self, chunk: bytes, chunk_index: int, cached: bool) -> None: + def write_chunk(self, chunk: bytes, chunk_index: int, + cached: bool) -> None: if self._internalStream.is_closed(): return self._session._LOGGER.debug( "Chunk {}/{} completed, cached: {}, stream: {}".format( - chunk_index + 1, self._chunks, cached, self.describe() - ) - ) + chunk_index + 1, self._chunks, cached, self.describe())) self._buffer[chunk_index] = self._audioDecrypt.decrypt_chunk( - chunk_index, chunk - ) + chunk_index, chunk) self._internalStream.notify_chunk_available(chunk_index) def stream(self) -> AbsChunkedInputStream: @@ -266,7 +261,8 @@ class CdnManager: def describe(self) -> str: if self._streamId.is_episode(): - return "episode_gid: {}".format(self._streamId.get_episode_gid()) + return "episode_gid: {}".format( + self._streamId.get_episode_gid()) return "file_id: {}".format(self._streamId.get_file_id()) def decrypt_time_ms(self) -> int: @@ -276,9 +272,10 @@ class CdnManager: resp = self.request(index) self.write_chunk(resp._buffer, index, False) - def request( - self, chunk: int = None, range_start: int = None, range_end: int = None - ) -> CdnManager.InternalResponse: + def request(self, + chunk: int = None, + range_start: int = None, + range_end: int = None) -> CdnManager.InternalResponse: if chunk is None and range_start is None and range_end is None: raise TypeError() @@ -288,7 +285,9 @@ class CdnManager: resp = self._session.client().get( self._cdnUrl._url, - headers={"Range": "bytes={}-{}".format(range_start, range_end)}, + headers={ + "Range": "bytes={}-{}".format(range_start, range_end) + }, ) if resp.status_code != 206: @@ -324,21 +323,16 @@ class CdnManager: def request_chunk_from_stream(self, index: int) -> None: self.streamer._executorService.submit( - lambda: self.streamer.request_chunk(index) - ) + lambda: self.streamer.request_chunk(index)) def stream_read_halted(self, chunk: int, _time: int) -> None: if self.streamer._haltListener is not None: self.streamer._executorService.submit( lambda: self.streamer._haltListener.stream_read_halted( - chunk, _time - ) - ) + chunk, _time)) def stream_read_resumed(self, chunk: int, _time: int) -> None: if self.streamer._haltListener is not None: self.streamer._executorService.submit( - lambda: self.streamer._haltListener.stream_read_resumed( - chunk, _time - ) - ) + lambda: self.streamer._haltListener. + stream_read_resumed(chunk, _time)) diff --git a/librespot/core/Session.py b/librespot/core/Session.py index 2f47c25..c86f78a 100644 --- a/librespot/core/Session.py +++ b/librespot/core/Session.py @@ -47,266 +47,264 @@ from librespot.Version import Version class Session(Closeable, SubListener, DealerClient.MessageListener): _LOGGER: logging = logging.getLogger(__name__) - _serverKey: bytes = bytes( - [ - 0xAC, - 0xE0, - 0x46, - 0x0B, - 0xFF, - 0xC2, - 0x30, - 0xAF, - 0xF4, - 0x6B, - 0xFE, - 0xC3, - 0xBF, - 0xBF, - 0x86, - 0x3D, - 0xA1, - 0x91, - 0xC6, - 0xCC, - 0x33, - 0x6C, - 0x93, - 0xA1, - 0x4F, - 0xB3, - 0xB0, - 0x16, - 0x12, - 0xAC, - 0xAC, - 0x6A, - 0xF1, - 0x80, - 0xE7, - 0xF6, - 0x14, - 0xD9, - 0x42, - 0x9D, - 0xBE, - 0x2E, - 0x34, - 0x66, - 0x43, - 0xE3, - 0x62, - 0xD2, - 0x32, - 0x7A, - 0x1A, - 0x0D, - 0x92, - 0x3B, - 0xAE, - 0xDD, - 0x14, - 0x02, - 0xB1, - 0x81, - 0x55, - 0x05, - 0x61, - 0x04, - 0xD5, - 0x2C, - 0x96, - 0xA4, - 0x4C, - 0x1E, - 0xCC, - 0x02, - 0x4A, - 0xD4, - 0xB2, - 0x0C, - 0x00, - 0x1F, - 0x17, - 0xED, - 0xC2, - 0x2F, - 0xC4, - 0x35, - 0x21, - 0xC8, - 0xF0, - 0xCB, - 0xAE, - 0xD2, - 0xAD, - 0xD7, - 0x2B, - 0x0F, - 0x9D, - 0xB3, - 0xC5, - 0x32, - 0x1A, - 0x2A, - 0xFE, - 0x59, - 0xF3, - 0x5A, - 0x0D, - 0xAC, - 0x68, - 0xF1, - 0xFA, - 0x62, - 0x1E, - 0xFB, - 0x2C, - 0x8D, - 0x0C, - 0xB7, - 0x39, - 0x2D, - 0x92, - 0x47, - 0xE3, - 0xD7, - 0x35, - 0x1A, - 0x6D, - 0xBD, - 0x24, - 0xC2, - 0xAE, - 0x25, - 0x5B, - 0x88, - 0xFF, - 0xAB, - 0x73, - 0x29, - 0x8A, - 0x0B, - 0xCC, - 0xCD, - 0x0C, - 0x58, - 0x67, - 0x31, - 0x89, - 0xE8, - 0xBD, - 0x34, - 0x80, - 0x78, - 0x4A, - 0x5F, - 0xC9, - 0x6B, - 0x89, - 0x9D, - 0x95, - 0x6B, - 0xFC, - 0x86, - 0xD7, - 0x4F, - 0x33, - 0xA6, - 0x78, - 0x17, - 0x96, - 0xC9, - 0xC3, - 0x2D, - 0x0D, - 0x32, - 0xA5, - 0xAB, - 0xCD, - 0x05, - 0x27, - 0xE2, - 0xF7, - 0x10, - 0xA3, - 0x96, - 0x13, - 0xC4, - 0x2F, - 0x99, - 0xC0, - 0x27, - 0xBF, - 0xED, - 0x04, - 0x9C, - 0x3C, - 0x27, - 0x58, - 0x04, - 0xB6, - 0xB2, - 0x19, - 0xF9, - 0xC1, - 0x2F, - 0x02, - 0xE9, - 0x48, - 0x63, - 0xEC, - 0xA1, - 0xB6, - 0x42, - 0xA0, - 0x9D, - 0x48, - 0x25, - 0xF8, - 0xB3, - 0x9D, - 0xD0, - 0xE8, - 0x6A, - 0xF9, - 0x48, - 0x4D, - 0xA1, - 0xC2, - 0xBA, - 0x86, - 0x30, - 0x42, - 0xEA, - 0x9D, - 0xB3, - 0x08, - 0x6C, - 0x19, - 0x0E, - 0x48, - 0xB3, - 0x9D, - 0x66, - 0xEB, - 0x00, - 0x06, - 0xA2, - 0x5A, - 0xEE, - 0xA1, - 0x1B, - 0x13, - 0x87, - 0x3C, - 0xD7, - 0x19, - 0xE6, - 0x55, - 0xBD, - ] - ) + _serverKey: bytes = bytes([ + 0xAC, + 0xE0, + 0x46, + 0x0B, + 0xFF, + 0xC2, + 0x30, + 0xAF, + 0xF4, + 0x6B, + 0xFE, + 0xC3, + 0xBF, + 0xBF, + 0x86, + 0x3D, + 0xA1, + 0x91, + 0xC6, + 0xCC, + 0x33, + 0x6C, + 0x93, + 0xA1, + 0x4F, + 0xB3, + 0xB0, + 0x16, + 0x12, + 0xAC, + 0xAC, + 0x6A, + 0xF1, + 0x80, + 0xE7, + 0xF6, + 0x14, + 0xD9, + 0x42, + 0x9D, + 0xBE, + 0x2E, + 0x34, + 0x66, + 0x43, + 0xE3, + 0x62, + 0xD2, + 0x32, + 0x7A, + 0x1A, + 0x0D, + 0x92, + 0x3B, + 0xAE, + 0xDD, + 0x14, + 0x02, + 0xB1, + 0x81, + 0x55, + 0x05, + 0x61, + 0x04, + 0xD5, + 0x2C, + 0x96, + 0xA4, + 0x4C, + 0x1E, + 0xCC, + 0x02, + 0x4A, + 0xD4, + 0xB2, + 0x0C, + 0x00, + 0x1F, + 0x17, + 0xED, + 0xC2, + 0x2F, + 0xC4, + 0x35, + 0x21, + 0xC8, + 0xF0, + 0xCB, + 0xAE, + 0xD2, + 0xAD, + 0xD7, + 0x2B, + 0x0F, + 0x9D, + 0xB3, + 0xC5, + 0x32, + 0x1A, + 0x2A, + 0xFE, + 0x59, + 0xF3, + 0x5A, + 0x0D, + 0xAC, + 0x68, + 0xF1, + 0xFA, + 0x62, + 0x1E, + 0xFB, + 0x2C, + 0x8D, + 0x0C, + 0xB7, + 0x39, + 0x2D, + 0x92, + 0x47, + 0xE3, + 0xD7, + 0x35, + 0x1A, + 0x6D, + 0xBD, + 0x24, + 0xC2, + 0xAE, + 0x25, + 0x5B, + 0x88, + 0xFF, + 0xAB, + 0x73, + 0x29, + 0x8A, + 0x0B, + 0xCC, + 0xCD, + 0x0C, + 0x58, + 0x67, + 0x31, + 0x89, + 0xE8, + 0xBD, + 0x34, + 0x80, + 0x78, + 0x4A, + 0x5F, + 0xC9, + 0x6B, + 0x89, + 0x9D, + 0x95, + 0x6B, + 0xFC, + 0x86, + 0xD7, + 0x4F, + 0x33, + 0xA6, + 0x78, + 0x17, + 0x96, + 0xC9, + 0xC3, + 0x2D, + 0x0D, + 0x32, + 0xA5, + 0xAB, + 0xCD, + 0x05, + 0x27, + 0xE2, + 0xF7, + 0x10, + 0xA3, + 0x96, + 0x13, + 0xC4, + 0x2F, + 0x99, + 0xC0, + 0x27, + 0xBF, + 0xED, + 0x04, + 0x9C, + 0x3C, + 0x27, + 0x58, + 0x04, + 0xB6, + 0xB2, + 0x19, + 0xF9, + 0xC1, + 0x2F, + 0x02, + 0xE9, + 0x48, + 0x63, + 0xEC, + 0xA1, + 0xB6, + 0x42, + 0xA0, + 0x9D, + 0x48, + 0x25, + 0xF8, + 0xB3, + 0x9D, + 0xD0, + 0xE8, + 0x6A, + 0xF9, + 0x48, + 0x4D, + 0xA1, + 0xC2, + 0xBA, + 0x86, + 0x30, + 0x42, + 0xEA, + 0x9D, + 0xB3, + 0x08, + 0x6C, + 0x19, + 0x0E, + 0x48, + 0xB3, + 0x9D, + 0x66, + 0xEB, + 0x00, + 0x06, + 0xA2, + 0x5A, + 0xEE, + 0xA1, + 0x1B, + 0x13, + 0x87, + 0x3C, + 0xD7, + 0x19, + 0xE6, + 0x55, + 0xBD, + ]) _keys: DiffieHellman = None _inner: Session.Inner = None _scheduler: sched.scheduler = sched.scheduler(time.time) @@ -344,9 +342,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self._conn = Session.ConnectionHolder.create(addr, inner.conf) self._client = Session._create_client(self._inner.conf) - self._LOGGER.info( - "Created new session! device_id: {}, ap: {}".format(inner.device_id, addr) - ) + self._LOGGER.info("Created new session! device_id: {}, ap: {}".format( + inner.device_id, addr)) @staticmethod def _create_client(conf: Session.Configuration) -> requests.Session: @@ -388,12 +385,12 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): client_hello = Keyexchange.ClientHello( build_info=Version.standard_build_info(), - 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_array(), server_keys_known=1 - ), - ), + gc=self._keys.public_key_array(), server_keys_known=1), ), client_nonce=nonce, padding=bytes([0x1E]), ) @@ -422,22 +419,21 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): ap_response_message.ParseFromString(buffer) shared_key = Utils.to_byte_array( self._keys.compute_shared_key( - ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs - ) - ) + ap_response_message.challenge.login_crypto_challenge. + diffie_hellman.gs)) # Check gs_signature rsa = RSA.construct((int.from_bytes(self._serverKey, "big"), 65537)) pkcs1_v1_5 = PKCS1_v1_5.new(rsa) sha1 = SHA1.new() - sha1.update( - ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs - ) + sha1.update(ap_response_message.challenge.login_crypto_challenge. + diffie_hellman.gs) # noinspection PyTypeChecker if not pkcs1_v1_5.verify( - sha1, - ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs_signature, + sha1, + ap_response_message.challenge.login_crypto_challenge. + diffie_hellman.gs_signature, ): raise RuntimeError("Failed signature check!") @@ -460,14 +456,13 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): client_response_plaintext = Keyexchange.ClientResponsePlaintext( login_crypto_response=Keyexchange.LoginCryptoResponseUnion( diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse( - hmac=challenge - ) - ), + hmac=challenge)), pow_response=Keyexchange.PoWResponseUnion(), crypto_response=Keyexchange.CryptoResponseUnion(), ) - client_response_plaintext_bytes = client_response_plaintext.SerializeToString() + client_response_plaintext_bytes = client_response_plaintext.SerializeToString( + ) length = 4 + len(client_response_plaintext_bytes) self._conn.write_int(length) self._conn.write(client_response_plaintext_bytes) @@ -477,12 +472,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self._conn.set_timeout(1) scrap = self._conn.read(4) if 4 == len(scrap): - length = ( - (scrap[0] << 24) - | (scrap[1] << 16) - | (scrap[2] << 8) - | (scrap[3] & 0xFF) - ) + length = ((scrap[0] << 24) + | (scrap[1] << 16) + | (scrap[2] << 8) + | (scrap[3] & 0xFF)) payload = self._conn.read(length - 4) failed = Keyexchange.APResponseMessage() failed.ParseFromString(payload) @@ -499,7 +492,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self._LOGGER.info("Connection successfully!") - def _authenticate(self, credentials: Authentication.LoginCredentials) -> None: + def _authenticate(self, + credentials: Authentication.LoginCredentials) -> None: self._authenticate_partial(credentials, False) with self._authLock: @@ -509,7 +503,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self._channelManager = ChannelManager(self) self._api = ApiClient.ApiClient(self) self._cdnManager = CdnManager(self) - self._contentFeeder = PlayableContentFeeder.PlayableContentFeeder(self) + self._contentFeeder = PlayableContentFeeder.PlayableContentFeeder( + self) self._cacheManager = CacheManager(self) self._dealer = DealerClient(self) self._search = SearchManager.SearchManager(self) @@ -522,15 +517,15 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): # TimeProvider.init(self) self._dealer.connect() - self._LOGGER.info( - "Authenticated as {}!".format(self._apWelcome.canonical_username) - ) + self._LOGGER.info("Authenticated as {}!".format( + self._apWelcome.canonical_username)) self.mercury().interested_in("spotify:user:attributes:update", self) - self.dealer().add_message_listener(self, "hm://connect-state/v1/connect/logout") + self.dealer().add_message_listener( + self, "hm://connect-state/v1/connect/logout") - def _authenticate_partial( - self, credentials: Authentication.LoginCredentials, remove_lock: bool - ) -> None: + def _authenticate_partial(self, + credentials: Authentication.LoginCredentials, + remove_lock: bool) -> None: if self._cipherPair is None: raise RuntimeError("Connection not established!") @@ -545,9 +540,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): version_string=Version.version_string(), ) - self._send_unchecked( - Packet.Type.login, client_response_encrypted.SerializeToString() - ) + self._send_unchecked(Packet.Type.login, + client_response_encrypted.SerializeToString()) packet = self._cipherPair.receive_encoded(self._conn) if packet.is_cmd(Packet.Type.ap_welcome): @@ -563,7 +557,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): preferred_locale += bytes([0x00, 0x00, 0x10, 0x00, 0x02]) preferred_locale += "preferred-locale".encode() preferred_locale += self._inner.preferred_locale.encode() - self._send_unchecked(Packet.Type.preferred_locale, preferred_locale) + self._send_unchecked(Packet.Type.preferred_locale, + preferred_locale) if remove_lock: with self._authLock: @@ -573,8 +568,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): if self._inner.conf.store_credentials: reusable = self._apWelcome.reusable_auth_credentials reusable_type = Authentication.AuthenticationType.Name( - self._apWelcome.reusable_auth_credentials_type - ) + self._apWelcome.reusable_auth_credentials_type) if self._inner.conf.stored_credentials_file is None: raise TypeError() @@ -597,9 +591,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): raise RuntimeError("Unknown CMD 0x" + packet.cmd.hex()) def close(self) -> None: - 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 @@ -652,9 +645,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): listener.on_closed() self._closeListeners: typing.List[Session.CloseListener] = [] - self._reconnectionListeners: typing.List[Session.ReconnectionListener] = [] + self._reconnectionListeners: typing.List[ + Session.ReconnectionListener] = [] - 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 _send_unchecked(self, cmd: bytes, payload: bytes) -> None: self._cipherPair.send_encoded(self._conn, cmd, payload) @@ -797,8 +792,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self._receiver.stop() self._conn = Session.ConnectionHolder.create( - ApResolver.get_random_accesspoint(), self._inner.conf - ) + ApResolver.get_random_accesspoint(), self._inner.conf) self._connect() self._authenticate_partial( Authentication.LoginCredentials( @@ -809,9 +803,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): True, ) - self._LOGGER.info( - "Re-authenticated as {}!".format(self._apWelcome.canonical_username) - ) + self._LOGGER.info("Re-authenticated as {}!".format( + self._apWelcome.canonical_username)) with self._reconnectionListenersLock: for listener in self._reconnectionListeners: @@ -821,11 +814,13 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): if listener not in self._closeListeners: self._closeListeners.append(listener) - def add_reconnection_listener(self, listener: ReconnectionListener) -> None: + def add_reconnection_listener(self, + listener: ReconnectionListener) -> None: if listener not in self._reconnectionListeners: self._reconnectionListeners.append(listener) - def remove_reconnection_listener(self, listener: ReconnectionListener) -> None: + def remove_reconnection_listener(self, + listener: ReconnectionListener) -> None: self._reconnectionListeners.remove(listener) def _parse_product_info(self, data) -> None: @@ -842,14 +837,12 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): for i in range(len(product)): self._userAttributes[product[i].tag] = product[i].text - self._LOGGER.debug("Parsed product info: {}".format(self._userAttributes)) + self._LOGGER.debug("Parsed product info: {}".format( + self._userAttributes)) def get_user_attribute(self, key: str, fallback: str = None) -> str: - return ( - self._userAttributes.get(key) - if self._userAttributes.get(key) is not None - else fallback - ) + return (self._userAttributes.get(key) + if self._userAttributes.get(key) is not None else fallback) def event(self, resp: MercuryClient.Response) -> None: if resp.uri == "spotify:user:attributes:update": @@ -858,13 +851,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): for pair in attributes_update.pairs_list: self._userAttributes[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 on_message( - self, uri: str, headers: typing.Dict[str, str], payload: bytes - ) -> None: + def on_message(self, uri: str, headers: typing.Dict[str, str], + payload: bytes) -> None: if uri == "hm://connect-state/v1/connect/logout": self.close() @@ -898,9 +889,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): 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 Utils.random_hex_string(40) - ) + self.device_id = (device_id if device_id is not None else + Utils.random_hex_string(40)) class AbsBuilder: conf = None @@ -934,8 +924,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): 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 @@ -945,7 +934,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): def stored(self): pass - def stored_file(self, stored_credentials: str = None) -> Session.Builder: + def stored_file(self, + stored_credentials: str = None) -> Session.Builder: if stored_credentials is None: stored_credentials = self.conf.stored_credentials_file if os.path.isfile(stored_credentials): @@ -957,7 +947,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): 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"]), ) @@ -1061,75 +1052,77 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): # 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 = None def set_proxy_enabled( - self, proxy_enabled: bool - ) -> Session.Configuration.Builder: + self, + proxy_enabled: bool) -> Session.Configuration.Builder: self.proxyEnabled = proxy_enabled return self def set_proxy_type( - self, proxy_type: Proxy.Type - ) -> Session.Configuration.Builder: + self, + proxy_type: Proxy.Type) -> Session.Configuration.Builder: self.proxyType = proxy_type return self def set_proxy_address( - self, proxy_address: str - ) -> Session.Configuration.Builder: + self, proxy_address: str) -> Session.Configuration.Builder: self.proxyAddress = proxy_address return self - def set_proxy_auth(self, proxy_auth: bool) -> Session.Configuration.Builder: + def set_proxy_auth( + self, proxy_auth: bool) -> Session.Configuration.Builder: self.proxyAuth = proxy_auth return self def set_proxy_username( - self, proxy_username: str - ) -> Session.Configuration.Builder: + self, + proxy_username: str) -> Session.Configuration.Builder: self.proxyUsername = proxy_username return self def set_proxy_password( - self, proxy_password: str - ) -> Session.Configuration.Builder: + self, + proxy_password: str) -> Session.Configuration.Builder: self.proxyPassword = proxy_password return self def set_cache_enabled( - self, cache_enabled: bool - ) -> Session.Configuration.Builder: + self, + cache_enabled: bool) -> Session.Configuration.Builder: 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: self.cache_dir = cache_dir 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: self.do_cache_clean_up = do_cache_clean_up return self def set_store_credentials( - self, store_credentials: bool - ) -> Session.Configuration.Builder: + self, + store_credentials: bool) -> Session.Configuration.Builder: self.store_credentials = store_credentials return self def set_stored_credential_file( - self, stored_credential_file: str + self, stored_credential_file: str ) -> Session.Configuration.Builder: self.stored_credentials_file = stored_credential_file return self def set_retry_on_chunk_error( - self, retry_on_chunk_error: bool + self, retry_on_chunk_error: bool ) -> Session.Configuration.Builder: self.retry_on_chunk_error = retry_on_chunk_error return self @@ -1153,7 +1146,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): 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 Accumulator: buffer: bytes = bytes() @@ -1180,7 +1174,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self.sock = sock @staticmethod - def create(addr: str, conf: Session.Configuration) -> Session.ConnectionHolder: + def create(addr: str, + conf: Session.Configuration) -> Session.ConnectionHolder: ap_addr = addr.split(":")[0] ap_port = int(addr.split(":")[1]) if not conf.proxyEnabled or conf.proxyType is Proxy.Type.DIRECT: @@ -1192,9 +1187,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): sock = socket.socket() sock.connect((conf.proxyAddress, conf.proxyPort)) - sock.send("CONNECT {}:{} HTTP/1.0\n".format(ap_addr, ap_port).encode()) + sock.send("CONNECT {}:{} HTTP/1.0\n".format(ap_addr, + ap_port).encode()) if conf.proxyAuth: - sock.send("Proxy-Authorization: {}\n".format(None).encode()) + sock.send( + "Proxy-Authorization: {}\n".format(None).encode()) sock.send(b"\n") @@ -1259,21 +1256,18 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): try: # noinspection PyProtectedMember packet = self.session._cipherPair.receive_encoded( - self.session._conn - ) + self.session._conn) cmd = Packet.Type.parse(packet.cmd) if cmd is None: self.session._LOGGER.info( - "Skipping unknown command cmd: 0x{}, payload: {}".format( - Utils.bytes_to_hex(packet.cmd), packet.payload - ) - ) + "Skipping unknown command cmd: 0x{}, payload: {}". + format(Utils.bytes_to_hex(packet.cmd), + packet.payload)) continue except RuntimeError as ex: if self.running: self.session._LOGGER.fatal( - "Failed reading packet! {}".format(ex) - ) + "Failed reading packet! {}".format(ex)) # noinspection PyProtectedMember self.session._reconnect() break @@ -1284,18 +1278,17 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): # noinspection PyProtectedMember if self.session._scheduledReconnect is not None: # noinspection PyProtectedMember - self.session._scheduler.cancel(self.session._scheduledReconnect) + self.session._scheduler.cancel( + self.session._scheduledReconnect) def anonymous(): self.session._LOGGER.warning( - "Socket timed out. Reconnecting..." - ) + "Socket timed out. Reconnecting...") self.session._reconnect() # noinspection PyProtectedMember 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) continue if cmd == Packet.Type.pong_ack: @@ -1303,8 +1296,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): if cmd == Packet.Type.country_code: self.session.country_code = packet.payload.decode() self.session._LOGGER.info( - "Received country_code: {}".format(self.session.country_code) - ) + "Received country_code: {}".format( + self.session.country_code)) continue if cmd == Packet.Type.license_version: license_version = BytesInputStream(packet.payload) @@ -1313,40 +1306,33 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): 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)) continue if cmd == Packet.Type.unknown_0x10: - self.session._LOGGER.debug( - "Received 0x10: {}".format(Utils.bytes_to_hex(packet.payload)) - ) + self.session._LOGGER.debug("Received 0x10: {}".format( + Utils.bytes_to_hex(packet.payload))) continue - if ( - cmd == Packet.Type.mercury_sub - or cmd == Packet.Type.mercury_unsub - or cmd == Packet.Type.mercury_event - or cmd == Packet.Type.mercury_req - ): + if (cmd == Packet.Type.mercury_sub + or cmd == Packet.Type.mercury_unsub + or cmd == Packet.Type.mercury_event + or cmd == Packet.Type.mercury_req): self.session.mercury().dispatch(packet) continue if cmd == Packet.Type.aes_key or cmd == Packet.Type.aes_key_error: self.session.audio_key().dispatch(packet) continue - if ( - cmd == Packet.Type.channel_error - or cmd == Packet.Type.stream_chunk_res - ): + if (cmd == Packet.Type.channel_error + or cmd == Packet.Type.stream_chunk_res): self.session.channel().dispatch(packet) continue if cmd == Packet.Type.product_info: # noinspection PyProtectedMember self.session._parse_product_info(packet.payload) continue - self.session._LOGGER.info("Skipping {}".format(Utils.bytes_to_hex(cmd))) + self.session._LOGGER.info("Skipping {}".format( + Utils.bytes_to_hex(cmd))) self.session._LOGGER.debug("Session.Receiver stopped")