From fc57a76aa0728e21f01ab103bc63eeae990145e9 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Fri, 21 May 2021 03:34:15 +0000 Subject: [PATCH 1/6] Format code with yapf This commit fixes the style issues introduced in 4635bf8 according to the output from yapf. Details: https://deepsource.io/gh/kokarare1212/librespot-python/transform/556eb7b2-7632-467e-b1b4-8e463d91a708/ --- examples/player.py | 43 +++++++++++++++--------- librespot/audio/AbsChunkedInputStream.py | 3 +- librespot/audio/cdn/CdnFeedHelper.py | 3 +- librespot/audio/cdn/CdnManager.py | 3 +- librespot/core/Session.py | 3 +- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/examples/player.py b/examples/player.py index 6b880e1..ec8b5c0 100644 --- a/examples/player.py +++ b/examples/player.py @@ -30,10 +30,15 @@ def client(): if args[0] == "exit" or args[0] == "quit": 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]) - track_url_search = re.search(r"^(https?://)?open.spotify.com/track/(?P[0-9a-zA-Z]{22})(\?si=.+?)?$", args[1]) + track_uri_search = re.search( + 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": @@ -51,11 +56,18 @@ def client(): wait() if (args[0] == "s" or args[0] == "search") and len(args) <= 2: 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"}, headers={"Authorization": "Bearer %s" % token}) + resp = requests.get("https://api.spotify.com/v1/search", { + "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: @@ -92,11 +104,11 @@ 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) - ffplay = subprocess.Popen(["ffplay", "-"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, + stream = session.content_feeder().load( + track_id, VorbisOnlyAudioQuality(AudioQuality.VERY_HIGH), False, None) + ffplay = subprocess.Popen(["ffplay", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) while True: byte = stream.input_stream.stream().read() @@ -107,12 +119,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 61de4dd..c2e867b 100644 --- a/librespot/audio/AbsChunkedInputStream.py +++ b/librespot/audio/AbsChunkedInputStream.py @@ -63,8 +63,7 @@ class AbsChunkedInputStream(InputStream, HaltListener): raise IOError("Stream is closed!") self._pos = where - self.check_availability(int(self._pos / (128 * 1024)), - False, False) + self.check_availability(int(self._pos / (128 * 1024)), False, False) def skip(self, n: int) -> int: if n < 0: diff --git a/librespot/audio/cdn/CdnFeedHelper.py b/librespot/audio/cdn/CdnFeedHelper.py index bcfb7fe..f9163ed 100644 --- a/librespot/audio/cdn/CdnFeedHelper.py +++ b/librespot/audio/cdn/CdnFeedHelper.py @@ -31,7 +31,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( diff --git a/librespot/audio/cdn/CdnManager.py b/librespot/audio/cdn/CdnManager.py index a964669..d4b589c 100644 --- a/librespot/audio/cdn/CdnManager.py +++ b/librespot/audio/cdn/CdnManager.py @@ -159,7 +159,8 @@ class CdnManager: else: self._expiration = -1 - class Streamer(GeneralAudioStream.GeneralAudioStream, GeneralWritableStream.GeneralWritableStream): + class Streamer(GeneralAudioStream.GeneralAudioStream, + GeneralWritableStream.GeneralWritableStream): _session: Session = None _streamId: StreamId = None _executorService = concurrent.futures.ThreadPoolExecutor() diff --git a/librespot/core/Session.py b/librespot/core/Session.py index e4751e2..ed54cb2 100644 --- a/librespot/core/Session.py +++ b/librespot/core/Session.py @@ -245,7 +245,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) From 1e0e47cced56b0099d118eb8f6f29e2851872805 Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Fri, 21 May 2021 03:39:59 +0000 Subject: [PATCH 2/6] Restyled by autopep8 --- examples/player.py | 2 +- librespot/core/Session.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/player.py b/examples/player.py index ec8b5c0..7a25647 100644 --- a/examples/player.py +++ b/examples/player.py @@ -62,7 +62,7 @@ def client(): "q": cmd[2:], "type": "track" }, - headers={"Authorization": "Bearer %s" % token}) + headers={"Authorization": "Bearer %s" % token}) i = 1 tracks = resp.json()["tracks"]["items"] for track in tracks: diff --git a/librespot/core/Session.py b/librespot/core/Session.py index ed54cb2..b8c3437 100644 --- a/librespot/core/Session.py +++ b/librespot/core/Session.py @@ -1023,17 +1023,17 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): 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: + 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: + 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: + cmd == Packet.Type.stream_chunk_res: self.session.channel().dispatch(packet) continue if cmd == Packet.Type.product_info: From 5132c9e02322707840483835cec29c98339aff50 Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Fri, 21 May 2021 03:40:40 +0000 Subject: [PATCH 3/6] Restyled by black --- examples/player.py | 61 ++- librespot/audio/AbsChunkedInputStream.py | 31 +- librespot/audio/cdn/CdnFeedHelper.py | 64 ++- librespot/audio/cdn/CdnManager.py | 149 ++++-- librespot/core/Session.py | 640 ++++++++++++++++------- 5 files changed, 648 insertions(+), 297 deletions(-) diff --git a/examples/player.py b/examples/player.py index 7a25647..cd506eb 100644 --- a/examples/player.py +++ b/examples/player.py @@ -31,14 +31,18 @@ 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]) + 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": @@ -56,18 +60,22 @@ def client(): wait() if (args[0] == "s" or args[0] == "search") and len(args) <= 2: 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" - }, - headers={"Authorization": "Bearer %s" % token}) + resp = requests.get( + "https://api.spotify.com/v1/search", + {"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: @@ -105,11 +113,14 @@ 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) - ffplay = subprocess.Popen(["ffplay", "-"], - stdin=subprocess.PIPE, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + track_id, VorbisOnlyAudioQuality(AudioQuality.VERY_HIGH), False, None + ) + ffplay = subprocess.Popen( + ["ffplay", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) while True: byte = stream.input_stream.stream().read() if byte == -1: @@ -119,11 +130,13 @@ 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 c2e867b..53efa7e 100644 --- a/librespot/audio/AbsChunkedInputStream.py +++ b/librespot/audio/AbsChunkedInputStream.py @@ -21,9 +21,7 @@ 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: @@ -108,10 +106,13 @@ 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 @@ -145,10 +146,7 @@ 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): @@ -158,8 +156,9 @@ 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,8 +173,7 @@ 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 @@ -223,4 +221,5 @@ 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 f9163ed..7a4efb0 100644 --- a/librespot/audio/cdn/CdnFeedHelper.py +++ b/librespot/audio/cdn/CdnFeedHelper.py @@ -17,10 +17,14 @@ class CdnFeedHelper: return random.choice(resp.cdnurl) @staticmethod - def load_track(session: Session, track: Metadata.Track, file: Metadata.AudioFile, - resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str], - preload: bool, halt_listener: HaltListener)\ - -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream: + def load_track( + session: Session, + track: Metadata.Track, + file: Metadata.AudioFile, + resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str], + preload: bool, + halt_listener: HaltListener, + ) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream: if type(resp_or_url) is str: url = resp_or_url else: @@ -31,19 +35,21 @@ class CdnFeedHelper: streamer = session.cdn().stream_file(file, key, url, halt_listener) input_stream = streamer.stream() - normalization_data = NormalizationData.NormalizationData.read( - input_stream) - if input_stream.skip(0xa7) != 0xa7: + normalization_data = NormalizationData.NormalizationData.read(input_stream) + if input_stream.skip(0xA7) != 0xA7: raise IOError("Couldn't skip 0xa7 bytes!") return PlayableContentFeeder.PlayableContentFeeder.LoadedStream( - track, streamer, normalization_data, + track, + 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) @@ -51,21 +57,27 @@ 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)) + episode, + streamer, + None, + PlayableContentFeeder.PlayableContentFeeder.Metrics(None, False, -1), + ) @staticmethod def load_episode( - session: Session, episode: Metadata.Episode, file: Metadata.AudioFile, - resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, - str], halt_listener: HaltListener + session: Session, + episode: Metadata.Episode, + file: Metadata.AudioFile, + resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str], + halt_listener: HaltListener, ) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream: if type(resp_or_url) is str: url = resp_or_url @@ -78,9 +90,13 @@ class CdnFeedHelper: streamer = session.cdn().stream_file(file, key, url, halt_listener) input_stream = streamer.stream() normalization_data = NormalizationData.read(input_stream) - if input_stream.skip(0xa7) != 0xa7: + if input_stream.skip(0xA7) != 0xA7: raise IOError("Couldn't skip 0xa7 bytes!") return PlayableContentFeeder.PlayableContentFeeder.LoadedStream( - episode, streamer, normalization_data, + episode, + 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 d4b589c..c582033 100644 --- a/librespot/audio/cdn/CdnManager.py +++ b/librespot/audio/cdn/CdnManager.py @@ -30,9 +30,11 @@ class CdnManager: self._session = session 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))) + 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)) + ) if resp.status_code != 200: raise IOError("{}".format(resp.status_code)) @@ -43,27 +45,45 @@ class CdnManager: return body - def stream_external_episode(self, episode: Metadata.Episode, - external_url: str, - halt_listener: HaltListener): - return CdnManager.Streamer(self._session, StreamId(episode), - SuperAudioFormat.MP3, - CdnManager.CdnUrl(self, None, external_url), - self._session.cache(), NoopAudioDecrypt(), - halt_listener) + def stream_external_episode( + self, episode: Metadata.Episode, external_url: str, halt_listener: HaltListener + ): + return CdnManager.Streamer( + self._session, + StreamId(episode), + SuperAudioFormat.MP3, + CdnManager.CdnUrl(self, None, external_url), + self._session.cache(), + NoopAudioDecrypt(), + halt_listener, + ) - def stream_file(self, file: Metadata.AudioFile, key: bytes, url: str, - halt_listener: HaltListener): - return CdnManager.Streamer(self._session, StreamId.StreamId(file), - SuperAudioFormat.get(file.format), - CdnManager.CdnUrl(self, file.file_id, url), - self._session.cache(), AesAudioDecrypt(key), - halt_listener) + def stream_file( + self, + file: Metadata.AudioFile, + key: bytes, + url: str, + halt_listener: HaltListener, + ): + return CdnManager.Streamer( + self._session, + StreamId.StreamId(file), + SuperAudioFormat.get(file.format), + CdnManager.CdnUrl(self, file.file_id, url), + self._session.cache(), + AesAudioDecrypt(key), + halt_listener, + ) def get_audio_url(self, file_id: bytes): resp = self._session.api().send( - "GET", "/storage-resolve/files/audio/interactive/{}".format( - Utils.bytes_to_hex(file_id)), None, None) + "GET", + "/storage-resolve/files/audio/interactive/{}".format( + Utils.bytes_to_hex(file_id) + ), + None, + None, + ) if resp.status_code != 200: raise IOError(resp.status_code) @@ -76,11 +96,13 @@ 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 @@ -140,7 +162,8 @@ 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 @@ -150,8 +173,10 @@ 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 @@ -159,8 +184,10 @@ class CdnManager: else: self._expiration = -1 - class Streamer(GeneralAudioStream.GeneralAudioStream, - GeneralWritableStream.GeneralWritableStream): + class Streamer( + GeneralAudioStream.GeneralAudioStream, + GeneralWritableStream.GeneralWritableStream, + ): _session: Session = None _streamId: StreamId = None _executorService = concurrent.futures.ThreadPoolExecutor() @@ -175,10 +202,16 @@ class CdnManager: _internalStream: CdnManager.Streamer.InternalStream = None _haltListener: HaltListener = None - def __init__(self, session: Session, stream_id: StreamId, - audio_format: SuperAudioFormat, cdn_url, - cache: CacheManager, audio_decrypt: AudioDecrypt, - halt_listener: HaltListener): + def __init__( + self, + session: Session, + stream_id: StreamId, + audio_format: SuperAudioFormat, + cdn_url, + cache: CacheManager, + audio_decrypt: AudioDecrypt, + halt_listener: HaltListener, + ): self._session = session self._streamId = stream_id self._audioFormat = audio_format @@ -186,39 +219,38 @@ 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: @@ -229,8 +261,7 @@ 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: @@ -240,10 +271,9 @@ 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() @@ -251,12 +281,10 @@ class CdnManager: range_start = ChannelManager.CHUNK_SIZE * chunk range_end = (chunk + 1) * ChannelManager.CHUNK_SIZE - 1 - resp = self._session.client().get(self._cdnUrl._url, - headers={ - "Range": - "bytes={}-{}".format( - range_start, range_end) - }) + resp = self._session.client().get( + self._cdnUrl._url, + headers={"Range": "bytes={}-{}".format(range_start, range_end)}, + ) if resp.status_code != 206: raise IOError(resp.status_code) @@ -291,16 +319,21 @@ 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 b8c3437..5bba924 100644 --- a/librespot/core/Session.py +++ b/librespot/core/Session.py @@ -31,30 +31,266 @@ import typing 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) @@ -92,8 +328,9 @@ 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: @@ -101,14 +338,16 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): if conf.proxyAuth and conf.proxyType is not Proxy.Type.DIRECT: if conf.proxyAuth: proxy_setting = [ - conf.proxyUsername, conf.proxyPassword, conf.proxyAddress, - conf.proxyPort + conf.proxyUsername, + conf.proxyPassword, + conf.proxyAddress, + conf.proxyPort, ] else: proxy_setting = [conf.proxyAddress, conf.proxyPort] client.proxies = { "http": "{}:{}@{}:{}".format(*proxy_setting), - "https": "{}:{}@{}:{}".format(*proxy_setting) + "https": "{}:{}@{}:{}".format(*proxy_setting), } return client @@ -119,7 +358,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): if (lo & 0x80) == 0: return lo hi = buffer[1] - return lo & 0x7f | hi << 7 + return lo & 0x7F | hi << 7 def client(self) -> requests.Session: return self._client @@ -133,14 +372,15 @@ 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])) + padding=bytes([0x1E]), + ) client_hello_bytes = client_hello.SerializeToString() length = 2 + 4 + len(client_hello_bytes) @@ -166,20 +406,23 @@ 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!") # Solve challenge @@ -201,12 +444,14 @@ 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( + crypto_response=Keyexchange.CryptoResponseUnion(), ) + + 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) @@ -216,8 +461,12 @@ 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) @@ -234,8 +483,7 @@ 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: @@ -245,8 +493,7 @@ 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) @@ -259,15 +506,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!") @@ -277,11 +524,14 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): os=Authentication.Os.OS_UNKNOWN, cpu_family=Authentication.CpuFamily.CPU_UNKNOWN, system_information_string=Version.system_info_string(), - device_id=self._inner.device_id), - version_string=Version.version_string()) + device_id=self._inner.device_id, + ), + 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): @@ -297,8 +547,7 @@ 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: @@ -308,7 +557,8 @@ 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() @@ -318,8 +568,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): { "username": self._apWelcome.canonical_username, "credentials": base64.b64encode(reusable).decode(), - "type": reusable_type - }, f) + "type": reusable_type, + }, + f, + ) elif packet.is_cmd(Packet.Type.auth_failure): ap_login_failed = Keyexchange.APLoginFailed() @@ -329,8 +581,9 @@ 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 @@ -383,11 +636,9 @@ 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) @@ -530,16 +781,21 @@ 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( typ=self._apWelcome.reusable_auth_credentials_type, username=self._apWelcome.canonical_username, - auth_data=self._apWelcome.reusable_auth_credentials), True) + auth_data=self._apWelcome.reusable_auth_credentials, + ), + 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: @@ -549,13 +805,11 @@ 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: @@ -572,12 +826,14 @@ 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": @@ -586,11 +842,13 @@ 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() @@ -612,18 +870,21 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): conf = None preferred_locale: str = None - def __init__(self, - device_type: Connect.DeviceType, - device_name: str, - preferred_locale: str, - conf: Session.Configuration, - device_id: str = None): + def __init__( + self, + device_type: Connect.DeviceType, + device_name: str, + preferred_locale: str, + conf: Session.Configuration, + device_id: str = None, + ): self.preferred_locale = preferred_locale 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 @@ -657,7 +918,8 @@ 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 @@ -667,8 +929,7 @@ 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): @@ -680,10 +941,10 @@ 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"])) + auth_data=base64.b64decode(obj["credentials"]), + ) except KeyError: pass @@ -693,7 +954,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): self.login_credentials = Authentication.LoginCredentials( username=username, typ=Authentication.AuthenticationType.AUTHENTICATION_USER_PASS, - auth_data=password.encode()) + auth_data=password.encode(), + ) return self def create(self) -> Session: @@ -701,10 +963,15 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): raise RuntimeError("You must select an authentication method.") session = Session( - Session.Inner(self.device_type, self.device_name, - self.preferred_locale, self.conf, - self.device_id), - ApResolver.get_random_accesspoint()) + Session.Inner( + self.device_type, + self.device_name, + self.preferred_locale, + self.conf, + self.device_id, + ), + ApResolver.get_random_accesspoint(), + ) session._connect() session._authenticate(self.login_credentials) return session @@ -731,12 +998,22 @@ class Session(Closeable, SubListener, DealerClient.MessageListener): # Fetching retry_on_chunk_error: bool - def __init__(self, proxy_enabled: bool, proxy_type: Proxy.Type, - proxy_address: str, proxy_port: int, proxy_auth: bool, - proxy_username: str, proxy_password: str, - cache_enabled: bool, cache_dir: str, - do_cache_clean_up: bool, store_credentials: bool, - stored_credentials_file: str, retry_on_chunk_error: bool): + def __init__( + self, + proxy_enabled: bool, + proxy_type: Proxy.Type, + proxy_address: str, + proxy_port: int, + proxy_auth: bool, + proxy_username: str, + proxy_password: str, + cache_enabled: bool, + cache_dir: str, + do_cache_clean_up: bool, + store_credentials: bool, + stored_credentials_file: str, + retry_on_chunk_error: bool, + ): self.proxyEnabled = proxy_enabled self.proxyType = proxy_type self.proxyAddress = proxy_address @@ -768,93 +1045,99 @@ 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 def build(self) -> Session.Configuration: return Session.Configuration( - self.proxyEnabled, self.proxyType, self.proxyAddress, - self.proxyPort, self.proxyAuth, self.proxyUsername, - self.proxyPassword, self.cache_enabled, self.cache_dir, - self.do_cache_clean_up, self.store_credentials, - self.stored_credentials_file, self.retry_on_chunk_error) + self.proxyEnabled, + self.proxyType, + self.proxyAddress, + self.proxyPort, + self.proxyAuth, + self.proxyUsername, + self.proxyPassword, + self.cache_enabled, + self.cache_dir, + self.do_cache_clean_up, + self.store_credentials, + self.stored_credentials_file, + self.retry_on_chunk_error, + ) 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() @@ -881,8 +1164,7 @@ 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: @@ -894,11 +1176,9 @@ 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") @@ -963,18 +1243,21 @@ 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 @@ -985,17 +1268,18 @@ 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: @@ -1003,8 +1287,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) @@ -1013,34 +1297,40 @@ 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: + 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") From a62f17c8c9b3bb2f36f4cb5e3e3d8d588e9d57b6 Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Fri, 21 May 2021 03:40:50 +0000 Subject: [PATCH 4/6] Restyled by isort --- examples/player.py | 14 ++++++----- librespot/audio/AbsChunkedInputStream.py | 5 ++-- librespot/audio/cdn/CdnFeedHelper.py | 11 +++++---- librespot/audio/cdn/CdnManager.py | 18 +++++++------- librespot/core/Session.py | 30 ++++++++++++++---------- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/examples/player.py b/examples/player.py index cd506eb..5d00a27 100644 --- a/examples/player.py +++ b/examples/player.py @@ -1,13 +1,15 @@ +import os +import platform +import re +import subprocess +import time + +import requests + from librespot.audio.decoders import AudioQuality from librespot.core import Session from librespot.metadata import TrackId from librespot.player.codecs import VorbisOnlyAudioQuality -import os -import platform -import re -import requests -import subprocess -import time quality: AudioQuality = AudioQuality.VERY_HIGH session: Session = None diff --git a/librespot/audio/AbsChunkedInputStream.py b/librespot/audio/AbsChunkedInputStream.py index 53efa7e..c730d82 100644 --- a/librespot/audio/AbsChunkedInputStream.py +++ b/librespot/audio/AbsChunkedInputStream.py @@ -1,10 +1,11 @@ -from librespot.audio.HaltListener import HaltListener -from librespot.standard.InputStream import InputStream import math import threading import time import typing +from librespot.audio.HaltListener import HaltListener +from librespot.standard.InputStream import InputStream + class AbsChunkedInputStream(InputStream, HaltListener): preload_ahead: typing.Final[int] = 3 diff --git a/librespot/audio/cdn/CdnFeedHelper.py b/librespot/audio/cdn/CdnFeedHelper.py index 7a4efb0..dbfaf2c 100644 --- a/librespot/audio/cdn/CdnFeedHelper.py +++ b/librespot/audio/cdn/CdnFeedHelper.py @@ -1,13 +1,16 @@ from __future__ import annotations -from librespot.audio import NormalizationData, PlayableContentFeeder, HaltListener -from librespot.common import Utils -from librespot.core import Session -from librespot.proto import Metadata, StorageResolve + import logging import random import time import typing +from librespot.audio import (HaltListener, NormalizationData, + PlayableContentFeeder) +from librespot.common import Utils +from librespot.core import Session +from librespot.proto import Metadata, StorageResolve + class CdnFeedHelper: _LOGGER: logging = logging.getLogger(__name__) diff --git a/librespot/audio/cdn/CdnManager.py b/librespot/audio/cdn/CdnManager.py index c582033..b4917f9 100644 --- a/librespot/audio/cdn/CdnManager.py +++ b/librespot/audio/cdn/CdnManager.py @@ -1,11 +1,5 @@ from __future__ import annotations -from librespot.audio.AbsChunkedInputStream import AbsChunkedInputStream -from librespot.audio import GeneralAudioStream, GeneralWritableStream, StreamId -from librespot.audio.decrypt import AesAudioDecrypt, NoopAudioDecrypt -from librespot.audio.format import SuperAudioFormat -from librespot.audio.storage import ChannelManager -from librespot.common import Utils -from librespot.proto import StorageResolve + import concurrent.futures import logging import math @@ -14,9 +8,17 @@ import time import typing import urllib.parse +from librespot.audio import GeneralAudioStream, GeneralWritableStream, StreamId +from librespot.audio.AbsChunkedInputStream import AbsChunkedInputStream +from librespot.audio.decrypt import AesAudioDecrypt, NoopAudioDecrypt +from librespot.audio.format import SuperAudioFormat +from librespot.audio.storage import ChannelManager +from librespot.common import Utils +from librespot.proto import StorageResolve + if typing.TYPE_CHECKING: - from librespot.audio.HaltListener import HaltListener from librespot.audio.decrypt.AudioDecrypt import AudioDecrypt + from librespot.audio.HaltListener import HaltListener from librespot.cache.CacheManager import CacheManager from librespot.core.Session import Session from librespot.proto import Metadata diff --git a/librespot/core/Session.py b/librespot/core/Session.py index 5bba924..c3ed487 100644 --- a/librespot/core/Session.py +++ b/librespot/core/Session.py @@ -1,13 +1,29 @@ from __future__ import annotations + +import base64 +import json +import logging +import os +import sched +import socket +import struct +import threading +import time +import typing + +import defusedxml.ElementTree +import requests from Crypto.Hash import HMAC, SHA1 from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 + from librespot.audio import AudioKeyManager, PlayableContentFeeder from librespot.audio.cdn import CdnManager from librespot.audio.storage import ChannelManager from librespot.cache import CacheManager from librespot.common.Utils import Utils -from librespot.core import ApResolver, EventService, SearchManager, TokenProvider +from librespot.core import (ApResolver, EventService, SearchManager, + TokenProvider) from librespot.crypto import CipherPair, DiffieHellman, Packet from librespot.dealer import ApiClient, DealerClient from librespot.mercury import MercuryClient, SubListener @@ -15,18 +31,6 @@ from librespot.proto import Authentication, Connect, Keyexchange from librespot.proto.ExplicitContentPubsub import UserAttributesUpdate from librespot.standard import BytesInputStream, Closeable, Proxy from librespot.Version import Version -import base64 -import defusedxml.ElementTree -import json -import logging -import os -import requests -import sched -import socket -import struct -import threading -import time -import typing class Session(Closeable, SubListener, DealerClient.MessageListener): From 7ac065fb4ba95965a106018ea721036863d73786 Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Fri, 21 May 2021 03:40:58 +0000 Subject: [PATCH 5/6] Restyled by reorder-python-imports --- librespot/audio/cdn/CdnFeedHelper.py | 8 +++++--- librespot/audio/cdn/CdnManager.py | 7 +++++-- librespot/core/Session.py | 30 +++++++++++++++++++--------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/librespot/audio/cdn/CdnFeedHelper.py b/librespot/audio/cdn/CdnFeedHelper.py index dbfaf2c..b0eca33 100644 --- a/librespot/audio/cdn/CdnFeedHelper.py +++ b/librespot/audio/cdn/CdnFeedHelper.py @@ -5,11 +5,13 @@ import random import time import typing -from librespot.audio import (HaltListener, NormalizationData, - PlayableContentFeeder) +from librespot.audio import HaltListener +from librespot.audio import NormalizationData +from librespot.audio import PlayableContentFeeder from librespot.common import Utils from librespot.core import Session -from librespot.proto import Metadata, StorageResolve +from librespot.proto import Metadata +from librespot.proto import StorageResolve class CdnFeedHelper: diff --git a/librespot/audio/cdn/CdnManager.py b/librespot/audio/cdn/CdnManager.py index b4917f9..6f3228a 100644 --- a/librespot/audio/cdn/CdnManager.py +++ b/librespot/audio/cdn/CdnManager.py @@ -8,9 +8,12 @@ import time import typing import urllib.parse -from librespot.audio import GeneralAudioStream, GeneralWritableStream, StreamId +from librespot.audio import GeneralAudioStream +from librespot.audio import GeneralWritableStream +from librespot.audio import StreamId from librespot.audio.AbsChunkedInputStream import AbsChunkedInputStream -from librespot.audio.decrypt import AesAudioDecrypt, NoopAudioDecrypt +from librespot.audio.decrypt import AesAudioDecrypt +from librespot.audio.decrypt import NoopAudioDecrypt from librespot.audio.format import SuperAudioFormat from librespot.audio.storage import ChannelManager from librespot.common import Utils diff --git a/librespot/core/Session.py b/librespot/core/Session.py index c3ed487..2f47c25 100644 --- a/librespot/core/Session.py +++ b/librespot/core/Session.py @@ -13,23 +13,35 @@ import typing import defusedxml.ElementTree import requests -from Crypto.Hash import HMAC, SHA1 +from Crypto.Hash import HMAC +from Crypto.Hash import SHA1 from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 -from librespot.audio import AudioKeyManager, PlayableContentFeeder +from librespot.audio import AudioKeyManager +from librespot.audio import PlayableContentFeeder from librespot.audio.cdn import CdnManager from librespot.audio.storage import ChannelManager from librespot.cache import CacheManager from librespot.common.Utils import Utils -from librespot.core import (ApResolver, EventService, SearchManager, - TokenProvider) -from librespot.crypto import CipherPair, DiffieHellman, Packet -from librespot.dealer import ApiClient, DealerClient -from librespot.mercury import MercuryClient, SubListener -from librespot.proto import Authentication, Connect, Keyexchange +from librespot.core import ApResolver +from librespot.core import EventService +from librespot.core import SearchManager +from librespot.core import TokenProvider +from librespot.crypto import CipherPair +from librespot.crypto import DiffieHellman +from librespot.crypto import Packet +from librespot.dealer import ApiClient +from librespot.dealer import DealerClient +from librespot.mercury import MercuryClient +from librespot.mercury import SubListener +from librespot.proto import Authentication +from librespot.proto import Connect +from librespot.proto import Keyexchange from librespot.proto.ExplicitContentPubsub import UserAttributesUpdate -from librespot.standard import BytesInputStream, Closeable, Proxy +from librespot.standard import BytesInputStream +from librespot.standard import Closeable +from librespot.standard import Proxy from librespot.Version import Version From c534f4bd101a7d6dcbe819a5975d416e6700282e Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Fri, 21 May 2021 03:41:17 +0000 Subject: [PATCH 6/6] Restyled by yapf --- examples/player.py | 46 +- librespot/audio/AbsChunkedInputStream.py | 31 +- librespot/audio/cdn/CdnFeedHelper.py | 25 +- librespot/audio/cdn/CdnManager.py | 82 ++- librespot/core/Session.py | 766 +++++++++++------------ 5 files changed, 463 insertions(+), 487 deletions(-) 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")