Restyled by yapf
This commit is contained in:
@@ -33,18 +33,15 @@ def client():
|
||||
return
|
||||
if (args[0] == "p" or args[0] == "play") and len(args) == 2:
|
||||
track_uri_search = re.search(
|
||||
r"^spotify:track:(?P<TrackID>[0-9a-zA-Z]{22})$", args[1]
|
||||
)
|
||||
r"^spotify:track:(?P<TrackID>[0-9a-zA-Z]{22})$", args[1])
|
||||
track_url_search = re.search(
|
||||
r"^(https?://)?open.spotify.com/track/(?P<TrackID>[0-9a-zA-Z]{22})(\?si=.+?)?$",
|
||||
args[1],
|
||||
)
|
||||
if track_uri_search is not None or track_url_search is not None:
|
||||
track_id_str = (
|
||||
track_uri_search
|
||||
if track_uri_search is not None
|
||||
else track_url_search
|
||||
).group("TrackID")
|
||||
track_id_str = (track_uri_search
|
||||
if track_uri_search is not None else
|
||||
track_url_search).group("TrackID")
|
||||
play(track_id_str)
|
||||
wait()
|
||||
if args[0] == "q" or args[0] == "quality":
|
||||
@@ -64,20 +61,22 @@ def client():
|
||||
token = session.tokens().get("user-read-email")
|
||||
resp = requests.get(
|
||||
"https://api.spotify.com/v1/search",
|
||||
{"limit": "5", "offset": "0", "q": cmd[2:], "type": "track"},
|
||||
{
|
||||
"limit": "5",
|
||||
"offset": "0",
|
||||
"q": cmd[2:],
|
||||
"type": "track"
|
||||
},
|
||||
headers={"Authorization": "Bearer %s" % token},
|
||||
)
|
||||
i = 1
|
||||
tracks = resp.json()["tracks"]["items"]
|
||||
for track in tracks:
|
||||
print(
|
||||
"%d, %s | %s"
|
||||
% (
|
||||
print("%d, %s | %s" % (
|
||||
i,
|
||||
track["name"],
|
||||
",".join([artist["name"] for artist in track["artists"]]),
|
||||
)
|
||||
)
|
||||
))
|
||||
i += 1
|
||||
position = -1
|
||||
while True:
|
||||
@@ -115,8 +114,7 @@ def login():
|
||||
def play(track_id_str: str):
|
||||
track_id = TrackId.from_base62(track_id_str)
|
||||
stream = session.content_feeder().load(
|
||||
track_id, VorbisOnlyAudioQuality(AudioQuality.VERY_HIGH), False, None
|
||||
)
|
||||
track_id, VorbisOnlyAudioQuality(AudioQuality.VERY_HIGH), False, None)
|
||||
ffplay = subprocess.Popen(
|
||||
["ffplay", "-"],
|
||||
stdin=subprocess.PIPE,
|
||||
@@ -132,13 +130,11 @@ def play(track_id_str: str):
|
||||
|
||||
|
||||
def splash():
|
||||
print(
|
||||
"=================================\n"
|
||||
print("=================================\n"
|
||||
"| Librespot-Python Player |\n"
|
||||
"| |\n"
|
||||
"| by kokarare1212 |\n"
|
||||
"=================================\n\n\n"
|
||||
)
|
||||
"=================================\n\n\n")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -22,7 +22,9 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
||||
_decoded_length: int = 0
|
||||
|
||||
def __init__(self, retry_on_chunk_error: bool):
|
||||
self.retries: typing.Final[typing.List[int]] = [0 for _ in range(self.chunks())]
|
||||
self.retries: typing.Final[typing.List[int]] = [
|
||||
0 for _ in range(self.chunks())
|
||||
]
|
||||
self.retry_on_chunk_error = retry_on_chunk_error
|
||||
|
||||
def is_closed(self) -> bool:
|
||||
@@ -107,13 +109,10 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
||||
self.request_chunk_from_stream(chunk)
|
||||
self.requested_chunks()[chunk] = True
|
||||
|
||||
for i in range(
|
||||
chunk + 1, min(self.chunks() - 1, chunk + self.preload_ahead) + 1
|
||||
):
|
||||
if (
|
||||
self.requested_chunks()[i]
|
||||
and self.retries[i] < self.preload_chunk_retries
|
||||
):
|
||||
for i in range(chunk + 1,
|
||||
min(self.chunks() - 1, chunk + self.preload_ahead) + 1):
|
||||
if (self.requested_chunks()[i]
|
||||
and self.retries[i] < self.preload_chunk_retries):
|
||||
self.request_chunk_from_stream(i)
|
||||
self.requested_chunks()[chunk] = True
|
||||
|
||||
@@ -147,7 +146,10 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
||||
|
||||
self.check_availability(chunk, True, True)
|
||||
|
||||
def read(self, b: bytearray = None, offset: int = None, length: int = None) -> int:
|
||||
def read(self,
|
||||
b: bytearray = None,
|
||||
offset: int = None,
|
||||
length: int = None) -> int:
|
||||
if b is None and offset is None and length is None:
|
||||
return self.internal_read()
|
||||
if not (b is not None and offset is not None and length is not None):
|
||||
@@ -157,9 +159,8 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
||||
raise IOError("Stream is closed!")
|
||||
|
||||
if offset < 0 or length < 0 or length > len(b) - offset:
|
||||
raise IndexError(
|
||||
"offset: {}, length: {}, buffer: {}".format(offset, length, len(b))
|
||||
)
|
||||
raise IndexError("offset: {}, length: {}, buffer: {}".format(
|
||||
offset, length, len(b)))
|
||||
elif length == 0:
|
||||
return 0
|
||||
|
||||
@@ -174,7 +175,8 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
||||
self.check_availability(chunk, True, False)
|
||||
|
||||
copy = min(len(self.buffer()[chunk]) - chunk_off, length - i)
|
||||
b[offset + 0 : copy] = self.buffer()[chunk][chunk_off : chunk_off + copy]
|
||||
b[offset + 0:copy] = self.buffer()[chunk][chunk_off:chunk_off +
|
||||
copy]
|
||||
i += copy
|
||||
self._pos += copy
|
||||
|
||||
@@ -222,5 +224,4 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
||||
@staticmethod
|
||||
def from_stream_error(stream_error: int):
|
||||
return AbsChunkedInputStream.ChunkException(
|
||||
"Failed due to stream error, code: {}".format(stream_error)
|
||||
)
|
||||
"Failed due to stream error, code: {}".format(stream_error))
|
||||
|
||||
@@ -40,7 +40,8 @@ class CdnFeedHelper:
|
||||
|
||||
streamer = session.cdn().stream_file(file, key, url, halt_listener)
|
||||
input_stream = streamer.stream()
|
||||
normalization_data = NormalizationData.NormalizationData.read(input_stream)
|
||||
normalization_data = NormalizationData.NormalizationData.read(
|
||||
input_stream)
|
||||
if input_stream.skip(0xA7) != 0xA7:
|
||||
raise IOError("Couldn't skip 0xa7 bytes!")
|
||||
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
||||
@@ -48,13 +49,13 @@ class CdnFeedHelper:
|
||||
streamer,
|
||||
normalization_data,
|
||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
||||
file.file_id, preload, -1 if preload else audio_key_time
|
||||
),
|
||||
file.file_id, preload, -1 if preload else audio_key_time),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def load_episode_external(
|
||||
session: Session, episode: Metadata.Episode, halt_listener: HaltListener
|
||||
session: Session, episode: Metadata.Episode,
|
||||
halt_listener: HaltListener
|
||||
) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
||||
resp = session.client().head(episode.external_url)
|
||||
|
||||
@@ -62,18 +63,17 @@ class CdnFeedHelper:
|
||||
CdnFeedHelper._LOGGER.warning("Couldn't resolve redirect!")
|
||||
|
||||
url = resp.url
|
||||
CdnFeedHelper._LOGGER.debug(
|
||||
"Fetched external url for {}: {}".format(
|
||||
Utils.Utils.bytes_to_hex(episode.gid), url
|
||||
)
|
||||
)
|
||||
CdnFeedHelper._LOGGER.debug("Fetched external url for {}: {}".format(
|
||||
Utils.Utils.bytes_to_hex(episode.gid), url))
|
||||
|
||||
streamer = session.cdn().stream_external_episode(episode, url, halt_listener)
|
||||
streamer = session.cdn().stream_external_episode(
|
||||
episode, url, halt_listener)
|
||||
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
||||
episode,
|
||||
streamer,
|
||||
None,
|
||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(None, False, -1),
|
||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
||||
None, False, -1),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -102,6 +102,5 @@ class CdnFeedHelper:
|
||||
streamer,
|
||||
normalization_data,
|
||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
||||
file.file_id, False, audio_key_time
|
||||
),
|
||||
file.file_id, False, audio_key_time),
|
||||
)
|
||||
|
||||
@@ -37,9 +37,9 @@ class CdnManager:
|
||||
def get_head(self, file_id: bytes):
|
||||
resp = self._session.client().get(
|
||||
self._session.get_user_attribute(
|
||||
"head-files-url", "https://heads-fa.spotify.com/head/{file_id}"
|
||||
).replace("{file_id}", Utils.bytes_to_hex(file_id))
|
||||
)
|
||||
"head-files-url",
|
||||
"https://heads-fa.spotify.com/head/{file_id}").replace(
|
||||
"{file_id}", Utils.bytes_to_hex(file_id)))
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise IOError("{}".format(resp.status_code))
|
||||
@@ -50,9 +50,9 @@ class CdnManager:
|
||||
|
||||
return body
|
||||
|
||||
def stream_external_episode(
|
||||
self, episode: Metadata.Episode, external_url: str, halt_listener: HaltListener
|
||||
):
|
||||
def stream_external_episode(self, episode: Metadata.Episode,
|
||||
external_url: str,
|
||||
halt_listener: HaltListener):
|
||||
return CdnManager.Streamer(
|
||||
self._session,
|
||||
StreamId(episode),
|
||||
@@ -84,8 +84,7 @@ class CdnManager:
|
||||
resp = self._session.api().send(
|
||||
"GET",
|
||||
"/storage-resolve/files/audio/interactive/{}".format(
|
||||
Utils.bytes_to_hex(file_id)
|
||||
),
|
||||
Utils.bytes_to_hex(file_id)),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -101,13 +100,11 @@ class CdnManager:
|
||||
proto.ParseFromString(body)
|
||||
if proto.result == StorageResolve.StorageResolveResponse.Result.CDN:
|
||||
url = random.choice(proto.cdnurl)
|
||||
self._LOGGER.debug(
|
||||
"Fetched CDN url for {}: {}".format(Utils.bytes_to_hex(file_id), url)
|
||||
)
|
||||
self._LOGGER.debug("Fetched CDN url for {}: {}".format(
|
||||
Utils.bytes_to_hex(file_id), url))
|
||||
return url
|
||||
raise CdnManager.CdnException(
|
||||
"Could not retrieve CDN url! result: {}".format(proto.result)
|
||||
)
|
||||
"Could not retrieve CDN url! result: {}".format(proto.result))
|
||||
|
||||
class CdnException(Exception):
|
||||
pass
|
||||
@@ -167,8 +164,7 @@ class CdnManager:
|
||||
if expire_at is None:
|
||||
self._expiration = -1
|
||||
self._cdnManager._LOGGER.warning(
|
||||
"Invalid __token__ in CDN url: {}".format(url)
|
||||
)
|
||||
"Invalid __token__ in CDN url: {}".format(url))
|
||||
return
|
||||
|
||||
self._expiration = expire_at * 1000
|
||||
@@ -178,10 +174,8 @@ class CdnManager:
|
||||
except ValueError:
|
||||
self._expiration = -1
|
||||
self._cdnManager._LOGGER.warning(
|
||||
"Couldn't extract expiration, invalid parameter in CDN url: ".format(
|
||||
url
|
||||
)
|
||||
)
|
||||
"Couldn't extract expiration, invalid parameter in CDN url: "
|
||||
.format(url))
|
||||
return
|
||||
|
||||
self._expiration = int(token_url.query[:i]) * 1000
|
||||
@@ -224,38 +218,39 @@ class CdnManager:
|
||||
self._cdnUrl = cdn_url
|
||||
self._haltListener = halt_listener
|
||||
|
||||
resp = self.request(range_start=0, range_end=ChannelManager.CHUNK_SIZE - 1)
|
||||
resp = self.request(range_start=0,
|
||||
range_end=ChannelManager.CHUNK_SIZE - 1)
|
||||
content_range = resp._headers.get("Content-Range")
|
||||
if content_range is None:
|
||||
raise IOError("Missing Content-Range header!")
|
||||
|
||||
split = Utils.split(content_range, "/")
|
||||
self._size = int(split[1])
|
||||
self._chunks = int(math.ceil(self._size / ChannelManager.CHUNK_SIZE))
|
||||
self._chunks = int(
|
||||
math.ceil(self._size / ChannelManager.CHUNK_SIZE))
|
||||
|
||||
first_chunk = resp._buffer
|
||||
|
||||
self._available = [False for _ in range(self._chunks)]
|
||||
self._requested = [False for _ in range(self._chunks)]
|
||||
self._buffer = [bytearray() for _ in range(self._chunks)]
|
||||
self._internalStream = CdnManager.Streamer.InternalStream(self, False)
|
||||
self._internalStream = CdnManager.Streamer.InternalStream(
|
||||
self, False)
|
||||
|
||||
self._requested[0] = True
|
||||
self.write_chunk(first_chunk, 0, False)
|
||||
|
||||
def write_chunk(self, chunk: bytes, chunk_index: int, cached: bool) -> None:
|
||||
def write_chunk(self, chunk: bytes, chunk_index: int,
|
||||
cached: bool) -> None:
|
||||
if self._internalStream.is_closed():
|
||||
return
|
||||
|
||||
self._session._LOGGER.debug(
|
||||
"Chunk {}/{} completed, cached: {}, stream: {}".format(
|
||||
chunk_index + 1, self._chunks, cached, self.describe()
|
||||
)
|
||||
)
|
||||
chunk_index + 1, self._chunks, cached, self.describe()))
|
||||
|
||||
self._buffer[chunk_index] = self._audioDecrypt.decrypt_chunk(
|
||||
chunk_index, chunk
|
||||
)
|
||||
chunk_index, chunk)
|
||||
self._internalStream.notify_chunk_available(chunk_index)
|
||||
|
||||
def stream(self) -> AbsChunkedInputStream:
|
||||
@@ -266,7 +261,8 @@ class CdnManager:
|
||||
|
||||
def describe(self) -> str:
|
||||
if self._streamId.is_episode():
|
||||
return "episode_gid: {}".format(self._streamId.get_episode_gid())
|
||||
return "episode_gid: {}".format(
|
||||
self._streamId.get_episode_gid())
|
||||
return "file_id: {}".format(self._streamId.get_file_id())
|
||||
|
||||
def decrypt_time_ms(self) -> int:
|
||||
@@ -276,9 +272,10 @@ class CdnManager:
|
||||
resp = self.request(index)
|
||||
self.write_chunk(resp._buffer, index, False)
|
||||
|
||||
def request(
|
||||
self, chunk: int = None, range_start: int = None, range_end: int = None
|
||||
) -> CdnManager.InternalResponse:
|
||||
def request(self,
|
||||
chunk: int = None,
|
||||
range_start: int = None,
|
||||
range_end: int = None) -> CdnManager.InternalResponse:
|
||||
if chunk is None and range_start is None and range_end is None:
|
||||
raise TypeError()
|
||||
|
||||
@@ -288,7 +285,9 @@ class CdnManager:
|
||||
|
||||
resp = self._session.client().get(
|
||||
self._cdnUrl._url,
|
||||
headers={"Range": "bytes={}-{}".format(range_start, range_end)},
|
||||
headers={
|
||||
"Range": "bytes={}-{}".format(range_start, range_end)
|
||||
},
|
||||
)
|
||||
|
||||
if resp.status_code != 206:
|
||||
@@ -324,21 +323,16 @@ class CdnManager:
|
||||
|
||||
def request_chunk_from_stream(self, index: int) -> None:
|
||||
self.streamer._executorService.submit(
|
||||
lambda: self.streamer.request_chunk(index)
|
||||
)
|
||||
lambda: self.streamer.request_chunk(index))
|
||||
|
||||
def stream_read_halted(self, chunk: int, _time: int) -> None:
|
||||
if self.streamer._haltListener is not None:
|
||||
self.streamer._executorService.submit(
|
||||
lambda: self.streamer._haltListener.stream_read_halted(
|
||||
chunk, _time
|
||||
)
|
||||
)
|
||||
chunk, _time))
|
||||
|
||||
def stream_read_resumed(self, chunk: int, _time: int) -> None:
|
||||
if self.streamer._haltListener is not None:
|
||||
self.streamer._executorService.submit(
|
||||
lambda: self.streamer._haltListener.stream_read_resumed(
|
||||
chunk, _time
|
||||
)
|
||||
)
|
||||
lambda: self.streamer._haltListener.
|
||||
stream_read_resumed(chunk, _time))
|
||||
|
||||
@@ -47,8 +47,7 @@ from librespot.Version import Version
|
||||
|
||||
class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
_LOGGER: logging = logging.getLogger(__name__)
|
||||
_serverKey: bytes = bytes(
|
||||
[
|
||||
_serverKey: bytes = bytes([
|
||||
0xAC,
|
||||
0xE0,
|
||||
0x46,
|
||||
@@ -305,8 +304,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
0xE6,
|
||||
0x55,
|
||||
0xBD,
|
||||
]
|
||||
)
|
||||
])
|
||||
_keys: DiffieHellman = None
|
||||
_inner: Session.Inner = None
|
||||
_scheduler: sched.scheduler = sched.scheduler(time.time)
|
||||
@@ -344,9 +342,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
self._conn = Session.ConnectionHolder.create(addr, inner.conf)
|
||||
self._client = Session._create_client(self._inner.conf)
|
||||
|
||||
self._LOGGER.info(
|
||||
"Created new session! device_id: {}, ap: {}".format(inner.device_id, addr)
|
||||
)
|
||||
self._LOGGER.info("Created new session! device_id: {}, ap: {}".format(
|
||||
inner.device_id, addr))
|
||||
|
||||
@staticmethod
|
||||
def _create_client(conf: Session.Configuration) -> requests.Session:
|
||||
@@ -388,12 +385,12 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
|
||||
client_hello = Keyexchange.ClientHello(
|
||||
build_info=Version.standard_build_info(),
|
||||
cryptosuites_supported=[Keyexchange.Cryptosuite.CRYPTO_SUITE_SHANNON],
|
||||
cryptosuites_supported=[
|
||||
Keyexchange.Cryptosuite.CRYPTO_SUITE_SHANNON
|
||||
],
|
||||
login_crypto_hello=Keyexchange.LoginCryptoHelloUnion(
|
||||
diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanHello(
|
||||
gc=self._keys.public_key_array(), server_keys_known=1
|
||||
),
|
||||
),
|
||||
gc=self._keys.public_key_array(), server_keys_known=1), ),
|
||||
client_nonce=nonce,
|
||||
padding=bytes([0x1E]),
|
||||
)
|
||||
@@ -422,22 +419,21 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
ap_response_message.ParseFromString(buffer)
|
||||
shared_key = Utils.to_byte_array(
|
||||
self._keys.compute_shared_key(
|
||||
ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs
|
||||
)
|
||||
)
|
||||
ap_response_message.challenge.login_crypto_challenge.
|
||||
diffie_hellman.gs))
|
||||
|
||||
# Check gs_signature
|
||||
|
||||
rsa = RSA.construct((int.from_bytes(self._serverKey, "big"), 65537))
|
||||
pkcs1_v1_5 = PKCS1_v1_5.new(rsa)
|
||||
sha1 = SHA1.new()
|
||||
sha1.update(
|
||||
ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs
|
||||
)
|
||||
sha1.update(ap_response_message.challenge.login_crypto_challenge.
|
||||
diffie_hellman.gs)
|
||||
# noinspection PyTypeChecker
|
||||
if not pkcs1_v1_5.verify(
|
||||
sha1,
|
||||
ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs_signature,
|
||||
ap_response_message.challenge.login_crypto_challenge.
|
||||
diffie_hellman.gs_signature,
|
||||
):
|
||||
raise RuntimeError("Failed signature check!")
|
||||
|
||||
@@ -460,14 +456,13 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
client_response_plaintext = Keyexchange.ClientResponsePlaintext(
|
||||
login_crypto_response=Keyexchange.LoginCryptoResponseUnion(
|
||||
diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse(
|
||||
hmac=challenge
|
||||
)
|
||||
),
|
||||
hmac=challenge)),
|
||||
pow_response=Keyexchange.PoWResponseUnion(),
|
||||
crypto_response=Keyexchange.CryptoResponseUnion(),
|
||||
)
|
||||
|
||||
client_response_plaintext_bytes = client_response_plaintext.SerializeToString()
|
||||
client_response_plaintext_bytes = client_response_plaintext.SerializeToString(
|
||||
)
|
||||
length = 4 + len(client_response_plaintext_bytes)
|
||||
self._conn.write_int(length)
|
||||
self._conn.write(client_response_plaintext_bytes)
|
||||
@@ -477,12 +472,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
self._conn.set_timeout(1)
|
||||
scrap = self._conn.read(4)
|
||||
if 4 == len(scrap):
|
||||
length = (
|
||||
(scrap[0] << 24)
|
||||
length = ((scrap[0] << 24)
|
||||
| (scrap[1] << 16)
|
||||
| (scrap[2] << 8)
|
||||
| (scrap[3] & 0xFF)
|
||||
)
|
||||
| (scrap[3] & 0xFF))
|
||||
payload = self._conn.read(length - 4)
|
||||
failed = Keyexchange.APResponseMessage()
|
||||
failed.ParseFromString(payload)
|
||||
@@ -499,7 +492,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
|
||||
self._LOGGER.info("Connection successfully!")
|
||||
|
||||
def _authenticate(self, credentials: Authentication.LoginCredentials) -> None:
|
||||
def _authenticate(self,
|
||||
credentials: Authentication.LoginCredentials) -> None:
|
||||
self._authenticate_partial(credentials, False)
|
||||
|
||||
with self._authLock:
|
||||
@@ -509,7 +503,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
self._channelManager = ChannelManager(self)
|
||||
self._api = ApiClient.ApiClient(self)
|
||||
self._cdnManager = CdnManager(self)
|
||||
self._contentFeeder = PlayableContentFeeder.PlayableContentFeeder(self)
|
||||
self._contentFeeder = PlayableContentFeeder.PlayableContentFeeder(
|
||||
self)
|
||||
self._cacheManager = CacheManager(self)
|
||||
self._dealer = DealerClient(self)
|
||||
self._search = SearchManager.SearchManager(self)
|
||||
@@ -522,15 +517,15 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
# TimeProvider.init(self)
|
||||
self._dealer.connect()
|
||||
|
||||
self._LOGGER.info(
|
||||
"Authenticated as {}!".format(self._apWelcome.canonical_username)
|
||||
)
|
||||
self._LOGGER.info("Authenticated as {}!".format(
|
||||
self._apWelcome.canonical_username))
|
||||
self.mercury().interested_in("spotify:user:attributes:update", self)
|
||||
self.dealer().add_message_listener(self, "hm://connect-state/v1/connect/logout")
|
||||
self.dealer().add_message_listener(
|
||||
self, "hm://connect-state/v1/connect/logout")
|
||||
|
||||
def _authenticate_partial(
|
||||
self, credentials: Authentication.LoginCredentials, remove_lock: bool
|
||||
) -> None:
|
||||
def _authenticate_partial(self,
|
||||
credentials: Authentication.LoginCredentials,
|
||||
remove_lock: bool) -> None:
|
||||
if self._cipherPair is None:
|
||||
raise RuntimeError("Connection not established!")
|
||||
|
||||
@@ -545,9 +540,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
version_string=Version.version_string(),
|
||||
)
|
||||
|
||||
self._send_unchecked(
|
||||
Packet.Type.login, client_response_encrypted.SerializeToString()
|
||||
)
|
||||
self._send_unchecked(Packet.Type.login,
|
||||
client_response_encrypted.SerializeToString())
|
||||
|
||||
packet = self._cipherPair.receive_encoded(self._conn)
|
||||
if packet.is_cmd(Packet.Type.ap_welcome):
|
||||
@@ -563,7 +557,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
preferred_locale += bytes([0x00, 0x00, 0x10, 0x00, 0x02])
|
||||
preferred_locale += "preferred-locale".encode()
|
||||
preferred_locale += self._inner.preferred_locale.encode()
|
||||
self._send_unchecked(Packet.Type.preferred_locale, preferred_locale)
|
||||
self._send_unchecked(Packet.Type.preferred_locale,
|
||||
preferred_locale)
|
||||
|
||||
if remove_lock:
|
||||
with self._authLock:
|
||||
@@ -573,8 +568,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
if self._inner.conf.store_credentials:
|
||||
reusable = self._apWelcome.reusable_auth_credentials
|
||||
reusable_type = Authentication.AuthenticationType.Name(
|
||||
self._apWelcome.reusable_auth_credentials_type
|
||||
)
|
||||
self._apWelcome.reusable_auth_credentials_type)
|
||||
|
||||
if self._inner.conf.stored_credentials_file is None:
|
||||
raise TypeError()
|
||||
@@ -597,9 +591,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
raise RuntimeError("Unknown CMD 0x" + packet.cmd.hex())
|
||||
|
||||
def close(self) -> None:
|
||||
self._LOGGER.info(
|
||||
"Closing session. device_id: {}".format(self._inner.device_id)
|
||||
)
|
||||
self._LOGGER.info("Closing session. device_id: {}".format(
|
||||
self._inner.device_id))
|
||||
|
||||
self._closing = True
|
||||
|
||||
@@ -652,9 +645,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
listener.on_closed()
|
||||
self._closeListeners: typing.List[Session.CloseListener] = []
|
||||
|
||||
self._reconnectionListeners: typing.List[Session.ReconnectionListener] = []
|
||||
self._reconnectionListeners: typing.List[
|
||||
Session.ReconnectionListener] = []
|
||||
|
||||
self._LOGGER.info("Closed session. device_id: {}".format(self._inner.device_id))
|
||||
self._LOGGER.info("Closed session. device_id: {}".format(
|
||||
self._inner.device_id))
|
||||
|
||||
def _send_unchecked(self, cmd: bytes, payload: bytes) -> None:
|
||||
self._cipherPair.send_encoded(self._conn, cmd, payload)
|
||||
@@ -797,8 +792,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
self._receiver.stop()
|
||||
|
||||
self._conn = Session.ConnectionHolder.create(
|
||||
ApResolver.get_random_accesspoint(), self._inner.conf
|
||||
)
|
||||
ApResolver.get_random_accesspoint(), self._inner.conf)
|
||||
self._connect()
|
||||
self._authenticate_partial(
|
||||
Authentication.LoginCredentials(
|
||||
@@ -809,9 +803,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
True,
|
||||
)
|
||||
|
||||
self._LOGGER.info(
|
||||
"Re-authenticated as {}!".format(self._apWelcome.canonical_username)
|
||||
)
|
||||
self._LOGGER.info("Re-authenticated as {}!".format(
|
||||
self._apWelcome.canonical_username))
|
||||
|
||||
with self._reconnectionListenersLock:
|
||||
for listener in self._reconnectionListeners:
|
||||
@@ -821,11 +814,13 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
if listener not in self._closeListeners:
|
||||
self._closeListeners.append(listener)
|
||||
|
||||
def add_reconnection_listener(self, listener: ReconnectionListener) -> None:
|
||||
def add_reconnection_listener(self,
|
||||
listener: ReconnectionListener) -> None:
|
||||
if listener not in self._reconnectionListeners:
|
||||
self._reconnectionListeners.append(listener)
|
||||
|
||||
def remove_reconnection_listener(self, listener: ReconnectionListener) -> None:
|
||||
def remove_reconnection_listener(self,
|
||||
listener: ReconnectionListener) -> None:
|
||||
self._reconnectionListeners.remove(listener)
|
||||
|
||||
def _parse_product_info(self, data) -> None:
|
||||
@@ -842,14 +837,12 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
for i in range(len(product)):
|
||||
self._userAttributes[product[i].tag] = product[i].text
|
||||
|
||||
self._LOGGER.debug("Parsed product info: {}".format(self._userAttributes))
|
||||
self._LOGGER.debug("Parsed product info: {}".format(
|
||||
self._userAttributes))
|
||||
|
||||
def get_user_attribute(self, key: str, fallback: str = None) -> str:
|
||||
return (
|
||||
self._userAttributes.get(key)
|
||||
if self._userAttributes.get(key) is not None
|
||||
else fallback
|
||||
)
|
||||
return (self._userAttributes.get(key)
|
||||
if self._userAttributes.get(key) is not None else fallback)
|
||||
|
||||
def event(self, resp: MercuryClient.Response) -> None:
|
||||
if resp.uri == "spotify:user:attributes:update":
|
||||
@@ -858,13 +851,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
|
||||
for pair in attributes_update.pairs_list:
|
||||
self._userAttributes[pair.key] = pair.value
|
||||
self._LOGGER.info(
|
||||
"Updated user attribute: {} -> {}".format(pair.key, pair.value)
|
||||
)
|
||||
self._LOGGER.info("Updated user attribute: {} -> {}".format(
|
||||
pair.key, pair.value))
|
||||
|
||||
def on_message(
|
||||
self, uri: str, headers: typing.Dict[str, str], payload: bytes
|
||||
) -> None:
|
||||
def on_message(self, uri: str, headers: typing.Dict[str, str],
|
||||
payload: bytes) -> None:
|
||||
if uri == "hm://connect-state/v1/connect/logout":
|
||||
self.close()
|
||||
|
||||
@@ -898,9 +889,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
self.conf = conf
|
||||
self.device_type = device_type
|
||||
self.device_name = device_name
|
||||
self.device_id = (
|
||||
device_id if device_id is not None else Utils.random_hex_string(40)
|
||||
)
|
||||
self.device_id = (device_id if device_id is not None else
|
||||
Utils.random_hex_string(40))
|
||||
|
||||
class AbsBuilder:
|
||||
conf = None
|
||||
@@ -934,8 +924,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
return self
|
||||
|
||||
def set_device_type(
|
||||
self, device_type: Connect.DeviceType
|
||||
) -> Session.AbsBuilder:
|
||||
self, device_type: Connect.DeviceType) -> Session.AbsBuilder:
|
||||
self.device_type = device_type
|
||||
return self
|
||||
|
||||
@@ -945,7 +934,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
def stored(self):
|
||||
pass
|
||||
|
||||
def stored_file(self, stored_credentials: str = None) -> Session.Builder:
|
||||
def stored_file(self,
|
||||
stored_credentials: str = None) -> Session.Builder:
|
||||
if stored_credentials is None:
|
||||
stored_credentials = self.conf.stored_credentials_file
|
||||
if os.path.isfile(stored_credentials):
|
||||
@@ -957,7 +947,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
else:
|
||||
try:
|
||||
self.login_credentials = Authentication.LoginCredentials(
|
||||
typ=Authentication.AuthenticationType.Value(obj["type"]),
|
||||
typ=Authentication.AuthenticationType.Value(
|
||||
obj["type"]),
|
||||
username=obj["username"],
|
||||
auth_data=base64.b64decode(obj["credentials"]),
|
||||
)
|
||||
@@ -1061,64 +1052,66 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
|
||||
# Stored credentials
|
||||
store_credentials: bool = True
|
||||
stored_credentials_file: str = os.path.join(os.getcwd(), "credentials.json")
|
||||
stored_credentials_file: str = os.path.join(
|
||||
os.getcwd(), "credentials.json")
|
||||
|
||||
# Fetching
|
||||
retry_on_chunk_error: bool = None
|
||||
|
||||
def set_proxy_enabled(
|
||||
self, proxy_enabled: bool
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
proxy_enabled: bool) -> Session.Configuration.Builder:
|
||||
self.proxyEnabled = proxy_enabled
|
||||
return self
|
||||
|
||||
def set_proxy_type(
|
||||
self, proxy_type: Proxy.Type
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
proxy_type: Proxy.Type) -> Session.Configuration.Builder:
|
||||
self.proxyType = proxy_type
|
||||
return self
|
||||
|
||||
def set_proxy_address(
|
||||
self, proxy_address: str
|
||||
) -> Session.Configuration.Builder:
|
||||
self, proxy_address: str) -> Session.Configuration.Builder:
|
||||
self.proxyAddress = proxy_address
|
||||
return self
|
||||
|
||||
def set_proxy_auth(self, proxy_auth: bool) -> Session.Configuration.Builder:
|
||||
def set_proxy_auth(
|
||||
self, proxy_auth: bool) -> Session.Configuration.Builder:
|
||||
self.proxyAuth = proxy_auth
|
||||
return self
|
||||
|
||||
def set_proxy_username(
|
||||
self, proxy_username: str
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
proxy_username: str) -> Session.Configuration.Builder:
|
||||
self.proxyUsername = proxy_username
|
||||
return self
|
||||
|
||||
def set_proxy_password(
|
||||
self, proxy_password: str
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
proxy_password: str) -> Session.Configuration.Builder:
|
||||
self.proxyPassword = proxy_password
|
||||
return self
|
||||
|
||||
def set_cache_enabled(
|
||||
self, cache_enabled: bool
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
cache_enabled: bool) -> Session.Configuration.Builder:
|
||||
self.cache_enabled = cache_enabled
|
||||
return self
|
||||
|
||||
def set_cache_dir(self, cache_dir: str) -> Session.Configuration.Builder:
|
||||
def set_cache_dir(self,
|
||||
cache_dir: str) -> Session.Configuration.Builder:
|
||||
self.cache_dir = cache_dir
|
||||
return self
|
||||
|
||||
def set_do_cache_clean_up(
|
||||
self, do_cache_clean_up: bool
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
do_cache_clean_up: bool) -> Session.Configuration.Builder:
|
||||
self.do_cache_clean_up = do_cache_clean_up
|
||||
return self
|
||||
|
||||
def set_store_credentials(
|
||||
self, store_credentials: bool
|
||||
) -> Session.Configuration.Builder:
|
||||
self,
|
||||
store_credentials: bool) -> Session.Configuration.Builder:
|
||||
self.store_credentials = store_credentials
|
||||
return self
|
||||
|
||||
@@ -1153,7 +1146,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
|
||||
class SpotifyAuthenticationException(Exception):
|
||||
def __init__(self, login_failed: Keyexchange.APLoginFailed):
|
||||
super().__init__(Keyexchange.ErrorCode.Name(login_failed.error_code))
|
||||
super().__init__(
|
||||
Keyexchange.ErrorCode.Name(login_failed.error_code))
|
||||
|
||||
class Accumulator:
|
||||
buffer: bytes = bytes()
|
||||
@@ -1180,7 +1174,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
self.sock = sock
|
||||
|
||||
@staticmethod
|
||||
def create(addr: str, conf: Session.Configuration) -> Session.ConnectionHolder:
|
||||
def create(addr: str,
|
||||
conf: Session.Configuration) -> Session.ConnectionHolder:
|
||||
ap_addr = addr.split(":")[0]
|
||||
ap_port = int(addr.split(":")[1])
|
||||
if not conf.proxyEnabled or conf.proxyType is Proxy.Type.DIRECT:
|
||||
@@ -1192,9 +1187,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
sock = socket.socket()
|
||||
sock.connect((conf.proxyAddress, conf.proxyPort))
|
||||
|
||||
sock.send("CONNECT {}:{} HTTP/1.0\n".format(ap_addr, ap_port).encode())
|
||||
sock.send("CONNECT {}:{} HTTP/1.0\n".format(ap_addr,
|
||||
ap_port).encode())
|
||||
if conf.proxyAuth:
|
||||
sock.send("Proxy-Authorization: {}\n".format(None).encode())
|
||||
sock.send(
|
||||
"Proxy-Authorization: {}\n".format(None).encode())
|
||||
|
||||
sock.send(b"\n")
|
||||
|
||||
@@ -1259,21 +1256,18 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
try:
|
||||
# noinspection PyProtectedMember
|
||||
packet = self.session._cipherPair.receive_encoded(
|
||||
self.session._conn
|
||||
)
|
||||
self.session._conn)
|
||||
cmd = Packet.Type.parse(packet.cmd)
|
||||
if cmd is None:
|
||||
self.session._LOGGER.info(
|
||||
"Skipping unknown command cmd: 0x{}, payload: {}".format(
|
||||
Utils.bytes_to_hex(packet.cmd), packet.payload
|
||||
)
|
||||
)
|
||||
"Skipping unknown command cmd: 0x{}, payload: {}".
|
||||
format(Utils.bytes_to_hex(packet.cmd),
|
||||
packet.payload))
|
||||
continue
|
||||
except RuntimeError as ex:
|
||||
if self.running:
|
||||
self.session._LOGGER.fatal(
|
||||
"Failed reading packet! {}".format(ex)
|
||||
)
|
||||
"Failed reading packet! {}".format(ex))
|
||||
# noinspection PyProtectedMember
|
||||
self.session._reconnect()
|
||||
break
|
||||
@@ -1284,18 +1278,17 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
# noinspection PyProtectedMember
|
||||
if self.session._scheduledReconnect is not None:
|
||||
# noinspection PyProtectedMember
|
||||
self.session._scheduler.cancel(self.session._scheduledReconnect)
|
||||
self.session._scheduler.cancel(
|
||||
self.session._scheduledReconnect)
|
||||
|
||||
def anonymous():
|
||||
self.session._LOGGER.warning(
|
||||
"Socket timed out. Reconnecting..."
|
||||
)
|
||||
"Socket timed out. Reconnecting...")
|
||||
self.session._reconnect()
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
self.session.scheduled_reconnect = self.session._scheduler.enter(
|
||||
2 * 60 + 5, 1, anonymous
|
||||
)
|
||||
2 * 60 + 5, 1, anonymous)
|
||||
self.session.send(Packet.Type.pong, packet.payload)
|
||||
continue
|
||||
if cmd == Packet.Type.pong_ack:
|
||||
@@ -1303,8 +1296,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
if cmd == Packet.Type.country_code:
|
||||
self.session.country_code = packet.payload.decode()
|
||||
self.session._LOGGER.info(
|
||||
"Received country_code: {}".format(self.session.country_code)
|
||||
)
|
||||
"Received country_code: {}".format(
|
||||
self.session.country_code))
|
||||
continue
|
||||
if cmd == Packet.Type.license_version:
|
||||
license_version = BytesInputStream(packet.payload)
|
||||
@@ -1313,40 +1306,33 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||
buffer = license_version.read()
|
||||
self.session._LOGGER.info(
|
||||
"Received license_version: {}, {}".format(
|
||||
license_id, buffer.decode()
|
||||
)
|
||||
)
|
||||
license_id, buffer.decode()))
|
||||
else:
|
||||
self.session._LOGGER.info(
|
||||
"Received license_version: {}".format(license_id)
|
||||
)
|
||||
"Received license_version: {}".format(license_id))
|
||||
continue
|
||||
if cmd == Packet.Type.unknown_0x10:
|
||||
self.session._LOGGER.debug(
|
||||
"Received 0x10: {}".format(Utils.bytes_to_hex(packet.payload))
|
||||
)
|
||||
self.session._LOGGER.debug("Received 0x10: {}".format(
|
||||
Utils.bytes_to_hex(packet.payload)))
|
||||
continue
|
||||
if (
|
||||
cmd == Packet.Type.mercury_sub
|
||||
if (cmd == Packet.Type.mercury_sub
|
||||
or cmd == Packet.Type.mercury_unsub
|
||||
or cmd == Packet.Type.mercury_event
|
||||
or cmd == Packet.Type.mercury_req
|
||||
):
|
||||
or cmd == Packet.Type.mercury_req):
|
||||
self.session.mercury().dispatch(packet)
|
||||
continue
|
||||
if cmd == Packet.Type.aes_key or cmd == Packet.Type.aes_key_error:
|
||||
self.session.audio_key().dispatch(packet)
|
||||
continue
|
||||
if (
|
||||
cmd == Packet.Type.channel_error
|
||||
or cmd == Packet.Type.stream_chunk_res
|
||||
):
|
||||
if (cmd == Packet.Type.channel_error
|
||||
or cmd == Packet.Type.stream_chunk_res):
|
||||
self.session.channel().dispatch(packet)
|
||||
continue
|
||||
if cmd == Packet.Type.product_info:
|
||||
# noinspection PyProtectedMember
|
||||
self.session._parse_product_info(packet.payload)
|
||||
continue
|
||||
self.session._LOGGER.info("Skipping {}".format(Utils.bytes_to_hex(cmd)))
|
||||
self.session._LOGGER.info("Skipping {}".format(
|
||||
Utils.bytes_to_hex(cmd)))
|
||||
|
||||
self.session._LOGGER.debug("Session.Receiver stopped")
|
||||
|
||||
Reference in New Issue
Block a user