diff --git a/librespot/Version.py b/librespot/Version.py index 4eb54c1..c471e24 100644 --- a/librespot/Version.py +++ b/librespot/Version.py @@ -25,9 +25,7 @@ class Version: @staticmethod def standard_build_info() -> BuildInfo: - return BuildInfo( - product=Product.PRODUCT_CLIENT, - product_flags=[ProductFlags.PRODUCT_FLAG_NONE], - platform=Version.platform(), - version=112800721 - ) + return BuildInfo(product=Product.PRODUCT_CLIENT, + product_flags=[ProductFlags.PRODUCT_FLAG_NONE], + platform=Version.platform(), + version=112800721) diff --git a/librespot/audio/__init__.py b/librespot/audio/__init__.py index 09b8567..8c3757d 100644 --- a/librespot/audio/__init__.py +++ b/librespot/audio/__init__.py @@ -190,7 +190,6 @@ class AbsChunkedInputStream(io.BytesIO, HaltListener): buffer.seek(0) return buffer.read() - def notify_chunk_available(self, index: int) -> None: self.available_chunks()[index] = True self.__decoded_length += len(self.buffer()[index]) @@ -250,7 +249,10 @@ class AudioKeyManager(PacketsReceiver, Closeable): "Couldn't handle packet, cmd: {}, length: {}".format( packet.cmd, len(packet.payload))) - def get_audio_key(self, gid: bytes, file_id: bytes, retry: bool = True) -> bytes: + def get_audio_key(self, + gid: bytes, + file_id: bytes, + retry: bool = True) -> bytes: seq: int with self.__seq_holder_lock: seq = self.__seq_holder @@ -294,14 +296,16 @@ class AudioKeyManager(PacketsReceiver, Closeable): self.__reference_lock.notify_all() def error(self, code: int) -> None: - self.__audio_key_manager.logger.fatal("Audio key error, code: {}".format(code)) + self.__audio_key_manager.logger.fatal( + "Audio key error, code: {}".format(code)) with self.__reference_lock: self.__reference.put(None) self.__reference_lock.notify_all() def wait_response(self) -> bytes: with self.__reference_lock: - self.__reference_lock.wait(AudioKeyManager.audio_key_request_timeout) + self.__reference_lock.wait( + AudioKeyManager.audio_key_request_timeout) return self.__reference.get(block=False) @@ -313,9 +317,11 @@ 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.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.LoadedStream: if type(resp_or_url) is str: url = resp_or_url else: @@ -333,15 +339,14 @@ class CdnFeedHelper: track, streamer, normalization_data, - PlayableContentFeeder.Metrics( - file.file_id, preload, -1 if preload else audio_key_time), + PlayableContentFeeder.Metrics(file.file_id, preload, + -1 if preload else audio_key_time), ) @staticmethod def load_episode_external( - session: Session, episode: Metadata.Episode, - halt_listener: HaltListener - ) -> PlayableContentFeeder.LoadedStream: + session: Session, episode: Metadata.Episode, + halt_listener: HaltListener) -> PlayableContentFeeder.LoadedStream: resp = session.client().head(episode.external_url) if resp.status_code != 200: @@ -385,8 +390,7 @@ class CdnFeedHelper: episode, streamer, normalization_data, - PlayableContentFeeder.Metrics( - file.file_id, False, audio_key_time), + PlayableContentFeeder.Metrics(file.file_id, False, audio_key_time), ) @@ -408,7 +412,9 @@ class CdnManager: raise IOError("Response body is empty!") 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), @@ -419,7 +425,8 @@ class CdnManager: halt_listener, ) - def stream_file(self, file: Metadata.AudioFile, key: bytes, url: str, halt_listener: HaltListener): + def stream_file(self, file: Metadata.AudioFile, key: bytes, url: str, + halt_listener: HaltListener): return CdnManager.Streamer( self.__session, StreamId(file), @@ -442,9 +449,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(util.bytes_to_hex(file_id), url)) + self.logger.debug("Fetched CDN url for {}: {}".format( + util.bytes_to_hex(file_id), url)) return url - raise CdnManager.CdnException("Could not retrieve CDN url! result: {}".format(proto.result)) + raise CdnManager.CdnException( + "Could not retrieve CDN url! result: {}".format(proto.result)) class CdnException(Exception): pass @@ -463,7 +472,8 @@ class CdnManager: __expiration: int url: str - def __init__(self, cdn_manager, file_id: typing.Union[bytes, None], url: str): + def __init__(self, cdn_manager, file_id: typing.Union[bytes, None], + url: str): self.__cdn_manager: CdnManager = cdn_manager self.__file_id = file_id self.set_url(url) @@ -498,7 +508,8 @@ class CdnManager: break if expire_at is None: self.__expiration = -1 - self.__cdn_manager.logger.warning("Invalid __token__ in CDN url: {}".format(url)) + self.__cdn_manager.logger.warning( + "Invalid __token__ in CDN url: {}".format(url)) return self.__expiration = expire_at * 1000 else: @@ -529,16 +540,18 @@ class CdnManager: __session: Session __stream_id: StreamId - def __init__(self, session: Session, stream_id: StreamId, audio_format: SuperAudioFormat, - cdn_url: CdnManager.CdnUrl, cache: CacheManager, audio_decrypt: AudioDecrypt, - halt_listener: HaltListener): + def __init__(self, session: Session, stream_id: StreamId, + audio_format: SuperAudioFormat, + cdn_url: CdnManager.CdnUrl, cache: CacheManager, + audio_decrypt: AudioDecrypt, halt_listener: HaltListener): self.__session = session self.__stream_id = stream_id self.__audio_format = audio_format self.__audio_decrypt = audio_decrypt self.__cdn_url = cdn_url self.halt_listener = halt_listener - response = self.request(range_start=0, range_end=ChannelManager.chunk_size - 1) + response = self.request(range_start=0, + range_end=ChannelManager.chunk_size - 1) content_range = response.headers.get("Content-Range") if content_range is None: raise IOError("Missing Content-Range header!") @@ -549,7 +562,8 @@ class CdnManager: self.available = [False for _ in range(self.chunks)] self.requested = [False for _ in range(self.chunks)] self.buffer = [b"" for _ in range(self.chunks)] - self.__internal_stream = CdnManager.Streamer.InternalStream(self, False) + self.__internal_stream = CdnManager.Streamer.InternalStream( + self, False) self.requested[0] = True self.write_chunk(first_chunk, 0, False) @@ -557,9 +571,11 @@ class CdnManager: cached: bool) -> None: if self.__internal_stream.is_closed(): return - self.__session.logger.debug("Chunk {}/{} completed, cached: {}, stream: {}" - .format(chunk_index + 1, self.chunks, cached, self.describe())) - self.buffer[chunk_index] = self.__audio_decrypt.decrypt_chunk(chunk_index, chunk) + self.__session.logger.debug( + "Chunk {}/{} completed, cached: {}, stream: {}".format( + chunk_index + 1, self.chunks, cached, self.describe())) + self.buffer[chunk_index] = self.__audio_decrypt.decrypt_chunk( + chunk_index, chunk) self.__internal_stream.notify_chunk_available(chunk_index) def stream(self) -> AbsChunkedInputStream: @@ -590,7 +606,9 @@ class CdnManager: range_end = (chunk + 1) * ChannelManager.chunk_size - 1 response = self.__session.client().get( self.__cdn_url.url, - headers={"Range": "bytes={}-{}".format(range_start, range_end)}, + headers={ + "Range": "bytes={}-{}".format(range_start, range_end) + }, ) if response.status_code != 206: raise IOError(response.status_code) @@ -650,8 +668,9 @@ class NormalizationData: self.album_gain_db = album_gain_db self.album_peak = album_peak - self._LOGGER.debug("Loaded normalization data, track_gain: {}, track_peak: {}, album_gain: {}, album_peak: {}" - .format(track_gain_db, track_peak, album_gain_db, album_peak)) + self._LOGGER.debug( + "Loaded normalization data, track_gain: {}, track_peak: {}, album_gain: {}, album_peak: {}" + .format(track_gain_db, track_peak, album_gain_db, album_peak)) @staticmethod def read(input_stream: io.BytesIO) -> NormalizationData: @@ -659,11 +678,15 @@ class NormalizationData: data = input_stream.read(4 * 4) input_stream.seek(16) buffer = io.BytesIO(data) - return NormalizationData(struct.unpack(" float: - normalisation_factor = float(math.pow(10, (self.track_gain_db + normalisation_pregain) / 20)) + normalisation_factor = float( + math.pow(10, (self.track_gain_db + normalisation_pregain) / 20)) if normalisation_factor * self.track_peak > 1: self._LOGGER \ .warning("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.") @@ -680,19 +703,23 @@ class PlayableContentFeeder: def __init__(self, session: Session): self.__session = session - def load(self, playable_id: PlayableId, audio_quality_picker: AudioQualityPicker, - preload: bool, halt_listener: typing.Union[HaltListener, None]): + def load(self, playable_id: PlayableId, + audio_quality_picker: AudioQualityPicker, preload: bool, + halt_listener: typing.Union[HaltListener, None]): if type(playable_id) is TrackId: - return self.load_track(playable_id, audio_quality_picker, preload, halt_listener) + return self.load_track(playable_id, audio_quality_picker, preload, + halt_listener) def load_stream(self, file: Metadata.AudioFile, track: Metadata.Track, - episode: Metadata.Episode, preload: bool, halt_lister: HaltListener): + episode: Metadata.Episode, preload: bool, + halt_lister: HaltListener): if track is None and episode is None: raise RuntimeError() response = self.resolve_storage_interactive(file.file_id, preload) if response.result == StorageResolve.StorageResolveResponse.Result.CDN: if track is not None: - return CdnFeedHelper.load_track(self.__session, track, file, response, preload, halt_lister) + return CdnFeedHelper.load_track(self.__session, track, file, + response, preload, halt_lister) return CdnFeedHelper.load_episode(self.__session, episode, file, response, preload, halt_lister) elif response.result == StorageResolve.StorageResolveResponse.Result.STORAGE: @@ -705,11 +732,13 @@ class PlayableContentFeeder: else: raise RuntimeError("Unknown result: {}".format(response.result)) - def load_track(self, track_id_or_track: typing.Union[TrackId, Metadata.Track], + def load_track(self, track_id_or_track: typing.Union[TrackId, + Metadata.Track], audio_quality_picker: AudioQualityPicker, preload: bool, halt_listener: HaltListener): if type(track_id_or_track) is TrackId: - original = self.__session.api().get_metadata_4_track(track_id_or_track) + original = self.__session.api().get_metadata_4_track( + track_id_or_track) track = self.pick_alternative_if_necessary(original) if track is None: raise RuntimeError("Cannot get alternative track") @@ -717,10 +746,12 @@ class PlayableContentFeeder: track = track_id_or_track file = audio_quality_picker.get_file(track.file) if file is None: - self.logger.fatal("Couldn't find any suitable audio file, available") + self.logger.fatal( + "Couldn't find any suitable audio file, available") return self.load_stream(file, track, None, preload, halt_listener) - def pick_alternative_if_necessary(self, track: Metadata.Track) -> typing.Union[Metadata.Track, None]: + def pick_alternative_if_necessary( + self, track: Metadata.Track) -> typing.Union[Metadata.Track, None]: if len(track.file) > 0: return track for alt in track.alternative: @@ -752,8 +783,11 @@ class PlayableContentFeeder: preload: bool) -> StorageResolve.StorageResolveResponse: resp = self.__session.api().send( "GET", - (self.storage_resolve_interactive_prefetch if preload else self.storage_resolve_interactive) - .format(util.bytes_to_hex(file_id)), None, None, + (self.storage_resolve_interactive_prefetch + if preload else self.storage_resolve_interactive).format( + util.bytes_to_hex(file_id)), + None, + None, ) if resp.status_code != 200: raise RuntimeError(resp.status_code) @@ -771,8 +805,10 @@ class PlayableContentFeeder: normalization_data: NormalizationData metrics: PlayableContentFeeder.Metrics - def __init__(self, track_or_episode: typing.Union[Metadata.Track, Metadata.Episode], - input_stream: GeneralAudioStream, normalization_data: typing.Union[NormalizationData, None], + def __init__(self, track_or_episode: typing.Union[Metadata.Track, + Metadata.Episode], + input_stream: GeneralAudioStream, + normalization_data: typing.Union[NormalizationData, None], metrics: PlayableContentFeeder.Metrics): if type(track_or_episode) is Metadata.Track: self.track = track_or_episode @@ -791,9 +827,10 @@ class PlayableContentFeeder: preloaded_audio_key: bool audio_key_time: int - def __init__(self, file_id: typing.Union[bytes, None], preloaded_audio_key: bool, - audio_key_time: int): - self.file_id = None if file_id is None else util.bytes_to_hex(file_id) + def __init__(self, file_id: typing.Union[bytes, None], + preloaded_audio_key: bool, audio_key_time: int): + self.file_id = None if file_id is None else util.bytes_to_hex( + file_id) self.preloaded_audio_key = preloaded_audio_key self.audio_key_time = audio_key_time if preloaded_audio_key and audio_key_time != -1: diff --git a/librespot/audio/decoders.py b/librespot/audio/decoders.py index 66e0e91..f6df0cd 100644 --- a/librespot/audio/decoders.py +++ b/librespot/audio/decoders.py @@ -27,9 +27,11 @@ class AudioQuality(enum.Enum): return AudioQuality.VERY_HIGH raise RuntimeError("Unknown format: {}".format(format)) - def get_matches(self, files: typing.List[AudioFile]) -> typing.List[AudioFile]: + def get_matches(self, + files: typing.List[AudioFile]) -> typing.List[AudioFile]: file_list = [] for file in files: - if hasattr(file, "format") and AudioQuality.get_quality(file.format) == self: + if hasattr(file, "format") and AudioQuality.get_quality( + file.format) == self: file_list.append(file) return file_list diff --git a/librespot/audio/decrypt.py b/librespot/audio/decrypt.py index 23af0e2..e72cd37 100644 --- a/librespot/audio/decrypt.py +++ b/librespot/audio/decrypt.py @@ -24,14 +24,16 @@ class AesAudioDecrypt(AudioDecrypt): iv = self.iv_int + int(ChannelManager.chunk_size * chunk_index / 16) start = time.time_ns() for i in range(0, len(buffer), 4096): - cipher = AES.new(key=self.key, mode=AES.MODE_CTR, + cipher = AES.new(key=self.key, + mode=AES.MODE_CTR, counter=Counter.new(128, initial_value=iv)) count = min(4096, len(buffer) - i) decrypted_buffer = cipher.decrypt(buffer[i:i + count]) new_buffer.write(decrypted_buffer) if count != len(decrypted_buffer): - raise RuntimeError("Couldn't process all data, actual: {}, expected: {}" - .format(len(decrypted_buffer), count)) + raise RuntimeError( + "Couldn't process all data, actual: {}, expected: {}". + format(len(decrypted_buffer), count)) iv += self.iv_diff self.decrypt_total_time += time.time_ns() - start self.decrypt_count += 1 @@ -39,4 +41,5 @@ class AesAudioDecrypt(AudioDecrypt): return new_buffer.read() def decrypt_time_ms(self): - return 0 if self.decrypt_count == 0 else int((self.decrypt_total_time / self.decrypt_count) / 1000000) + return 0 if self.decrypt_count == 0 else int( + (self.decrypt_total_time / self.decrypt_count) / 1000000) diff --git a/librespot/audio/storage.py b/librespot/audio/storage.py index 32fcadf..747943d 100644 --- a/librespot/audio/storage.py +++ b/librespot/audio/storage.py @@ -50,21 +50,25 @@ class ChannelManager(Closeable, PacketsReceiver): chunk_id = struct.unpack(">H", payload.read(2))[0] channel = self.channels.get(chunk_id) if channel is None: - self.logger.warning("Couldn't find channel, id: {}, received: {}" - .format(chunk_id, len(packet.payload))) + self.logger.warning( + "Couldn't find channel, id: {}, received: {}".format( + chunk_id, len(packet.payload))) return channel.add_to_queue(payload) elif packet.is_cmd(Packet.Type.channel_error): chunk_id = struct.unpack(">H", payload.read(2))[0] channel = self.channels.get(chunk_id) if channel is None: - self.logger.warning("Dropping channel error, id: {}, code: {}" - .format(chunk_id, struct.unpack(">H", payload.read(2))[0])) + self.logger.warning( + "Dropping channel error, id: {}, code: {}".format( + chunk_id, + struct.unpack(">H", payload.read(2))[0])) return channel.stream_error(struct.unpack(">H", payload.read(2))[0]) else: - self.logger.warning("Couldn't handle packet, cmd: {}, payload: {}" - .format(packet.cmd, util.bytes_to_hex(packet.payload))) + self.logger.warning( + "Couldn't handle packet, cmd: {}, payload: {}".format( + packet.cmd, util.bytes_to_hex(packet.payload))) def close(self) -> None: self.executor_service.shutdown() @@ -86,14 +90,16 @@ class ChannelManager(Closeable, PacketsReceiver): with self.channel_manager.seq_holder_lock: self.chunk_id = self.channel_manager.seq_holder self.channel_manager.seq_holder += 1 - self.channel_manager.executor_service.submit(lambda: ChannelManager.Channel.Handler(self)) + self.channel_manager.executor_service.submit( + lambda: ChannelManager.Channel.Handler(self)) def _handle(self, payload: bytes) -> bool: if len(payload) == 0: if not self.__header: self.__file.write_chunk(payload, self.__chunk_index, False) return True - self.channel_manager.logger.debug("Received empty chunk, skipping.") + self.channel_manager.logger.debug( + "Received empty chunk, skipping.") return False if self.__header: length: int @@ -123,7 +129,10 @@ class ChannelManager(Closeable, PacketsReceiver): self.__channel = channel def run(self) -> None: - self.__channel.channel_manager.logger.debug("ChannelManager.Handler is starting") + self.__channel.channel_manager.logger.debug( + "ChannelManager.Handler is starting") with self.__channel.q.all_tasks_done: - self.__channel.channel_manager.channels.pop(self.__channel.chunk_id) - self.__channel.channel_manager.logger.debug("ChannelManager.Handler is shutting down") + self.__channel.channel_manager.channels.pop( + self.__channel.chunk_id) + self.__channel.channel_manager.logger.debug( + "ChannelManager.Handler is shutting down") diff --git a/librespot/core.py b/librespot/core.py index e376e62..e7bfd78 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -41,36 +41,51 @@ class ApiClient(Closeable): self.__session = session self.__base_url = "https://{}".format(ApResolver.get_random_spclient()) - def build_request(self, method: str, suffix: str, headers: typing.Union[None, typing.Dict[str, str]], - body: typing.Union[None, bytes]) -> requests.PreparedRequest: + def build_request( + self, method: str, suffix: str, + headers: typing.Union[None, typing.Dict[str, str]], + body: typing.Union[None, bytes]) -> requests.PreparedRequest: request = requests.PreparedRequest() request.method = method request.data = body request.headers = {} if headers is not None: request.headers = headers - request.headers["Authorization"] = "Bearer {}".format(self.__session.tokens().get("playlist-read")) + request.headers["Authorization"] = "Bearer {}".format( + self.__session.tokens().get("playlist-read")) request.url = self.__base_url + suffix return request - def send(self, method: str, suffix: str, headers: typing.Union[None, typing.Dict[str, str]], - body: typing.Union[None, bytes]) -> requests.Response: - response = self.__session.client().send(self.build_request(method, suffix, headers, body)) + def send(self, method: str, suffix: str, + headers: typing.Union[None, typing.Dict[str, str]], + body: typing.Union[None, bytes]) -> requests.Response: + response = self.__session.client().send( + self.build_request(method, suffix, headers, body)) return response - def put_connect_state(self, connection_id: str, proto: Connect.PutStateRequest) -> None: + def put_connect_state(self, connection_id: str, + proto: Connect.PutStateRequest) -> None: response = self.send( - "PUT", "/connect-state/v1/devices/{}".format(self.__session.device_id()), - {"Content-Type": "application/protobuf", "X-Spotify-Connection-Id": connection_id}, + "PUT", + "/connect-state/v1/devices/{}".format(self.__session.device_id()), + { + "Content-Type": "application/protobuf", + "X-Spotify-Connection-Id": connection_id + }, proto.SerializeToString(), ) if response.status_code == 413: - self.logger.warning("PUT state payload is too large: {} bytes uncompressed.".format(len(proto.SerializeToString()))) + self.logger.warning( + "PUT state payload is too large: {} bytes uncompressed.". + format(len(proto.SerializeToString()))) elif response.status_code != 200: - self.logger.warning("PUT state returned {}. headers: {}".format(response.status_code, response.headers)) + self.logger.warning("PUT state returned {}. headers: {}".format( + response.status_code, response.headers)) def get_metadata_4_track(self, track: TrackId) -> Metadata.Track: - response = self.send("GET", "/metadata/4/track/{}".format(track.hex_id()), None, None) + response = self.send("GET", + "/metadata/4/track/{}".format(track.hex_id()), + None, None) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -80,7 +95,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_episode(self, episode: EpisodeId) -> Metadata.Episode: - response = self.send("GET", "/metadata/4/episode/{}".format(episode.hex_id()), None, None) + response = self.send("GET", + "/metadata/4/episode/{}".format(episode.hex_id()), + None, None) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -90,7 +107,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_album(self, album: AlbumId) -> Metadata.Album: - response = self.send("GET", "/metadata/4/album/{}".format(album.hex_id()), None, None) + response = self.send("GET", + "/metadata/4/album/{}".format(album.hex_id()), + None, None) ApiClient.StatusCodeException.check_status(response) body = response.content @@ -101,7 +120,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_artist(self, artist: ArtistId) -> Metadata.Artist: - response = self.send("GET", "/metadata/4/artist/{}".format(artist.hex_id()), None, None) + response = self.send("GET", + "/metadata/4/artist/{}".format(artist.hex_id()), + None, None) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -111,7 +132,9 @@ class ApiClient(Closeable): return proto def get_metadata_4_show(self, show: ShowId) -> Metadata.Show: - response = self.send("GET", "/metadata/4/show/{}".format(show.hex_id()), None, None) + response = self.send("GET", + "/metadata/4/show/{}".format(show.hex_id()), None, + None) ApiClient.StatusCodeException.check_status(response) body = response.content if body is None: @@ -145,7 +168,8 @@ class ApResolver: Returns: The resulting object will be returned """ - response = requests.get("{}?type={}".format(ApResolver.base_url, service_type)) + response = requests.get("{}?type={}".format(ApResolver.base_url, + service_type)) return response.json() @staticmethod @@ -203,14 +227,19 @@ class EventService(Closeable): try: body = event_builder.to_array() resp = self.__session.mercury().send_sync( - RawMercuryRequest.Builder().set_uri("hm://event-service/v1/events") - .set_method("POST").add_user_field("Accept-Language", "en") - .add_user_field("X-ClientTimeStamp", int(time.time() * 1000)).add_payload_part(body).build()) - self.logger.debug("Event sent. body: {}, result: {}".format(body, resp.status_code)) + RawMercuryRequest.Builder().set_uri( + "hm://event-service/v1/events").set_method("POST"). + add_user_field("Accept-Language", "en").add_user_field( + "X-ClientTimeStamp", + int(time.time() * 1000)).add_payload_part(body).build()) + self.logger.debug("Event sent. body: {}, result: {}".format( + body, resp.status_code)) except IOError as ex: - self.logger.error("Failed sending event: {} {}".format(event_builder, ex)) + self.logger.error("Failed sending event: {} {}".format( + event_builder, ex)) - def send_event(self, event_or_builder: typing.Union[GenericEvent, EventBuilder]): + def send_event(self, event_or_builder: typing.Union[GenericEvent, + EventBuilder]): if type(event_or_builder) is EventService.GenericEvent: builder = event_or_builder.build() elif type(event_or_builder) is EventService.EventBuilder: @@ -258,7 +287,9 @@ class EventService(Closeable): s = "" self.body.write(s.encode()) - def append(self, c: int = None, s: str = None) -> EventService.EventBuilder: + def append(self, + c: int = None, + s: str = None) -> EventService.EventBuilder: if c is None and s is None or c is not None and s is not None: raise TypeError() if c is not None: @@ -326,7 +357,8 @@ class Session(Closeable, SubListener): self.connection = Session.ConnectionHolder.create(address, None) self.__inner = inner self.__keys = DiffieHellman() - self.logger.info("Created new session! device_id: {}, ap: {}".format(inner.device_id, address)) + self.logger.info("Created new session! device_id: {}, ap: {}".format( + inner.device_id, address)) def api(self) -> ApiClient: self.__wait_auth_lock() @@ -346,7 +378,8 @@ class Session(Closeable, SubListener): raise RuntimeError("Session isn't authenticated!") return self.__audio_key_manager - def authenticate(self, credential: Authentication.LoginCredentials) -> None: + def authenticate(self, + credential: Authentication.LoginCredentials) -> None: """ Log in to Spotify Args: @@ -365,7 +398,8 @@ class Session(Closeable, SubListener): self.__event_service = EventService(self) self.__auth_lock_bool = False self.__auth_lock.notify_all() - self.logger.info("Authenticated as {}!".format(self.__ap_welcome.canonical_username)) + self.logger.info("Authenticated as {}!".format( + self.__ap_welcome.canonical_username)) self.mercury().interested_in("spotify:user:attributes:update", self) def cache(self) -> CacheManager: @@ -393,7 +427,8 @@ class Session(Closeable, SubListener): """ Close instance """ - self.logger.info("Closing session. device_id: {}".format(self.__inner.device_id)) + self.logger.info("Closing session. device_id: {}".format( + self.__inner.device_id)) self.__closing = True if self.__audio_key_manager is not None: self.__audio_key_manager = None @@ -416,7 +451,8 @@ class Session(Closeable, SubListener): self.__ap_welcome = None self.cipher_pair = None self.__closed = True - self.logger.info("Closed session. device_id: {}".format(self.__inner.device_id)) + self.logger.info("Closed session. device_id: {}".format( + self.__inner.device_id)) def connect(self) -> None: """ @@ -433,10 +469,7 @@ class Session(Closeable, SubListener): ], login_crypto_hello=Keyexchange.LoginCryptoHelloUnion( diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanHello( - gc=self.__keys.public_key_bytes(), - server_keys_known=1 - ), - ), + gc=self.__keys.public_key_bytes(), server_keys_known=1), ), padding=b"\x1e", ) client_hello_bytes = client_hello_proto.SerializeToString() @@ -450,22 +483,24 @@ class Session(Closeable, SubListener): # Read APResponseMessage ap_response_message_length = self.connection.read_int() acc.write_int(ap_response_message_length) - ap_response_message_bytes = self.connection.read(ap_response_message_length - 4) + ap_response_message_bytes = self.connection.read( + ap_response_message_length - 4) acc.write(ap_response_message_bytes) ap_response_message_proto = Keyexchange.APResponseMessage() ap_response_message_proto.ParseFromString(ap_response_message_bytes) shared_key = util.int_to_bytes( self.__keys.compute_shared_key( - ap_response_message_proto.challenge.login_crypto_challenge.diffie_hellman.gs - ) - ) + ap_response_message_proto.challenge.login_crypto_challenge. + diffie_hellman.gs)) # Check gs_signature rsa = RSA.construct((int.from_bytes(self.__server_key, "big"), 65537)) pkcs1_v1_5 = PKCS1_v1_5.new(rsa) sha1 = SHA1.new() - sha1.update(ap_response_message_proto.challenge.login_crypto_challenge.diffie_hellman.gs) + sha1.update(ap_response_message_proto.challenge.login_crypto_challenge. + diffie_hellman.gs) if not pkcs1_v1_5.verify( - sha1, ap_response_message_proto.challenge.login_crypto_challenge.diffie_hellman.gs_signature): + sha1, ap_response_message_proto.challenge. + login_crypto_challenge.diffie_hellman.gs_signature): raise RuntimeError("Failed signature check!") # Solve challenge buffer = io.BytesIO() @@ -481,11 +516,12 @@ class Session(Closeable, SubListener): client_response_plaintext_proto = Keyexchange.ClientResponsePlaintext( crypto_response=Keyexchange.CryptoResponseUnion(), login_crypto_response=Keyexchange.LoginCryptoResponseUnion( - diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse(hmac=challenge) - ), + diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse( + hmac=challenge)), pow_response=Keyexchange.PoWResponseUnion(), ) - client_response_plaintext_bytes = client_response_plaintext_proto.SerializeToString() + client_response_plaintext_bytes = client_response_plaintext_proto.SerializeToString( + ) self.connection.write_int(4 + len(client_response_plaintext_bytes)) self.connection.write(client_response_plaintext_bytes) self.connection.flush() @@ -493,7 +529,8 @@ class Session(Closeable, SubListener): self.connection.set_timeout(1) scrap = self.connection.read(4) if len(scrap) == 4: - payload = self.connection.read(struct.unpack(">i", scrap)[0] - 4) + payload = self.connection.read( + struct.unpack(">i", scrap)[0] - 4) failed = Keyexchange.APResponseMessage() failed.ParseFromString(payload) raise RuntimeError(failed) @@ -522,7 +559,8 @@ class Session(Closeable, SubListener): return self.__inner.device_id def get_user_attribute(self, key: str, fallback: str = None) -> str: - return self.__user_attributes.get(key) if self.__user_attributes.get(key) is not None else fallback + return self.__user_attributes.get(key) if self.__user_attributes.get( + key) is not None else fallback def is_valid(self) -> bool: if self.__closed: @@ -550,7 +588,8 @@ class Session(Closeable, SubListener): return for i in range(len(product)): self.__user_attributes[product[i].tag] = product[i].text - self.logger.debug("Parsed product info: {}".format(self.__user_attributes)) + self.logger.debug("Parsed product info: {}".format( + self.__user_attributes)) def reconnect(self) -> None: """ @@ -559,7 +598,8 @@ class Session(Closeable, SubListener): if self.connection is not None: self.connection.close() self.__receiver.stop() - self.connection = Session.ConnectionHolder.create(ApResolver.get_random_accesspoint(), self.__inner.conf) + self.connection = Session.ConnectionHolder.create( + ApResolver.get_random_accesspoint(), self.__inner.conf) self.connect() self.__authenticate_partial( Authentication.LoginCredentials( @@ -569,7 +609,8 @@ class Session(Closeable, SubListener): ), True, ) - self.logger.info("Re-authenticated as {}!".format(self.__ap_welcome.canonical_username)) + self.logger.info("Re-authenticated as {}!".format( + self.__ap_welcome.canonical_username)) def reconnecting(self) -> bool: return not self.__closing and not self.__closed and self.connection is None @@ -597,7 +638,9 @@ class Session(Closeable, SubListener): raise RuntimeError("Session isn't authenticated!") return self.__token_provider - def __authenticate_partial(self, credential: Authentication.LoginCredentials, remove_lock: bool) -> None: + def __authenticate_partial(self, + credential: Authentication.LoginCredentials, + remove_lock: bool) -> None: """ Login to Spotify Args: @@ -615,7 +658,9 @@ class Session(Closeable, SubListener): ), version_string=Version.version_string(), ) - self.__send_unchecked(Packet.Type.login, client_response_encrypted_proto.SerializeToString()) + self.__send_unchecked( + Packet.Type.login, + client_response_encrypted_proto.SerializeToString()) packet = self.cipher_pair.receive_encoded(self.connection) if packet.is_cmd(Packet.Type.ap_welcome): self.__ap_welcome = Authentication.APWelcome() @@ -624,9 +669,11 @@ class Session(Closeable, SubListener): bytes0x0f = Random.get_random_bytes(0x14) self.__send_unchecked(Packet.Type.unknown_0x0f, bytes0x0f) preferred_locale = io.BytesIO() - preferred_locale.write(b"\x00\x00\x10\x00\x02preferred-locale" + self.__inner.preferred_locale.encode()) + preferred_locale.write(b"\x00\x00\x10\x00\x02preferred-locale" + + self.__inner.preferred_locale.encode()) preferred_locale.seek(0) - self.__send_unchecked(Packet.Type.preferred_locale, preferred_locale.read()) + self.__send_unchecked(Packet.Type.preferred_locale, + preferred_locale.read()) if remove_lock: with self.__auth_lock: self.__auth_lock_bool = False @@ -636,13 +683,15 @@ class Session(Closeable, SubListener): reusable_type = Authentication.AuthenticationType.Name( self.__ap_welcome.reusable_auth_credentials_type) if self.__inner.conf.stored_credentials_file is None: - raise TypeError("The file path to be saved is not specified") + raise TypeError( + "The file path to be saved is not specified") with open(self.__inner.conf.stored_credentials_file, "w") as f: - json.dump({ - "username": self.__ap_welcome.canonical_username, - "credentials": base64.b64encode(reusable).decode(), - "type": reusable_type, - }, f) + json.dump( + { + "username": self.__ap_welcome.canonical_username, + "credentials": base64.b64encode(reusable).decode(), + "type": reusable_type, + }, f) elif packet.is_cmd(Packet.Type.auth_failure): ap_login_failed = Keyexchange.APLoginFailed() @@ -746,7 +795,8 @@ class Session(Closeable, SubListener): """ pass - def stored_file(self, stored_credentials: str = None) -> Session.Builder: + def stored_file(self, + stored_credentials: str = None) -> Session.Builder: """ Create credential from stored file Args: @@ -765,7 +815,8 @@ class Session(Closeable, SubListener): else: try: self.login_credentials = Authentication.LoginCredentials( - typ=Authentication.AuthenticationType.Value(obj["type"]), + typ=Authentication.AuthenticationType.Value( + obj["type"]), username=obj["username"], auth_data=base64.b64decode(obj["credentials"]), ) @@ -834,20 +885,20 @@ class Session(Closeable, SubListener): 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, + # 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 @@ -880,7 +931,8 @@ class Session(Closeable, SubListener): # Stored credentials store_credentials: bool = True - stored_credentials_file: str = os.path.join(os.getcwd(), "credentials.json") + stored_credentials_file: str = os.path.join( + os.getcwd(), "credentials.json") # Fetching retry_on_chunk_error: bool = True @@ -919,7 +971,9 @@ class Session(Closeable, SubListener): # self.proxyPassword = proxy_password # return self - def set_cache_enabled(self, cache_enabled: bool) -> Session.Configuration.Builder: + def set_cache_enabled( + self, + cache_enabled: bool) -> Session.Configuration.Builder: """ Set cache_enabled Args: @@ -930,7 +984,8 @@ class Session(Closeable, SubListener): self.cache_enabled = cache_enabled return self - def set_cache_dir(self, cache_dir: str) -> Session.Configuration.Builder: + def set_cache_dir(self, + cache_dir: str) -> Session.Configuration.Builder: """ Set cache_dir Args: @@ -941,7 +996,9 @@ class Session(Closeable, SubListener): self.cache_dir = cache_dir return self - def set_do_cache_clean_up(self, do_cache_clean_up: bool) -> Session.Configuration.Builder: + def set_do_cache_clean_up( + self, + do_cache_clean_up: bool) -> Session.Configuration.Builder: """ Set do_cache_clean_up Args: @@ -952,7 +1009,9 @@ class Session(Closeable, SubListener): self.do_cache_clean_up = do_cache_clean_up return self - def set_store_credentials(self, store_credentials: bool) -> Session.Configuration.Builder: + def set_store_credentials( + self, + store_credentials: bool) -> Session.Configuration.Builder: """ Set store_credentials Args: @@ -963,7 +1022,9 @@ class Session(Closeable, SubListener): self.store_credentials = store_credentials return self - def set_stored_credential_file(self, stored_credential_file: str) -> Session.Configuration.Builder: + def set_stored_credential_file( + self, stored_credential_file: str + ) -> Session.Configuration.Builder: """ Set stored_credential_file Args: @@ -974,7 +1035,9 @@ class Session(Closeable, SubListener): self.stored_credentials_file = stored_credential_file return self - def set_retry_on_chunk_error(self, retry_on_chunk_error: bool) -> Session.Configuration.Builder: + def set_retry_on_chunk_error( + self, retry_on_chunk_error: bool + ) -> Session.Configuration.Builder: """ Set retry_on_chunk_error Args: @@ -1111,18 +1174,19 @@ class Session(Closeable, SubListener): preferred_locale: str def __init__( - self, - device_type: Connect.DeviceType, - device_name: str, - preferred_locale: str, - conf: Session.Configuration, - device_id: str = None, + 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 util.random_hex_string(40)) + self.device_id = (device_id if device_id is not None else + util.random_hex_string(40)) class Receiver: __session: Session @@ -1148,65 +1212,79 @@ class Session(Closeable, SubListener): packet: Packet cmd: bytes try: - packet = self.__session.cipher_pair.receive_encoded(self.__session.connection) + packet = self.__session.cipher_pair.receive_encoded( + self.__session.connection) cmd = Packet.Type.parse(packet.cmd) if cmd is None: self.__session.logger.info( "Skipping unknown command cmd: 0x{}, payload: {}". - format(util.bytes_to_hex(packet.cmd), packet.payload)) + format(util.bytes_to_hex(packet.cmd), + packet.payload)) continue except RuntimeError as ex: if self.__running: - self.__session.logger.fatal("Failed reading packet! {}".format(ex)) + self.__session.logger.fatal( + "Failed reading packet! {}".format(ex)) self.__session.reconnect() break if not self.__running: break if cmd == Packet.Type.ping: if self.__session.scheduled_reconnect is not None: - self.__session.scheduler.cancel(self.__session.scheduled_reconnect) + self.__session.scheduler.cancel( + self.__session.scheduled_reconnect) def anonymous(): - self.__session.logger.warning("Socket timed out. Reconnecting...") + self.__session.logger.warning( + "Socket timed out. Reconnecting...") self.__session.reconnect() - self.__session.scheduled_reconnect = self.__session.scheduler.enter(2 * 60 + 5, 1, anonymous) + self.__session.scheduled_reconnect = self.__session.scheduler.enter( + 2 * 60 + 5, 1, anonymous) self.__session.send(Packet.Type.pong, packet.payload) elif cmd == Packet.Type.pong_ack: continue elif cmd == Packet.Type.country_code: self.__session.country_code = packet.payload.decode() - self.__session.logger.info("Received country_code: {}".format(self.__session.country_code)) + self.__session.logger.info( + "Received country_code: {}".format( + self.__session.country_code)) elif cmd == Packet.Type.license_version: license_version = io.BytesIO(packet.payload) - license_id = struct.unpack(">h", license_version.read(2))[0] + license_id = struct.unpack(">h", + license_version.read(2))[0] if license_id != 0: buffer = license_version.read() self.__session.logger.info( - "Received license_version: {}, {}".format(license_id, buffer.decode())) + "Received license_version: {}, {}".format( + license_id, buffer.decode())) else: - self.__session.logger.info("Received license_version: {}".format(license_id)) + self.__session.logger.info( + "Received license_version: {}".format(license_id)) elif cmd == Packet.Type.unknown_0x10: - self.__session.logger.debug("Received 0x10: {}".format(util.bytes_to_hex(packet.payload))) + self.__session.logger.debug("Received 0x10: {}".format( + util.bytes_to_hex(packet.payload))) elif cmd in [ - Packet.Type.mercury_sub, Packet.Type.mercury_unsub, - Packet.Type.mercury_event, Packet.Type.mercury_req + Packet.Type.mercury_sub, Packet.Type.mercury_unsub, + Packet.Type.mercury_event, Packet.Type.mercury_req ]: self.__session.mercury().dispatch(packet) elif cmd in [Packet.Type.aes_key, Packet.Type.aes_key_error]: self.__session.audio_key().dispatch(packet) elif cmd in [ - Packet.Type.channel_error, Packet.Type.stream_chunk_res + Packet.Type.channel_error, Packet.Type.stream_chunk_res ]: self.__session.channel().dispatch(packet) elif cmd == Packet.Type.product_info: self.__session.parse_product_info(packet.payload) else: - self.__session.logger.info("Skipping {}".format(util.bytes_to_hex(cmd))) + self.__session.logger.info("Skipping {}".format( + util.bytes_to_hex(cmd))) class SpotifyAuthenticationException(Exception): def __init__(self, login_failed: Keyexchange.APLoginFailed): - super().__init__(Keyexchange.ErrorCode.Name(login_failed.error_code)) + super().__init__( + Keyexchange.ErrorCode.Name(login_failed.error_code)) class TokenProvider: @@ -1218,7 +1296,8 @@ class TokenProvider: def __init__(self, session: Session): self._session = session - def find_token_with_all_scopes(self, scopes: typing.List[str]) -> typing.Union[StoredToken, None]: + def find_token_with_all_scopes( + self, scopes: typing.List[str]) -> typing.Union[StoredToken, None]: for token in self.__tokens: if token.has_scopes(scopes): return token @@ -1238,11 +1317,15 @@ class TokenProvider: else: return token self.logger.debug( - "Token expired or not suitable, requesting again. scopes: {}, old_token: {}".format(scopes, token)) + "Token expired or not suitable, requesting again. scopes: {}, old_token: {}" + .format(scopes, token)) response = self._session.mercury().send_sync_json( - MercuryRequests.request_token(self._session.device_id(), ",".join(scopes))) + MercuryRequests.request_token(self._session.device_id(), + ",".join(scopes))) token = TokenProvider.StoredToken(response) - self.logger.debug("Updated token successfully! scopes: {}, new_token: {}".format(scopes, token)) + self.logger.debug( + "Updated token successfully! scopes: {}, new_token: {}".format( + scopes, token)) self.__tokens.append(token) return token @@ -1259,8 +1342,10 @@ class TokenProvider: self.scopes = obj["scope"] def expired(self) -> bool: - return (self.timestamp + - (self.expires_in - TokenProvider.token_expire_threshold) * 1000 < int(time.time_ns() / 1000)) + return ( + self.timestamp + + (self.expires_in - TokenProvider.token_expire_threshold) * 1000 + < int(time.time_ns() / 1000)) def has_scope(self, scope: str) -> bool: for s in self.scopes: diff --git a/librespot/crypto.py b/librespot/crypto.py index d4f112c..bf342c5 100644 --- a/librespot/crypto.py +++ b/librespot/crypto.py @@ -22,7 +22,8 @@ class CipherPair: self.__receive_cipher = Shannon() self.__receive_cipher.key(receive_key) - def send_encoded(self, connection: Session.ConnectionHolder, cmd: bytes, payload: bytes) -> None: + def send_encoded(self, connection: Session.ConnectionHolder, cmd: bytes, + payload: bytes) -> None: """ Send decrypted data to the socket :param connection: @@ -57,7 +58,8 @@ class CipherPair: header_bytes = self.__receive_cipher.decrypt(connection.read(3)) cmd = struct.pack(">s", bytes([header_bytes[0]])) payload_length = (header_bytes[1] << 8) | (header_bytes[2] & 0xff) - payload_bytes = self.__receive_cipher.decrypt(connection.read(payload_length)) + payload_bytes = self.__receive_cipher.decrypt( + connection.read(payload_length)) mac = connection.read(4) expected_mac = self.__receive_cipher.finish(4) if mac != expected_mac: @@ -78,7 +80,8 @@ class DiffieHellman: b'\x13\x9b"QJ\x08y\x8e4\x04\xdd\xef\x95\x19' b'\xb3\xcd:C\x1b0+\nm\xf2_\x147O\xe15mmQ\xc2' b'E\xe4\x85\xb5vb^~\xc6\xf4LB\xe9\xa6:6 \xff' - b'\xff\xff\xff\xff\xff\xff\xff', byteorder="big") + b'\xff\xff\xff\xff\xff\xff\xff', + byteorder="big") __private_key: int __public_key: int diff --git a/librespot/dealer.py b/librespot/dealer.py index 29bc85c..143c256 100644 --- a/librespot/dealer.py +++ b/librespot/dealer.py @@ -9,5 +9,3 @@ import typing if typing.TYPE_CHECKING: from librespot.core import Session - - diff --git a/librespot/mercury.py b/librespot/mercury.py index 6592567..f7807e1 100644 --- a/librespot/mercury.py +++ b/librespot/mercury.py @@ -93,7 +93,8 @@ class MercuryClient(Closeable, PacketsReceiver): if not dispatched: self.logger.debug( "Couldn't dispatch Mercury event seq: {}, uri: {}, code: {}, payload: {}" - .format(seq, header.uri, header.status_code, response.payload)) + .format(seq, header.uri, header.status_code, + response.payload)) elif (packet.is_cmd(Packet.Type.mercury_req) or packet.is_cmd(Packet.Type.mercury_sub) or packet.is_cmd(Packet.Type.mercury_sub)): @@ -103,8 +104,8 @@ class MercuryClient(Closeable, PacketsReceiver): callback.response(response) else: self.logger.warning( - "Skipped Mercury response, seq: {}, uri: {}, code: {}" - .format(seq, response.uri, response.status_code)) + "Skipped Mercury response, seq: {}, uri: {}, code: {}". + format(seq, response.uri, response.status_code)) with self.__remove_callback_lock: self.__remove_callback_lock.notify_all() else: @@ -113,7 +114,8 @@ class MercuryClient(Closeable, PacketsReceiver): seq, header.uri, header.status_code)) def interested_in(self, uri: str, listener: SubListener) -> None: - self.__subscriptions.append(MercuryClient.InternalSubListener(uri, listener, False)) + self.__subscriptions.append( + MercuryClient.InternalSubListener(uri, listener, False)) def not_interested_in(self, listener: SubListener) -> None: try: @@ -139,7 +141,8 @@ class MercuryClient(Closeable, PacketsReceiver): seq = self.__seq_holder self.__seq_holder += 1 self.logger.debug( - "Send Mercury request, seq: {}, uri: {}, method: {}".format(seq, request.header.uri, request.header.method)) + "Send Mercury request, seq: {}, uri: {}, method: {}".format( + seq, request.header.uri, request.header.method)) buffer.write(struct.pack(">H", 4)) buffer.write(struct.pack(">i", seq)) buffer.write(b"\x01") @@ -169,8 +172,9 @@ class MercuryClient(Closeable, PacketsReceiver): try: response = callback.wait_response() if response is None: - raise IOError("Request timeout out, {} passed, yet no response. seq: {}" - .format(self.mercury_request_timeout, seq)) + raise IOError( + "Request timeout out, {} passed, yet no response. seq: {}". + format(self.mercury_request_timeout, seq)) return response except queue.Empty as e: raise IOError(e) @@ -195,9 +199,11 @@ class MercuryClient(Closeable, PacketsReceiver): for payload in response.payload: sub = Pubsub.Subscription() sub.ParseFromString(payload) - self.__subscriptions.append(MercuryClient.InternalSubListener(sub.uri, listener, True)) + self.__subscriptions.append( + MercuryClient.InternalSubListener(sub.uri, listener, True)) else: - self.__subscriptions.append(MercuryClient.InternalSubListener(uri, listener, True)) + self.__subscriptions.append( + MercuryClient.InternalSubListener(uri, listener, True)) self.logger.debug("Subscribed successfully to {}!".format(uri)) def unsubscribe(self, uri) -> None: @@ -263,7 +269,8 @@ class MercuryClient(Closeable, PacketsReceiver): payload: typing.List[bytes] status_code: int - def __init__(self, header: Mercury.Header, payload: typing.List[bytes]): + def __init__(self, header: Mercury.Header, + payload: typing.List[bytes]): self.uri = header.uri self.status_code = header.status_code self.payload = payload[1:] @@ -281,7 +288,8 @@ class MercuryClient(Closeable, PacketsReceiver): self.__reference.task_done() def wait_response(self) -> typing.Any: - return self.__reference.get(timeout=MercuryClient.mercury_request_timeout) + return self.__reference.get( + timeout=MercuryClient.mercury_request_timeout) class MercuryRequests: @@ -298,7 +306,8 @@ class MercuryRequests: return JsonMercuryRequest( RawMercuryRequest.get( "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}" - .format(scope, MercuryRequests.keymaster_client_id, device_id))) + .format(scope, MercuryRequests.keymaster_client_id, + device_id))) class RawMercuryRequest: @@ -311,15 +320,18 @@ class RawMercuryRequest: @staticmethod def sub(uri: str): - return RawMercuryRequest.new_builder().set_uri(uri).set_method("SUB").build() + return RawMercuryRequest.new_builder().set_uri(uri).set_method( + "SUB").build() @staticmethod def unsub(uri: str): - return RawMercuryRequest.new_builder().set_uri(uri).set_method("UNSUB").build() + return RawMercuryRequest.new_builder().set_uri(uri).set_method( + "UNSUB").build() @staticmethod def get(uri: str): - return RawMercuryRequest.new_builder().set_uri(uri).set_method("GET").build() + return RawMercuryRequest.new_builder().set_uri(uri).set_method( + "GET").build() @staticmethod def send(uri: str, part: bytes): diff --git a/librespot/metadata.py b/librespot/metadata.py index 290c9d1..adc3c22 100644 --- a/librespot/metadata.py +++ b/librespot/metadata.py @@ -135,8 +135,7 @@ class ArtistId(SpotifyId): @staticmethod def from_base62(base62: str) -> ArtistId: - return ArtistId(util.bytes_to_hex(ArtistId.base62.decode(base62, - 16))) + return ArtistId(util.bytes_to_hex(ArtistId.base62.decode(base62, 16))) @staticmethod def from_hex(hex_str: str) -> ArtistId: @@ -205,8 +204,7 @@ class ShowId(SpotifyId): matcher = ShowId.pattern.search(uri) if matcher is not None: show_id = matcher.group(1) - return ShowId( - util.bytes_to_hex(ShowId.base62.decode(show_id, 16))) + return ShowId(util.bytes_to_hex(ShowId.base62.decode(show_id, 16))) raise TypeError("Not a Spotify show ID: {}".format(uri)) @staticmethod diff --git a/librespot/player/__init__.py b/librespot/player/__init__.py index 60282ce..ac83f4a 100644 --- a/librespot/player/__init__.py +++ b/librespot/player/__init__.py @@ -85,4 +85,3 @@ class PlayerConfiguration: self.initial_volume, self.volume_steps, ) - diff --git a/librespot/player/codecs.py b/librespot/player/codecs.py index 2405751..acadaf7 100644 --- a/librespot/player/codecs.py +++ b/librespot/player/codecs.py @@ -17,18 +17,24 @@ class VorbisOnlyAudioQuality(AudioQualityPicker): @staticmethod def get_vorbis_file(files: typing.List[Metadata.AudioFile]): for file in files: - if hasattr(file, "format") and SuperAudioFormat.get(file.format) == SuperAudioFormat.VORBIS: + if hasattr(file, "format") and SuperAudioFormat.get( + file.format) == SuperAudioFormat.VORBIS: return file return None def get_file(self, files: typing.List[Metadata.AudioFile]): - matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches(files) - vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(matches) + matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches( + files) + vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file( + matches) if vorbis is None: - vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(files) + vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file( + files) if vorbis is not None: - self.logger.warning("Using {} because preferred {} couldn't be found." - .format(vorbis.format, self.preferred)) + self.logger.warning( + "Using {} because preferred {} couldn't be found.".format( + vorbis.format, self.preferred)) else: - self.logger.fatal("Couldn't find any Vorbis file, available: {}") + self.logger.fatal( + "Couldn't find any Vorbis file, available: {}") return vorbis diff --git a/librespot/structure.py b/librespot/structure.py index f97f341..7519729 100644 --- a/librespot/structure.py +++ b/librespot/structure.py @@ -18,7 +18,8 @@ class AudioDecrypt: class AudioQualityPicker: - def get_file(self, files: typing.List[Metadata.AudioFile]) -> Metadata.AudioFile: + def get_file(self, + files: typing.List[Metadata.AudioFile]) -> Metadata.AudioFile: raise NotImplementedError diff --git a/librespot/util.py b/librespot/util.py index bde34d7..bdb93a2 100644 --- a/librespot/util.py +++ b/librespot/util.py @@ -52,20 +52,24 @@ class Base62: return Base62(Base62.CharacterSets.inverted) def encode(self, message: bytes, length: int = -1): - indices = self.convert(message, self.standard_base, self.target_base, length) + indices = self.convert(message, self.standard_base, self.target_base, + length) return self.translate(indices, self.alphabet) def decode(self, encoded: bytes, length: int = -1): prepared = self.translate(encoded, self.lookup) - return self.convert(prepared, self.target_base, self.standard_base, length) + return self.convert(prepared, self.target_base, self.standard_base, + length) def translate(self, indices: bytes, dictionary: bytes): translation = bytearray(len(indices)) for i in range(len(indices)): - translation[i] = dictionary[int.from_bytes(indices[i].encode(),"big")] + translation[i] = dictionary[int.from_bytes(indices[i].encode(), + "big")] return translation - def convert(self, message: bytes, source_base: int, target_base: int, length: int): + def convert(self, message: bytes, source_base: int, target_base: int, + length: int): estimated_length = self.estimate_output_length( len(message), source_base, target_base) if length == -1 else length out = b"" @@ -91,9 +95,11 @@ class Base62: return self.reverse(out[:estimated_length]) return self.reverse(out) - def estimate_output_length(self, input_length: int, source_base: int, target_base: int): + def estimate_output_length(self, input_length: int, source_base: int, + target_base: int): return int( - math.ceil((math.log(source_base) / math.log(target_base)) * input_length)) + math.ceil((math.log(source_base) / math.log(target_base)) * + input_length)) def reverse(self, arr: bytes): length = len(arr) diff --git a/setup.py b/setup.py index b78959e..478d19a 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,20 @@ import setuptools -setuptools.setup( - name="librespot", - version="0.0.1", - description="Open Source Spotify Client", - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - author="kokarare1212", - url="https://github.com/kokarare1212/librespot-python", - license="Apache-2.0", - packages=setuptools.find_packages("."), - install_requires=["defusedxml", "protobuf", "pycryptodomex", "pyogg", "requests"], - classifiers=[ - "Development Status :: 1 - Planning", - "License :: OSI Approved :: Apache Software License", - "Topic :: Multimedia :: Sound/Audio" - ]) +setuptools.setup(name="librespot", + version="0.0.1", + description="Open Source Spotify Client", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="kokarare1212", + url="https://github.com/kokarare1212/librespot-python", + license="Apache-2.0", + packages=setuptools.find_packages("."), + install_requires=[ + "defusedxml", "protobuf", "pycryptodomex", "pyogg", + "requests" + ], + classifiers=[ + "Development Status :: 1 - Planning", + "License :: OSI Approved :: Apache Software License", + "Topic :: Multimedia :: Sound/Audio" + ])