Format code with yapf

This commit fixes the style issues introduced in 2790f48 according to the output
from yapf.

Details: https://deepsource.io/gh/kokarare1212/librespot-python/transform/04a80a03-ea85-44b3-9159-2580bda68c1c/
This commit is contained in:
deepsource-autofix[bot] 2021-09-12 04:58:42 +00:00 committed by GitHub
parent 2790f484c8
commit 0741dbdd43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 396 additions and 237 deletions

View File

@ -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)

View File

@ -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("<f", buffer.read(4))[0], struct.unpack("<f", buffer.read(4))[0],
struct.unpack("<f", buffer.read(4))[0], struct.unpack("<f", buffer.read(4))[0])
return NormalizationData(
struct.unpack("<f", buffer.read(4))[0],
struct.unpack("<f", buffer.read(4))[0],
struct.unpack("<f", buffer.read(4))[0],
struct.unpack("<f", buffer.read(4))[0])
def get_factor(self, normalisation_pregain) -> 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:

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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:

View File

@ -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

View File

@ -9,5 +9,3 @@ import typing
if typing.TYPE_CHECKING:
from librespot.core import Session

View File

@ -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):

View File

@ -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

View File

@ -85,4 +85,3 @@ class PlayerConfiguration:
self.initial_volume,
self.volume_steps,
)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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"
])