Restyled by black
This commit is contained in:
@@ -31,14 +31,18 @@ def client():
|
|||||||
return
|
return
|
||||||
if (args[0] == "p" or args[0] == "play") and len(args) == 2:
|
if (args[0] == "p" or args[0] == "play") and len(args) == 2:
|
||||||
track_uri_search = re.search(
|
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(
|
track_url_search = re.search(
|
||||||
r"^(https?://)?open.spotify.com/track/(?P<TrackID>[0-9a-zA-Z]{22})(\?si=.+?)?$",
|
r"^(https?://)?open.spotify.com/track/(?P<TrackID>[0-9a-zA-Z]{22})(\?si=.+?)?$",
|
||||||
args[1])
|
args[1],
|
||||||
|
)
|
||||||
if track_uri_search is not None or track_url_search is not None:
|
if track_uri_search is not None or track_url_search is not None:
|
||||||
track_id_str = (track_uri_search
|
track_id_str = (
|
||||||
if track_uri_search is not None else
|
track_uri_search
|
||||||
track_url_search).group("TrackID")
|
if track_uri_search is not None
|
||||||
|
else track_url_search
|
||||||
|
).group("TrackID")
|
||||||
play(track_id_str)
|
play(track_id_str)
|
||||||
wait()
|
wait()
|
||||||
if args[0] == "q" or args[0] == "quality":
|
if args[0] == "q" or args[0] == "quality":
|
||||||
@@ -56,18 +60,22 @@ def client():
|
|||||||
wait()
|
wait()
|
||||||
if (args[0] == "s" or args[0] == "search") and len(args) <= 2:
|
if (args[0] == "s" or args[0] == "search") and len(args) <= 2:
|
||||||
token = session.tokens().get("user-read-email")
|
token = session.tokens().get("user-read-email")
|
||||||
resp = requests.get("https://api.spotify.com/v1/search", {
|
resp = requests.get(
|
||||||
"limit": "5",
|
"https://api.spotify.com/v1/search",
|
||||||
"offset": "0",
|
{"limit": "5", "offset": "0", "q": cmd[2:], "type": "track"},
|
||||||
"q": cmd[2:],
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
"type": "track"
|
)
|
||||||
},
|
|
||||||
headers={"Authorization": "Bearer %s" % token})
|
|
||||||
i = 1
|
i = 1
|
||||||
tracks = resp.json()["tracks"]["items"]
|
tracks = resp.json()["tracks"]["items"]
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
print("%d, %s | %s" % (i, track["name"], ",".join(
|
print(
|
||||||
[artist["name"] for artist in track["artists"]])))
|
"%d, %s | %s"
|
||||||
|
% (
|
||||||
|
i,
|
||||||
|
track["name"],
|
||||||
|
",".join([artist["name"] for artist in track["artists"]]),
|
||||||
|
)
|
||||||
|
)
|
||||||
i += 1
|
i += 1
|
||||||
position = -1
|
position = -1
|
||||||
while True:
|
while True:
|
||||||
@@ -105,11 +113,14 @@ def login():
|
|||||||
def play(track_id_str: str):
|
def play(track_id_str: str):
|
||||||
track_id = TrackId.from_base62(track_id_str)
|
track_id = TrackId.from_base62(track_id_str)
|
||||||
stream = session.content_feeder().load(
|
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,
|
ffplay = subprocess.Popen(
|
||||||
stdout=subprocess.DEVNULL,
|
["ffplay", "-"],
|
||||||
stderr=subprocess.DEVNULL)
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
while True:
|
while True:
|
||||||
byte = stream.input_stream.stream().read()
|
byte = stream.input_stream.stream().read()
|
||||||
if byte == -1:
|
if byte == -1:
|
||||||
@@ -119,11 +130,13 @@ def play(track_id_str: str):
|
|||||||
|
|
||||||
|
|
||||||
def splash():
|
def splash():
|
||||||
print("=================================\n"
|
print(
|
||||||
"| Librespot-Python Player |\n"
|
"=================================\n"
|
||||||
"| |\n"
|
"| Librespot-Python Player |\n"
|
||||||
"| by kokarare1212 |\n"
|
"| |\n"
|
||||||
"=================================\n\n\n")
|
"| by kokarare1212 |\n"
|
||||||
|
"=================================\n\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
|||||||
_decoded_length: int = 0
|
_decoded_length: int = 0
|
||||||
|
|
||||||
def __init__(self, retry_on_chunk_error: bool):
|
def __init__(self, retry_on_chunk_error: bool):
|
||||||
self.retries: typing.Final[typing.List[int]] = [
|
self.retries: typing.Final[typing.List[int]] = [0 for _ in range(self.chunks())]
|
||||||
0 for _ in range(self.chunks())
|
|
||||||
]
|
|
||||||
self.retry_on_chunk_error = retry_on_chunk_error
|
self.retry_on_chunk_error = retry_on_chunk_error
|
||||||
|
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
@@ -108,10 +106,13 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
|||||||
self.request_chunk_from_stream(chunk)
|
self.request_chunk_from_stream(chunk)
|
||||||
self.requested_chunks()[chunk] = True
|
self.requested_chunks()[chunk] = True
|
||||||
|
|
||||||
for i in range(chunk + 1,
|
for i in range(
|
||||||
min(self.chunks() - 1, chunk + self.preload_ahead) + 1):
|
chunk + 1, min(self.chunks() - 1, chunk + self.preload_ahead) + 1
|
||||||
if self.requested_chunks(
|
):
|
||||||
)[i] and self.retries[i] < self.preload_chunk_retries:
|
if (
|
||||||
|
self.requested_chunks()[i]
|
||||||
|
and self.retries[i] < self.preload_chunk_retries
|
||||||
|
):
|
||||||
self.request_chunk_from_stream(i)
|
self.request_chunk_from_stream(i)
|
||||||
self.requested_chunks()[chunk] = True
|
self.requested_chunks()[chunk] = True
|
||||||
|
|
||||||
@@ -145,10 +146,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
|||||||
|
|
||||||
self.check_availability(chunk, True, True)
|
self.check_availability(chunk, True, True)
|
||||||
|
|
||||||
def read(self,
|
def read(self, b: bytearray = None, offset: int = None, length: int = None) -> int:
|
||||||
b: bytearray = None,
|
|
||||||
offset: int = None,
|
|
||||||
length: int = None) -> int:
|
|
||||||
if b is None and offset is None and length is None:
|
if b is None and offset is None and length is None:
|
||||||
return self.internal_read()
|
return self.internal_read()
|
||||||
if not (b is not None and offset is not None and length is not None):
|
if not (b is not None and offset is not None and length is not None):
|
||||||
@@ -158,8 +156,9 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
|||||||
raise IOError("Stream is closed!")
|
raise IOError("Stream is closed!")
|
||||||
|
|
||||||
if offset < 0 or length < 0 or length > len(b) - offset:
|
if offset < 0 or length < 0 or length > len(b) - offset:
|
||||||
raise IndexError("offset: {}, length: {}, buffer: {}".format(
|
raise IndexError(
|
||||||
offset, length, len(b)))
|
"offset: {}, length: {}, buffer: {}".format(offset, length, len(b))
|
||||||
|
)
|
||||||
elif length == 0:
|
elif length == 0:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -174,8 +173,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
|||||||
self.check_availability(chunk, True, False)
|
self.check_availability(chunk, True, False)
|
||||||
|
|
||||||
copy = min(len(self.buffer()[chunk]) - chunk_off, length - i)
|
copy = min(len(self.buffer()[chunk]) - chunk_off, length - i)
|
||||||
b[offset + 0:copy] = self.buffer()[chunk][chunk_off:chunk_off +
|
b[offset + 0 : copy] = self.buffer()[chunk][chunk_off : chunk_off + copy]
|
||||||
copy]
|
|
||||||
i += copy
|
i += copy
|
||||||
self._pos += copy
|
self._pos += copy
|
||||||
|
|
||||||
@@ -223,4 +221,5 @@ class AbsChunkedInputStream(InputStream, HaltListener):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_stream_error(stream_error: int):
|
def from_stream_error(stream_error: int):
|
||||||
return AbsChunkedInputStream.ChunkException(
|
return AbsChunkedInputStream.ChunkException(
|
||||||
"Failed due to stream error, code: {}".format(stream_error))
|
"Failed due to stream error, code: {}".format(stream_error)
|
||||||
|
)
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ class CdnFeedHelper:
|
|||||||
return random.choice(resp.cdnurl)
|
return random.choice(resp.cdnurl)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_track(session: Session, track: Metadata.Track, file: Metadata.AudioFile,
|
def load_track(
|
||||||
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str],
|
session: Session,
|
||||||
preload: bool, halt_listener: HaltListener)\
|
track: Metadata.Track,
|
||||||
-> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
file: Metadata.AudioFile,
|
||||||
|
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str],
|
||||||
|
preload: bool,
|
||||||
|
halt_listener: HaltListener,
|
||||||
|
) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
||||||
if type(resp_or_url) is str:
|
if type(resp_or_url) is str:
|
||||||
url = resp_or_url
|
url = resp_or_url
|
||||||
else:
|
else:
|
||||||
@@ -31,19 +35,21 @@ class CdnFeedHelper:
|
|||||||
|
|
||||||
streamer = session.cdn().stream_file(file, key, url, halt_listener)
|
streamer = session.cdn().stream_file(file, key, url, halt_listener)
|
||||||
input_stream = streamer.stream()
|
input_stream = streamer.stream()
|
||||||
normalization_data = NormalizationData.NormalizationData.read(
|
normalization_data = NormalizationData.NormalizationData.read(input_stream)
|
||||||
input_stream)
|
if input_stream.skip(0xA7) != 0xA7:
|
||||||
if input_stream.skip(0xa7) != 0xa7:
|
|
||||||
raise IOError("Couldn't skip 0xa7 bytes!")
|
raise IOError("Couldn't skip 0xa7 bytes!")
|
||||||
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
||||||
track, streamer, normalization_data,
|
track,
|
||||||
|
streamer,
|
||||||
|
normalization_data,
|
||||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
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
|
@staticmethod
|
||||||
def load_episode_external(
|
def load_episode_external(
|
||||||
session: Session, episode: Metadata.Episode,
|
session: Session, episode: Metadata.Episode, halt_listener: HaltListener
|
||||||
halt_listener: HaltListener
|
|
||||||
) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
||||||
resp = session.client().head(episode.external_url)
|
resp = session.client().head(episode.external_url)
|
||||||
|
|
||||||
@@ -51,21 +57,27 @@ class CdnFeedHelper:
|
|||||||
CdnFeedHelper._LOGGER.warning("Couldn't resolve redirect!")
|
CdnFeedHelper._LOGGER.warning("Couldn't resolve redirect!")
|
||||||
|
|
||||||
url = resp.url
|
url = resp.url
|
||||||
CdnFeedHelper._LOGGER.debug("Fetched external url for {}: {}".format(
|
CdnFeedHelper._LOGGER.debug(
|
||||||
Utils.Utils.bytes_to_hex(episode.gid), url))
|
"Fetched external url for {}: {}".format(
|
||||||
|
Utils.Utils.bytes_to_hex(episode.gid), url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
streamer = session.cdn().stream_external_episode(
|
streamer = session.cdn().stream_external_episode(episode, url, halt_listener)
|
||||||
episode, url, halt_listener)
|
|
||||||
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
||||||
episode, streamer, None,
|
episode,
|
||||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
streamer,
|
||||||
None, False, -1))
|
None,
|
||||||
|
PlayableContentFeeder.PlayableContentFeeder.Metrics(None, False, -1),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_episode(
|
def load_episode(
|
||||||
session: Session, episode: Metadata.Episode, file: Metadata.AudioFile,
|
session: Session,
|
||||||
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse,
|
episode: Metadata.Episode,
|
||||||
str], halt_listener: HaltListener
|
file: Metadata.AudioFile,
|
||||||
|
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str],
|
||||||
|
halt_listener: HaltListener,
|
||||||
) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
) -> PlayableContentFeeder.PlayableContentFeeder.LoadedStream:
|
||||||
if type(resp_or_url) is str:
|
if type(resp_or_url) is str:
|
||||||
url = resp_or_url
|
url = resp_or_url
|
||||||
@@ -78,9 +90,13 @@ class CdnFeedHelper:
|
|||||||
streamer = session.cdn().stream_file(file, key, url, halt_listener)
|
streamer = session.cdn().stream_file(file, key, url, halt_listener)
|
||||||
input_stream = streamer.stream()
|
input_stream = streamer.stream()
|
||||||
normalization_data = NormalizationData.read(input_stream)
|
normalization_data = NormalizationData.read(input_stream)
|
||||||
if input_stream.skip(0xa7) != 0xa7:
|
if input_stream.skip(0xA7) != 0xA7:
|
||||||
raise IOError("Couldn't skip 0xa7 bytes!")
|
raise IOError("Couldn't skip 0xa7 bytes!")
|
||||||
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
return PlayableContentFeeder.PlayableContentFeeder.LoadedStream(
|
||||||
episode, streamer, normalization_data,
|
episode,
|
||||||
|
streamer,
|
||||||
|
normalization_data,
|
||||||
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
PlayableContentFeeder.PlayableContentFeeder.Metrics(
|
||||||
file.file_id, False, audio_key_time))
|
file.file_id, False, audio_key_time
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ class CdnManager:
|
|||||||
self._session = session
|
self._session = session
|
||||||
|
|
||||||
def get_head(self, file_id: bytes):
|
def get_head(self, file_id: bytes):
|
||||||
resp = self._session.client() \
|
resp = self._session.client().get(
|
||||||
.get(self._session.get_user_attribute("head-files-url", "https://heads-fa.spotify.com/head/{file_id}")
|
self._session.get_user_attribute(
|
||||||
.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:
|
if resp.status_code != 200:
|
||||||
raise IOError("{}".format(resp.status_code))
|
raise IOError("{}".format(resp.status_code))
|
||||||
@@ -43,27 +45,45 @@ class CdnManager:
|
|||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def stream_external_episode(self, episode: Metadata.Episode,
|
def stream_external_episode(
|
||||||
external_url: str,
|
self, episode: Metadata.Episode, external_url: str, halt_listener: HaltListener
|
||||||
halt_listener: HaltListener):
|
):
|
||||||
return CdnManager.Streamer(self._session, StreamId(episode),
|
return CdnManager.Streamer(
|
||||||
SuperAudioFormat.MP3,
|
self._session,
|
||||||
CdnManager.CdnUrl(self, None, external_url),
|
StreamId(episode),
|
||||||
self._session.cache(), NoopAudioDecrypt(),
|
SuperAudioFormat.MP3,
|
||||||
halt_listener)
|
CdnManager.CdnUrl(self, None, external_url),
|
||||||
|
self._session.cache(),
|
||||||
|
NoopAudioDecrypt(),
|
||||||
|
halt_listener,
|
||||||
|
)
|
||||||
|
|
||||||
def stream_file(self, file: Metadata.AudioFile, key: bytes, url: str,
|
def stream_file(
|
||||||
halt_listener: HaltListener):
|
self,
|
||||||
return CdnManager.Streamer(self._session, StreamId.StreamId(file),
|
file: Metadata.AudioFile,
|
||||||
SuperAudioFormat.get(file.format),
|
key: bytes,
|
||||||
CdnManager.CdnUrl(self, file.file_id, url),
|
url: str,
|
||||||
self._session.cache(), AesAudioDecrypt(key),
|
halt_listener: HaltListener,
|
||||||
halt_listener)
|
):
|
||||||
|
return CdnManager.Streamer(
|
||||||
|
self._session,
|
||||||
|
StreamId.StreamId(file),
|
||||||
|
SuperAudioFormat.get(file.format),
|
||||||
|
CdnManager.CdnUrl(self, file.file_id, url),
|
||||||
|
self._session.cache(),
|
||||||
|
AesAudioDecrypt(key),
|
||||||
|
halt_listener,
|
||||||
|
)
|
||||||
|
|
||||||
def get_audio_url(self, file_id: bytes):
|
def get_audio_url(self, file_id: bytes):
|
||||||
resp = self._session.api().send(
|
resp = self._session.api().send(
|
||||||
"GET", "/storage-resolve/files/audio/interactive/{}".format(
|
"GET",
|
||||||
Utils.bytes_to_hex(file_id)), None, None)
|
"/storage-resolve/files/audio/interactive/{}".format(
|
||||||
|
Utils.bytes_to_hex(file_id)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise IOError(resp.status_code)
|
raise IOError(resp.status_code)
|
||||||
@@ -76,11 +96,13 @@ class CdnManager:
|
|||||||
proto.ParseFromString(body)
|
proto.ParseFromString(body)
|
||||||
if proto.result == StorageResolve.StorageResolveResponse.Result.CDN:
|
if proto.result == StorageResolve.StorageResolveResponse.Result.CDN:
|
||||||
url = random.choice(proto.cdnurl)
|
url = random.choice(proto.cdnurl)
|
||||||
self._LOGGER.debug("Fetched CDN url for {}: {}".format(
|
self._LOGGER.debug(
|
||||||
Utils.bytes_to_hex(file_id), url))
|
"Fetched CDN url for {}: {}".format(Utils.bytes_to_hex(file_id), url)
|
||||||
|
)
|
||||||
return url
|
return url
|
||||||
raise CdnManager.CdnException(
|
raise CdnManager.CdnException(
|
||||||
"Could not retrieve CDN url! result: {}".format(proto.result))
|
"Could not retrieve CDN url! result: {}".format(proto.result)
|
||||||
|
)
|
||||||
|
|
||||||
class CdnException(Exception):
|
class CdnException(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -140,7 +162,8 @@ class CdnManager:
|
|||||||
if expire_at is None:
|
if expire_at is None:
|
||||||
self._expiration = -1
|
self._expiration = -1
|
||||||
self._cdnManager._LOGGER.warning(
|
self._cdnManager._LOGGER.warning(
|
||||||
"Invalid __token__ in CDN url: {}".format(url))
|
"Invalid __token__ in CDN url: {}".format(url)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._expiration = expire_at * 1000
|
self._expiration = expire_at * 1000
|
||||||
@@ -150,8 +173,10 @@ class CdnManager:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
self._expiration = -1
|
self._expiration = -1
|
||||||
self._cdnManager._LOGGER.warning(
|
self._cdnManager._LOGGER.warning(
|
||||||
"Couldn't extract expiration, invalid parameter in CDN url: "
|
"Couldn't extract expiration, invalid parameter in CDN url: ".format(
|
||||||
.format(url))
|
url
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._expiration = int(token_url.query[:i]) * 1000
|
self._expiration = int(token_url.query[:i]) * 1000
|
||||||
@@ -159,8 +184,10 @@ class CdnManager:
|
|||||||
else:
|
else:
|
||||||
self._expiration = -1
|
self._expiration = -1
|
||||||
|
|
||||||
class Streamer(GeneralAudioStream.GeneralAudioStream,
|
class Streamer(
|
||||||
GeneralWritableStream.GeneralWritableStream):
|
GeneralAudioStream.GeneralAudioStream,
|
||||||
|
GeneralWritableStream.GeneralWritableStream,
|
||||||
|
):
|
||||||
_session: Session = None
|
_session: Session = None
|
||||||
_streamId: StreamId = None
|
_streamId: StreamId = None
|
||||||
_executorService = concurrent.futures.ThreadPoolExecutor()
|
_executorService = concurrent.futures.ThreadPoolExecutor()
|
||||||
@@ -175,10 +202,16 @@ class CdnManager:
|
|||||||
_internalStream: CdnManager.Streamer.InternalStream = None
|
_internalStream: CdnManager.Streamer.InternalStream = None
|
||||||
_haltListener: HaltListener = None
|
_haltListener: HaltListener = None
|
||||||
|
|
||||||
def __init__(self, session: Session, stream_id: StreamId,
|
def __init__(
|
||||||
audio_format: SuperAudioFormat, cdn_url,
|
self,
|
||||||
cache: CacheManager, audio_decrypt: AudioDecrypt,
|
session: Session,
|
||||||
halt_listener: HaltListener):
|
stream_id: StreamId,
|
||||||
|
audio_format: SuperAudioFormat,
|
||||||
|
cdn_url,
|
||||||
|
cache: CacheManager,
|
||||||
|
audio_decrypt: AudioDecrypt,
|
||||||
|
halt_listener: HaltListener,
|
||||||
|
):
|
||||||
self._session = session
|
self._session = session
|
||||||
self._streamId = stream_id
|
self._streamId = stream_id
|
||||||
self._audioFormat = audio_format
|
self._audioFormat = audio_format
|
||||||
@@ -186,39 +219,38 @@ class CdnManager:
|
|||||||
self._cdnUrl = cdn_url
|
self._cdnUrl = cdn_url
|
||||||
self._haltListener = halt_listener
|
self._haltListener = halt_listener
|
||||||
|
|
||||||
resp = self.request(range_start=0,
|
resp = self.request(range_start=0, range_end=ChannelManager.CHUNK_SIZE - 1)
|
||||||
range_end=ChannelManager.CHUNK_SIZE - 1)
|
|
||||||
content_range = resp._headers.get("Content-Range")
|
content_range = resp._headers.get("Content-Range")
|
||||||
if content_range is None:
|
if content_range is None:
|
||||||
raise IOError("Missing Content-Range header!")
|
raise IOError("Missing Content-Range header!")
|
||||||
|
|
||||||
split = Utils.split(content_range, "/")
|
split = Utils.split(content_range, "/")
|
||||||
self._size = int(split[1])
|
self._size = int(split[1])
|
||||||
self._chunks = int(
|
self._chunks = int(math.ceil(self._size / ChannelManager.CHUNK_SIZE))
|
||||||
math.ceil(self._size / ChannelManager.CHUNK_SIZE))
|
|
||||||
|
|
||||||
first_chunk = resp._buffer
|
first_chunk = resp._buffer
|
||||||
|
|
||||||
self._available = [False for _ in range(self._chunks)]
|
self._available = [False for _ in range(self._chunks)]
|
||||||
self._requested = [False for _ in range(self._chunks)]
|
self._requested = [False for _ in range(self._chunks)]
|
||||||
self._buffer = [bytearray() for _ in range(self._chunks)]
|
self._buffer = [bytearray() for _ in range(self._chunks)]
|
||||||
self._internalStream = CdnManager.Streamer.InternalStream(
|
self._internalStream = CdnManager.Streamer.InternalStream(self, False)
|
||||||
self, False)
|
|
||||||
|
|
||||||
self._requested[0] = True
|
self._requested[0] = True
|
||||||
self.write_chunk(first_chunk, 0, False)
|
self.write_chunk(first_chunk, 0, False)
|
||||||
|
|
||||||
def write_chunk(self, chunk: bytes, chunk_index: int,
|
def write_chunk(self, chunk: bytes, chunk_index: int, cached: bool) -> None:
|
||||||
cached: bool) -> None:
|
|
||||||
if self._internalStream.is_closed():
|
if self._internalStream.is_closed():
|
||||||
return
|
return
|
||||||
|
|
||||||
self._session._LOGGER.debug(
|
self._session._LOGGER.debug(
|
||||||
"Chunk {}/{} completed, cached: {}, stream: {}".format(
|
"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(
|
self._buffer[chunk_index] = self._audioDecrypt.decrypt_chunk(
|
||||||
chunk_index, chunk)
|
chunk_index, chunk
|
||||||
|
)
|
||||||
self._internalStream.notify_chunk_available(chunk_index)
|
self._internalStream.notify_chunk_available(chunk_index)
|
||||||
|
|
||||||
def stream(self) -> AbsChunkedInputStream:
|
def stream(self) -> AbsChunkedInputStream:
|
||||||
@@ -229,8 +261,7 @@ class CdnManager:
|
|||||||
|
|
||||||
def describe(self) -> str:
|
def describe(self) -> str:
|
||||||
if self._streamId.is_episode():
|
if self._streamId.is_episode():
|
||||||
return "episode_gid: {}".format(
|
return "episode_gid: {}".format(self._streamId.get_episode_gid())
|
||||||
self._streamId.get_episode_gid())
|
|
||||||
return "file_id: {}".format(self._streamId.get_file_id())
|
return "file_id: {}".format(self._streamId.get_file_id())
|
||||||
|
|
||||||
def decrypt_time_ms(self) -> int:
|
def decrypt_time_ms(self) -> int:
|
||||||
@@ -240,10 +271,9 @@ class CdnManager:
|
|||||||
resp = self.request(index)
|
resp = self.request(index)
|
||||||
self.write_chunk(resp._buffer, index, False)
|
self.write_chunk(resp._buffer, index, False)
|
||||||
|
|
||||||
def request(self,
|
def request(
|
||||||
chunk: int = None,
|
self, chunk: int = None, range_start: int = None, range_end: int = None
|
||||||
range_start: int = None,
|
) -> CdnManager.InternalResponse:
|
||||||
range_end: int = None) -> CdnManager.InternalResponse:
|
|
||||||
if chunk is None and range_start is None and range_end is None:
|
if chunk is None and range_start is None and range_end is None:
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
@@ -251,12 +281,10 @@ class CdnManager:
|
|||||||
range_start = ChannelManager.CHUNK_SIZE * chunk
|
range_start = ChannelManager.CHUNK_SIZE * chunk
|
||||||
range_end = (chunk + 1) * ChannelManager.CHUNK_SIZE - 1
|
range_end = (chunk + 1) * ChannelManager.CHUNK_SIZE - 1
|
||||||
|
|
||||||
resp = self._session.client().get(self._cdnUrl._url,
|
resp = self._session.client().get(
|
||||||
headers={
|
self._cdnUrl._url,
|
||||||
"Range":
|
headers={"Range": "bytes={}-{}".format(range_start, range_end)},
|
||||||
"bytes={}-{}".format(
|
)
|
||||||
range_start, range_end)
|
|
||||||
})
|
|
||||||
|
|
||||||
if resp.status_code != 206:
|
if resp.status_code != 206:
|
||||||
raise IOError(resp.status_code)
|
raise IOError(resp.status_code)
|
||||||
@@ -291,16 +319,21 @@ class CdnManager:
|
|||||||
|
|
||||||
def request_chunk_from_stream(self, index: int) -> None:
|
def request_chunk_from_stream(self, index: int) -> None:
|
||||||
self.streamer._executorService.submit(
|
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:
|
def stream_read_halted(self, chunk: int, _time: int) -> None:
|
||||||
if self.streamer._haltListener is not None:
|
if self.streamer._haltListener is not None:
|
||||||
self.streamer._executorService.submit(
|
self.streamer._executorService.submit(
|
||||||
lambda: self.streamer._haltListener.stream_read_halted(
|
lambda: self.streamer._haltListener.stream_read_halted(
|
||||||
chunk, _time))
|
chunk, _time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def stream_read_resumed(self, chunk: int, _time: int) -> None:
|
def stream_read_resumed(self, chunk: int, _time: int) -> None:
|
||||||
if self.streamer._haltListener is not None:
|
if self.streamer._haltListener is not None:
|
||||||
self.streamer._executorService.submit(
|
self.streamer._executorService.submit(
|
||||||
lambda: self.streamer._haltListener.
|
lambda: self.streamer._haltListener.stream_read_resumed(
|
||||||
stream_read_resumed(chunk, _time))
|
chunk, _time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -31,30 +31,266 @@ import typing
|
|||||||
|
|
||||||
class Session(Closeable, SubListener, DealerClient.MessageListener):
|
class Session(Closeable, SubListener, DealerClient.MessageListener):
|
||||||
_LOGGER: logging = logging.getLogger(__name__)
|
_LOGGER: logging = logging.getLogger(__name__)
|
||||||
_serverKey: bytes = bytes([
|
_serverKey: bytes = bytes(
|
||||||
0xac, 0xe0, 0x46, 0x0b, 0xff, 0xc2, 0x30, 0xaf, 0xf4, 0x6b, 0xfe, 0xc3,
|
[
|
||||||
0xbf, 0xbf, 0x86, 0x3d, 0xa1, 0x91, 0xc6, 0xcc, 0x33, 0x6c, 0x93, 0xa1,
|
0xAC,
|
||||||
0x4f, 0xb3, 0xb0, 0x16, 0x12, 0xac, 0xac, 0x6a, 0xf1, 0x80, 0xe7, 0xf6,
|
0xE0,
|
||||||
0x14, 0xd9, 0x42, 0x9d, 0xbe, 0x2e, 0x34, 0x66, 0x43, 0xe3, 0x62, 0xd2,
|
0x46,
|
||||||
0x32, 0x7a, 0x1a, 0x0d, 0x92, 0x3b, 0xae, 0xdd, 0x14, 0x02, 0xb1, 0x81,
|
0x0B,
|
||||||
0x55, 0x05, 0x61, 0x04, 0xd5, 0x2c, 0x96, 0xa4, 0x4c, 0x1e, 0xcc, 0x02,
|
0xFF,
|
||||||
0x4a, 0xd4, 0xb2, 0x0c, 0x00, 0x1f, 0x17, 0xed, 0xc2, 0x2f, 0xc4, 0x35,
|
0xC2,
|
||||||
0x21, 0xc8, 0xf0, 0xcb, 0xae, 0xd2, 0xad, 0xd7, 0x2b, 0x0f, 0x9d, 0xb3,
|
0x30,
|
||||||
0xc5, 0x32, 0x1a, 0x2a, 0xfe, 0x59, 0xf3, 0x5a, 0x0d, 0xac, 0x68, 0xf1,
|
0xAF,
|
||||||
0xfa, 0x62, 0x1e, 0xfb, 0x2c, 0x8d, 0x0c, 0xb7, 0x39, 0x2d, 0x92, 0x47,
|
0xF4,
|
||||||
0xe3, 0xd7, 0x35, 0x1a, 0x6d, 0xbd, 0x24, 0xc2, 0xae, 0x25, 0x5b, 0x88,
|
0x6B,
|
||||||
0xff, 0xab, 0x73, 0x29, 0x8a, 0x0b, 0xcc, 0xcd, 0x0c, 0x58, 0x67, 0x31,
|
0xFE,
|
||||||
0x89, 0xe8, 0xbd, 0x34, 0x80, 0x78, 0x4a, 0x5f, 0xc9, 0x6b, 0x89, 0x9d,
|
0xC3,
|
||||||
0x95, 0x6b, 0xfc, 0x86, 0xd7, 0x4f, 0x33, 0xa6, 0x78, 0x17, 0x96, 0xc9,
|
0xBF,
|
||||||
0xc3, 0x2d, 0x0d, 0x32, 0xa5, 0xab, 0xcd, 0x05, 0x27, 0xe2, 0xf7, 0x10,
|
0xBF,
|
||||||
0xa3, 0x96, 0x13, 0xc4, 0x2f, 0x99, 0xc0, 0x27, 0xbf, 0xed, 0x04, 0x9c,
|
0x86,
|
||||||
0x3c, 0x27, 0x58, 0x04, 0xb6, 0xb2, 0x19, 0xf9, 0xc1, 0x2f, 0x02, 0xe9,
|
0x3D,
|
||||||
0x48, 0x63, 0xec, 0xa1, 0xb6, 0x42, 0xa0, 0x9d, 0x48, 0x25, 0xf8, 0xb3,
|
0xA1,
|
||||||
0x9d, 0xd0, 0xe8, 0x6a, 0xf9, 0x48, 0x4d, 0xa1, 0xc2, 0xba, 0x86, 0x30,
|
0x91,
|
||||||
0x42, 0xea, 0x9d, 0xb3, 0x08, 0x6c, 0x19, 0x0e, 0x48, 0xb3, 0x9d, 0x66,
|
0xC6,
|
||||||
0xeb, 0x00, 0x06, 0xa2, 0x5a, 0xee, 0xa1, 0x1b, 0x13, 0x87, 0x3c, 0xd7,
|
0xCC,
|
||||||
0x19, 0xe6, 0x55, 0xbd
|
0x33,
|
||||||
])
|
0x6C,
|
||||||
|
0x93,
|
||||||
|
0xA1,
|
||||||
|
0x4F,
|
||||||
|
0xB3,
|
||||||
|
0xB0,
|
||||||
|
0x16,
|
||||||
|
0x12,
|
||||||
|
0xAC,
|
||||||
|
0xAC,
|
||||||
|
0x6A,
|
||||||
|
0xF1,
|
||||||
|
0x80,
|
||||||
|
0xE7,
|
||||||
|
0xF6,
|
||||||
|
0x14,
|
||||||
|
0xD9,
|
||||||
|
0x42,
|
||||||
|
0x9D,
|
||||||
|
0xBE,
|
||||||
|
0x2E,
|
||||||
|
0x34,
|
||||||
|
0x66,
|
||||||
|
0x43,
|
||||||
|
0xE3,
|
||||||
|
0x62,
|
||||||
|
0xD2,
|
||||||
|
0x32,
|
||||||
|
0x7A,
|
||||||
|
0x1A,
|
||||||
|
0x0D,
|
||||||
|
0x92,
|
||||||
|
0x3B,
|
||||||
|
0xAE,
|
||||||
|
0xDD,
|
||||||
|
0x14,
|
||||||
|
0x02,
|
||||||
|
0xB1,
|
||||||
|
0x81,
|
||||||
|
0x55,
|
||||||
|
0x05,
|
||||||
|
0x61,
|
||||||
|
0x04,
|
||||||
|
0xD5,
|
||||||
|
0x2C,
|
||||||
|
0x96,
|
||||||
|
0xA4,
|
||||||
|
0x4C,
|
||||||
|
0x1E,
|
||||||
|
0xCC,
|
||||||
|
0x02,
|
||||||
|
0x4A,
|
||||||
|
0xD4,
|
||||||
|
0xB2,
|
||||||
|
0x0C,
|
||||||
|
0x00,
|
||||||
|
0x1F,
|
||||||
|
0x17,
|
||||||
|
0xED,
|
||||||
|
0xC2,
|
||||||
|
0x2F,
|
||||||
|
0xC4,
|
||||||
|
0x35,
|
||||||
|
0x21,
|
||||||
|
0xC8,
|
||||||
|
0xF0,
|
||||||
|
0xCB,
|
||||||
|
0xAE,
|
||||||
|
0xD2,
|
||||||
|
0xAD,
|
||||||
|
0xD7,
|
||||||
|
0x2B,
|
||||||
|
0x0F,
|
||||||
|
0x9D,
|
||||||
|
0xB3,
|
||||||
|
0xC5,
|
||||||
|
0x32,
|
||||||
|
0x1A,
|
||||||
|
0x2A,
|
||||||
|
0xFE,
|
||||||
|
0x59,
|
||||||
|
0xF3,
|
||||||
|
0x5A,
|
||||||
|
0x0D,
|
||||||
|
0xAC,
|
||||||
|
0x68,
|
||||||
|
0xF1,
|
||||||
|
0xFA,
|
||||||
|
0x62,
|
||||||
|
0x1E,
|
||||||
|
0xFB,
|
||||||
|
0x2C,
|
||||||
|
0x8D,
|
||||||
|
0x0C,
|
||||||
|
0xB7,
|
||||||
|
0x39,
|
||||||
|
0x2D,
|
||||||
|
0x92,
|
||||||
|
0x47,
|
||||||
|
0xE3,
|
||||||
|
0xD7,
|
||||||
|
0x35,
|
||||||
|
0x1A,
|
||||||
|
0x6D,
|
||||||
|
0xBD,
|
||||||
|
0x24,
|
||||||
|
0xC2,
|
||||||
|
0xAE,
|
||||||
|
0x25,
|
||||||
|
0x5B,
|
||||||
|
0x88,
|
||||||
|
0xFF,
|
||||||
|
0xAB,
|
||||||
|
0x73,
|
||||||
|
0x29,
|
||||||
|
0x8A,
|
||||||
|
0x0B,
|
||||||
|
0xCC,
|
||||||
|
0xCD,
|
||||||
|
0x0C,
|
||||||
|
0x58,
|
||||||
|
0x67,
|
||||||
|
0x31,
|
||||||
|
0x89,
|
||||||
|
0xE8,
|
||||||
|
0xBD,
|
||||||
|
0x34,
|
||||||
|
0x80,
|
||||||
|
0x78,
|
||||||
|
0x4A,
|
||||||
|
0x5F,
|
||||||
|
0xC9,
|
||||||
|
0x6B,
|
||||||
|
0x89,
|
||||||
|
0x9D,
|
||||||
|
0x95,
|
||||||
|
0x6B,
|
||||||
|
0xFC,
|
||||||
|
0x86,
|
||||||
|
0xD7,
|
||||||
|
0x4F,
|
||||||
|
0x33,
|
||||||
|
0xA6,
|
||||||
|
0x78,
|
||||||
|
0x17,
|
||||||
|
0x96,
|
||||||
|
0xC9,
|
||||||
|
0xC3,
|
||||||
|
0x2D,
|
||||||
|
0x0D,
|
||||||
|
0x32,
|
||||||
|
0xA5,
|
||||||
|
0xAB,
|
||||||
|
0xCD,
|
||||||
|
0x05,
|
||||||
|
0x27,
|
||||||
|
0xE2,
|
||||||
|
0xF7,
|
||||||
|
0x10,
|
||||||
|
0xA3,
|
||||||
|
0x96,
|
||||||
|
0x13,
|
||||||
|
0xC4,
|
||||||
|
0x2F,
|
||||||
|
0x99,
|
||||||
|
0xC0,
|
||||||
|
0x27,
|
||||||
|
0xBF,
|
||||||
|
0xED,
|
||||||
|
0x04,
|
||||||
|
0x9C,
|
||||||
|
0x3C,
|
||||||
|
0x27,
|
||||||
|
0x58,
|
||||||
|
0x04,
|
||||||
|
0xB6,
|
||||||
|
0xB2,
|
||||||
|
0x19,
|
||||||
|
0xF9,
|
||||||
|
0xC1,
|
||||||
|
0x2F,
|
||||||
|
0x02,
|
||||||
|
0xE9,
|
||||||
|
0x48,
|
||||||
|
0x63,
|
||||||
|
0xEC,
|
||||||
|
0xA1,
|
||||||
|
0xB6,
|
||||||
|
0x42,
|
||||||
|
0xA0,
|
||||||
|
0x9D,
|
||||||
|
0x48,
|
||||||
|
0x25,
|
||||||
|
0xF8,
|
||||||
|
0xB3,
|
||||||
|
0x9D,
|
||||||
|
0xD0,
|
||||||
|
0xE8,
|
||||||
|
0x6A,
|
||||||
|
0xF9,
|
||||||
|
0x48,
|
||||||
|
0x4D,
|
||||||
|
0xA1,
|
||||||
|
0xC2,
|
||||||
|
0xBA,
|
||||||
|
0x86,
|
||||||
|
0x30,
|
||||||
|
0x42,
|
||||||
|
0xEA,
|
||||||
|
0x9D,
|
||||||
|
0xB3,
|
||||||
|
0x08,
|
||||||
|
0x6C,
|
||||||
|
0x19,
|
||||||
|
0x0E,
|
||||||
|
0x48,
|
||||||
|
0xB3,
|
||||||
|
0x9D,
|
||||||
|
0x66,
|
||||||
|
0xEB,
|
||||||
|
0x00,
|
||||||
|
0x06,
|
||||||
|
0xA2,
|
||||||
|
0x5A,
|
||||||
|
0xEE,
|
||||||
|
0xA1,
|
||||||
|
0x1B,
|
||||||
|
0x13,
|
||||||
|
0x87,
|
||||||
|
0x3C,
|
||||||
|
0xD7,
|
||||||
|
0x19,
|
||||||
|
0xE6,
|
||||||
|
0x55,
|
||||||
|
0xBD,
|
||||||
|
]
|
||||||
|
)
|
||||||
_keys: DiffieHellman = None
|
_keys: DiffieHellman = None
|
||||||
_inner: Session.Inner = None
|
_inner: Session.Inner = None
|
||||||
_scheduler: sched.scheduler = sched.scheduler(time.time)
|
_scheduler: sched.scheduler = sched.scheduler(time.time)
|
||||||
@@ -92,8 +328,9 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
self._conn = Session.ConnectionHolder.create(addr, inner.conf)
|
self._conn = Session.ConnectionHolder.create(addr, inner.conf)
|
||||||
self._client = Session._create_client(self._inner.conf)
|
self._client = Session._create_client(self._inner.conf)
|
||||||
|
|
||||||
self._LOGGER.info("Created new session! device_id: {}, ap: {}".format(
|
self._LOGGER.info(
|
||||||
inner.device_id, addr))
|
"Created new session! device_id: {}, ap: {}".format(inner.device_id, addr)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_client(conf: Session.Configuration) -> requests.Session:
|
def _create_client(conf: Session.Configuration) -> requests.Session:
|
||||||
@@ -101,14 +338,16 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
if conf.proxyAuth and conf.proxyType is not Proxy.Type.DIRECT:
|
if conf.proxyAuth and conf.proxyType is not Proxy.Type.DIRECT:
|
||||||
if conf.proxyAuth:
|
if conf.proxyAuth:
|
||||||
proxy_setting = [
|
proxy_setting = [
|
||||||
conf.proxyUsername, conf.proxyPassword, conf.proxyAddress,
|
conf.proxyUsername,
|
||||||
conf.proxyPort
|
conf.proxyPassword,
|
||||||
|
conf.proxyAddress,
|
||||||
|
conf.proxyPort,
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
proxy_setting = [conf.proxyAddress, conf.proxyPort]
|
proxy_setting = [conf.proxyAddress, conf.proxyPort]
|
||||||
client.proxies = {
|
client.proxies = {
|
||||||
"http": "{}:{}@{}:{}".format(*proxy_setting),
|
"http": "{}:{}@{}:{}".format(*proxy_setting),
|
||||||
"https": "{}:{}@{}:{}".format(*proxy_setting)
|
"https": "{}:{}@{}:{}".format(*proxy_setting),
|
||||||
}
|
}
|
||||||
|
|
||||||
return client
|
return client
|
||||||
@@ -119,7 +358,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
if (lo & 0x80) == 0:
|
if (lo & 0x80) == 0:
|
||||||
return lo
|
return lo
|
||||||
hi = buffer[1]
|
hi = buffer[1]
|
||||||
return lo & 0x7f | hi << 7
|
return lo & 0x7F | hi << 7
|
||||||
|
|
||||||
def client(self) -> requests.Session:
|
def client(self) -> requests.Session:
|
||||||
return self._client
|
return self._client
|
||||||
@@ -133,14 +372,15 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
|
|
||||||
client_hello = Keyexchange.ClientHello(
|
client_hello = Keyexchange.ClientHello(
|
||||||
build_info=Version.standard_build_info(),
|
build_info=Version.standard_build_info(),
|
||||||
cryptosuites_supported=[
|
cryptosuites_supported=[Keyexchange.Cryptosuite.CRYPTO_SUITE_SHANNON],
|
||||||
Keyexchange.Cryptosuite.CRYPTO_SUITE_SHANNON
|
|
||||||
],
|
|
||||||
login_crypto_hello=Keyexchange.LoginCryptoHelloUnion(
|
login_crypto_hello=Keyexchange.LoginCryptoHelloUnion(
|
||||||
diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanHello(
|
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,
|
client_nonce=nonce,
|
||||||
padding=bytes([0x1e]))
|
padding=bytes([0x1E]),
|
||||||
|
)
|
||||||
|
|
||||||
client_hello_bytes = client_hello.SerializeToString()
|
client_hello_bytes = client_hello.SerializeToString()
|
||||||
length = 2 + 4 + len(client_hello_bytes)
|
length = 2 + 4 + len(client_hello_bytes)
|
||||||
@@ -166,20 +406,23 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
ap_response_message.ParseFromString(buffer)
|
ap_response_message.ParseFromString(buffer)
|
||||||
shared_key = Utils.to_byte_array(
|
shared_key = Utils.to_byte_array(
|
||||||
self._keys.compute_shared_key(
|
self._keys.compute_shared_key(
|
||||||
ap_response_message.challenge.login_crypto_challenge.
|
ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs
|
||||||
diffie_hellman.gs))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Check gs_signature
|
# Check gs_signature
|
||||||
|
|
||||||
rsa = RSA.construct((int.from_bytes(self._serverKey, "big"), 65537))
|
rsa = RSA.construct((int.from_bytes(self._serverKey, "big"), 65537))
|
||||||
pkcs1_v1_5 = PKCS1_v1_5.new(rsa)
|
pkcs1_v1_5 = PKCS1_v1_5.new(rsa)
|
||||||
sha1 = SHA1.new()
|
sha1 = SHA1.new()
|
||||||
sha1.update(ap_response_message.challenge.login_crypto_challenge.
|
sha1.update(
|
||||||
diffie_hellman.gs)
|
ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs
|
||||||
|
)
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
if not pkcs1_v1_5.verify(
|
if not pkcs1_v1_5.verify(
|
||||||
sha1, ap_response_message.challenge.login_crypto_challenge.
|
sha1,
|
||||||
diffie_hellman.gs_signature):
|
ap_response_message.challenge.login_crypto_challenge.diffie_hellman.gs_signature,
|
||||||
|
):
|
||||||
raise RuntimeError("Failed signature check!")
|
raise RuntimeError("Failed signature check!")
|
||||||
|
|
||||||
# Solve challenge
|
# Solve challenge
|
||||||
@@ -201,12 +444,14 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
client_response_plaintext = Keyexchange.ClientResponsePlaintext(
|
client_response_plaintext = Keyexchange.ClientResponsePlaintext(
|
||||||
login_crypto_response=Keyexchange.LoginCryptoResponseUnion(
|
login_crypto_response=Keyexchange.LoginCryptoResponseUnion(
|
||||||
diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse(
|
diffie_hellman=Keyexchange.LoginCryptoDiffieHellmanResponse(
|
||||||
hmac=challenge)),
|
hmac=challenge
|
||||||
|
)
|
||||||
|
),
|
||||||
pow_response=Keyexchange.PoWResponseUnion(),
|
pow_response=Keyexchange.PoWResponseUnion(),
|
||||||
crypto_response=Keyexchange.CryptoResponseUnion())
|
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)
|
length = 4 + len(client_response_plaintext_bytes)
|
||||||
self._conn.write_int(length)
|
self._conn.write_int(length)
|
||||||
self._conn.write(client_response_plaintext_bytes)
|
self._conn.write(client_response_plaintext_bytes)
|
||||||
@@ -216,8 +461,12 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
self._conn.set_timeout(1)
|
self._conn.set_timeout(1)
|
||||||
scrap = self._conn.read(4)
|
scrap = self._conn.read(4)
|
||||||
if 4 == len(scrap):
|
if 4 == len(scrap):
|
||||||
length = (scrap[0] << 24) | (scrap[1] << 16) | (
|
length = (
|
||||||
scrap[2] << 8) | (scrap[3] & 0xff)
|
(scrap[0] << 24)
|
||||||
|
| (scrap[1] << 16)
|
||||||
|
| (scrap[2] << 8)
|
||||||
|
| (scrap[3] & 0xFF)
|
||||||
|
)
|
||||||
payload = self._conn.read(length - 4)
|
payload = self._conn.read(length - 4)
|
||||||
failed = Keyexchange.APResponseMessage()
|
failed = Keyexchange.APResponseMessage()
|
||||||
failed.ParseFromString(payload)
|
failed.ParseFromString(payload)
|
||||||
@@ -234,8 +483,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
|
|
||||||
self._LOGGER.info("Connection successfully!")
|
self._LOGGER.info("Connection successfully!")
|
||||||
|
|
||||||
def _authenticate(self,
|
def _authenticate(self, credentials: Authentication.LoginCredentials) -> None:
|
||||||
credentials: Authentication.LoginCredentials) -> None:
|
|
||||||
self._authenticate_partial(credentials, False)
|
self._authenticate_partial(credentials, False)
|
||||||
|
|
||||||
with self._authLock:
|
with self._authLock:
|
||||||
@@ -245,8 +493,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
self._channelManager = ChannelManager(self)
|
self._channelManager = ChannelManager(self)
|
||||||
self._api = ApiClient.ApiClient(self)
|
self._api = ApiClient.ApiClient(self)
|
||||||
self._cdnManager = CdnManager(self)
|
self._cdnManager = CdnManager(self)
|
||||||
self._contentFeeder = PlayableContentFeeder.PlayableContentFeeder(
|
self._contentFeeder = PlayableContentFeeder.PlayableContentFeeder(self)
|
||||||
self)
|
|
||||||
self._cacheManager = CacheManager(self)
|
self._cacheManager = CacheManager(self)
|
||||||
self._dealer = DealerClient(self)
|
self._dealer = DealerClient(self)
|
||||||
self._search = SearchManager.SearchManager(self)
|
self._search = SearchManager.SearchManager(self)
|
||||||
@@ -259,15 +506,15 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
# TimeProvider.init(self)
|
# TimeProvider.init(self)
|
||||||
self._dealer.connect()
|
self._dealer.connect()
|
||||||
|
|
||||||
self._LOGGER.info("Authenticated as {}!".format(
|
self._LOGGER.info(
|
||||||
self._apWelcome.canonical_username))
|
"Authenticated as {}!".format(self._apWelcome.canonical_username)
|
||||||
|
)
|
||||||
self.mercury().interested_in("spotify:user:attributes:update", self)
|
self.mercury().interested_in("spotify:user:attributes:update", self)
|
||||||
self.dealer().add_message_listener(
|
self.dealer().add_message_listener(self, "hm://connect-state/v1/connect/logout")
|
||||||
self, "hm://connect-state/v1/connect/logout")
|
|
||||||
|
|
||||||
def _authenticate_partial(self,
|
def _authenticate_partial(
|
||||||
credentials: Authentication.LoginCredentials,
|
self, credentials: Authentication.LoginCredentials, remove_lock: bool
|
||||||
remove_lock: bool) -> None:
|
) -> None:
|
||||||
if self._cipherPair is None:
|
if self._cipherPair is None:
|
||||||
raise RuntimeError("Connection not established!")
|
raise RuntimeError("Connection not established!")
|
||||||
|
|
||||||
@@ -277,11 +524,14 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
os=Authentication.Os.OS_UNKNOWN,
|
os=Authentication.Os.OS_UNKNOWN,
|
||||||
cpu_family=Authentication.CpuFamily.CPU_UNKNOWN,
|
cpu_family=Authentication.CpuFamily.CPU_UNKNOWN,
|
||||||
system_information_string=Version.system_info_string(),
|
system_information_string=Version.system_info_string(),
|
||||||
device_id=self._inner.device_id),
|
device_id=self._inner.device_id,
|
||||||
version_string=Version.version_string())
|
),
|
||||||
|
version_string=Version.version_string(),
|
||||||
|
)
|
||||||
|
|
||||||
self._send_unchecked(Packet.Type.login,
|
self._send_unchecked(
|
||||||
client_response_encrypted.SerializeToString())
|
Packet.Type.login, client_response_encrypted.SerializeToString()
|
||||||
|
)
|
||||||
|
|
||||||
packet = self._cipherPair.receive_encoded(self._conn)
|
packet = self._cipherPair.receive_encoded(self._conn)
|
||||||
if packet.is_cmd(Packet.Type.ap_welcome):
|
if packet.is_cmd(Packet.Type.ap_welcome):
|
||||||
@@ -297,8 +547,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
preferred_locale += bytes([0x00, 0x00, 0x10, 0x00, 0x02])
|
preferred_locale += bytes([0x00, 0x00, 0x10, 0x00, 0x02])
|
||||||
preferred_locale += "preferred-locale".encode()
|
preferred_locale += "preferred-locale".encode()
|
||||||
preferred_locale += self._inner.preferred_locale.encode()
|
preferred_locale += self._inner.preferred_locale.encode()
|
||||||
self._send_unchecked(Packet.Type.preferred_locale,
|
self._send_unchecked(Packet.Type.preferred_locale, preferred_locale)
|
||||||
preferred_locale)
|
|
||||||
|
|
||||||
if remove_lock:
|
if remove_lock:
|
||||||
with self._authLock:
|
with self._authLock:
|
||||||
@@ -308,7 +557,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
if self._inner.conf.store_credentials:
|
if self._inner.conf.store_credentials:
|
||||||
reusable = self._apWelcome.reusable_auth_credentials
|
reusable = self._apWelcome.reusable_auth_credentials
|
||||||
reusable_type = Authentication.AuthenticationType.Name(
|
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:
|
if self._inner.conf.stored_credentials_file is None:
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
@@ -318,8 +568,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
{
|
{
|
||||||
"username": self._apWelcome.canonical_username,
|
"username": self._apWelcome.canonical_username,
|
||||||
"credentials": base64.b64encode(reusable).decode(),
|
"credentials": base64.b64encode(reusable).decode(),
|
||||||
"type": reusable_type
|
"type": reusable_type,
|
||||||
}, f)
|
},
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
|
||||||
elif packet.is_cmd(Packet.Type.auth_failure):
|
elif packet.is_cmd(Packet.Type.auth_failure):
|
||||||
ap_login_failed = Keyexchange.APLoginFailed()
|
ap_login_failed = Keyexchange.APLoginFailed()
|
||||||
@@ -329,8 +581,9 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
raise RuntimeError("Unknown CMD 0x" + packet.cmd.hex())
|
raise RuntimeError("Unknown CMD 0x" + packet.cmd.hex())
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self._LOGGER.info("Closing session. device_id: {}".format(
|
self._LOGGER.info(
|
||||||
self._inner.device_id))
|
"Closing session. device_id: {}".format(self._inner.device_id)
|
||||||
|
)
|
||||||
|
|
||||||
self._closing = True
|
self._closing = True
|
||||||
|
|
||||||
@@ -383,11 +636,9 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
listener.on_closed()
|
listener.on_closed()
|
||||||
self._closeListeners: typing.List[Session.CloseListener] = []
|
self._closeListeners: typing.List[Session.CloseListener] = []
|
||||||
|
|
||||||
self._reconnectionListeners: typing.List[
|
self._reconnectionListeners: typing.List[Session.ReconnectionListener] = []
|
||||||
Session.ReconnectionListener] = []
|
|
||||||
|
|
||||||
self._LOGGER.info("Closed session. device_id: {}".format(
|
self._LOGGER.info("Closed session. device_id: {}".format(self._inner.device_id))
|
||||||
self._inner.device_id))
|
|
||||||
|
|
||||||
def _send_unchecked(self, cmd: bytes, payload: bytes) -> None:
|
def _send_unchecked(self, cmd: bytes, payload: bytes) -> None:
|
||||||
self._cipherPair.send_encoded(self._conn, cmd, payload)
|
self._cipherPair.send_encoded(self._conn, cmd, payload)
|
||||||
@@ -530,16 +781,21 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
self._receiver.stop()
|
self._receiver.stop()
|
||||||
|
|
||||||
self._conn = Session.ConnectionHolder.create(
|
self._conn = Session.ConnectionHolder.create(
|
||||||
ApResolver.get_random_accesspoint(), self._inner.conf)
|
ApResolver.get_random_accesspoint(), self._inner.conf
|
||||||
|
)
|
||||||
self._connect()
|
self._connect()
|
||||||
self._authenticate_partial(
|
self._authenticate_partial(
|
||||||
Authentication.LoginCredentials(
|
Authentication.LoginCredentials(
|
||||||
typ=self._apWelcome.reusable_auth_credentials_type,
|
typ=self._apWelcome.reusable_auth_credentials_type,
|
||||||
username=self._apWelcome.canonical_username,
|
username=self._apWelcome.canonical_username,
|
||||||
auth_data=self._apWelcome.reusable_auth_credentials), True)
|
auth_data=self._apWelcome.reusable_auth_credentials,
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
self._LOGGER.info("Re-authenticated as {}!".format(
|
self._LOGGER.info(
|
||||||
self._apWelcome.canonical_username))
|
"Re-authenticated as {}!".format(self._apWelcome.canonical_username)
|
||||||
|
)
|
||||||
|
|
||||||
with self._reconnectionListenersLock:
|
with self._reconnectionListenersLock:
|
||||||
for listener in self._reconnectionListeners:
|
for listener in self._reconnectionListeners:
|
||||||
@@ -549,13 +805,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
if listener not in self._closeListeners:
|
if listener not in self._closeListeners:
|
||||||
self._closeListeners.append(listener)
|
self._closeListeners.append(listener)
|
||||||
|
|
||||||
def add_reconnection_listener(self,
|
def add_reconnection_listener(self, listener: ReconnectionListener) -> None:
|
||||||
listener: ReconnectionListener) -> None:
|
|
||||||
if listener not in self._reconnectionListeners:
|
if listener not in self._reconnectionListeners:
|
||||||
self._reconnectionListeners.append(listener)
|
self._reconnectionListeners.append(listener)
|
||||||
|
|
||||||
def remove_reconnection_listener(self,
|
def remove_reconnection_listener(self, listener: ReconnectionListener) -> None:
|
||||||
listener: ReconnectionListener) -> None:
|
|
||||||
self._reconnectionListeners.remove(listener)
|
self._reconnectionListeners.remove(listener)
|
||||||
|
|
||||||
def _parse_product_info(self, data) -> None:
|
def _parse_product_info(self, data) -> None:
|
||||||
@@ -572,12 +826,14 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
for i in range(len(product)):
|
for i in range(len(product)):
|
||||||
self._userAttributes[product[i].tag] = product[i].text
|
self._userAttributes[product[i].tag] = product[i].text
|
||||||
|
|
||||||
self._LOGGER.debug("Parsed product info: {}".format(
|
self._LOGGER.debug("Parsed product info: {}".format(self._userAttributes))
|
||||||
self._userAttributes))
|
|
||||||
|
|
||||||
def get_user_attribute(self, key: str, fallback: str = None) -> str:
|
def get_user_attribute(self, key: str, fallback: str = None) -> str:
|
||||||
return self._userAttributes.get(key) if self._userAttributes.get(
|
return (
|
||||||
key) is not None else fallback
|
self._userAttributes.get(key)
|
||||||
|
if self._userAttributes.get(key) is not None
|
||||||
|
else fallback
|
||||||
|
)
|
||||||
|
|
||||||
def event(self, resp: MercuryClient.Response) -> None:
|
def event(self, resp: MercuryClient.Response) -> None:
|
||||||
if resp.uri == "spotify:user:attributes:update":
|
if resp.uri == "spotify:user:attributes:update":
|
||||||
@@ -586,11 +842,13 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
|
|
||||||
for pair in attributes_update.pairs_list:
|
for pair in attributes_update.pairs_list:
|
||||||
self._userAttributes[pair.key] = pair.value
|
self._userAttributes[pair.key] = pair.value
|
||||||
self._LOGGER.info("Updated user attribute: {} -> {}".format(
|
self._LOGGER.info(
|
||||||
pair.key, pair.value))
|
"Updated user attribute: {} -> {}".format(pair.key, pair.value)
|
||||||
|
)
|
||||||
|
|
||||||
def on_message(self, uri: str, headers: typing.Dict[str, str],
|
def on_message(
|
||||||
payload: bytes) -> None:
|
self, uri: str, headers: typing.Dict[str, str], payload: bytes
|
||||||
|
) -> None:
|
||||||
if uri == "hm://connect-state/v1/connect/logout":
|
if uri == "hm://connect-state/v1/connect/logout":
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@@ -612,18 +870,21 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
conf = None
|
conf = None
|
||||||
preferred_locale: str = None
|
preferred_locale: str = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
device_type: Connect.DeviceType,
|
self,
|
||||||
device_name: str,
|
device_type: Connect.DeviceType,
|
||||||
preferred_locale: str,
|
device_name: str,
|
||||||
conf: Session.Configuration,
|
preferred_locale: str,
|
||||||
device_id: str = None):
|
conf: Session.Configuration,
|
||||||
|
device_id: str = None,
|
||||||
|
):
|
||||||
self.preferred_locale = preferred_locale
|
self.preferred_locale = preferred_locale
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.device_type = device_type
|
self.device_type = device_type
|
||||||
self.device_name = device_name
|
self.device_name = device_name
|
||||||
self.device_id = device_id if device_id is not None else Utils.random_hex_string(
|
self.device_id = (
|
||||||
40)
|
device_id if device_id is not None else Utils.random_hex_string(40)
|
||||||
|
)
|
||||||
|
|
||||||
class AbsBuilder:
|
class AbsBuilder:
|
||||||
conf = None
|
conf = None
|
||||||
@@ -657,7 +918,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def set_device_type(
|
def set_device_type(
|
||||||
self, device_type: Connect.DeviceType) -> Session.AbsBuilder:
|
self, device_type: Connect.DeviceType
|
||||||
|
) -> Session.AbsBuilder:
|
||||||
self.device_type = device_type
|
self.device_type = device_type
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -667,8 +929,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
def stored(self):
|
def stored(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stored_file(self,
|
def stored_file(self, stored_credentials: str = None) -> Session.Builder:
|
||||||
stored_credentials: str = None) -> Session.Builder:
|
|
||||||
if stored_credentials is None:
|
if stored_credentials is None:
|
||||||
stored_credentials = self.conf.stored_credentials_file
|
stored_credentials = self.conf.stored_credentials_file
|
||||||
if os.path.isfile(stored_credentials):
|
if os.path.isfile(stored_credentials):
|
||||||
@@ -680,10 +941,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.login_credentials = Authentication.LoginCredentials(
|
self.login_credentials = Authentication.LoginCredentials(
|
||||||
typ=Authentication.AuthenticationType.Value(
|
typ=Authentication.AuthenticationType.Value(obj["type"]),
|
||||||
obj["type"]),
|
|
||||||
username=obj["username"],
|
username=obj["username"],
|
||||||
auth_data=base64.b64decode(obj["credentials"]))
|
auth_data=base64.b64decode(obj["credentials"]),
|
||||||
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -693,7 +954,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
self.login_credentials = Authentication.LoginCredentials(
|
self.login_credentials = Authentication.LoginCredentials(
|
||||||
username=username,
|
username=username,
|
||||||
typ=Authentication.AuthenticationType.AUTHENTICATION_USER_PASS,
|
typ=Authentication.AuthenticationType.AUTHENTICATION_USER_PASS,
|
||||||
auth_data=password.encode())
|
auth_data=password.encode(),
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def create(self) -> Session:
|
def create(self) -> Session:
|
||||||
@@ -701,10 +963,15 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
raise RuntimeError("You must select an authentication method.")
|
raise RuntimeError("You must select an authentication method.")
|
||||||
|
|
||||||
session = Session(
|
session = Session(
|
||||||
Session.Inner(self.device_type, self.device_name,
|
Session.Inner(
|
||||||
self.preferred_locale, self.conf,
|
self.device_type,
|
||||||
self.device_id),
|
self.device_name,
|
||||||
ApResolver.get_random_accesspoint())
|
self.preferred_locale,
|
||||||
|
self.conf,
|
||||||
|
self.device_id,
|
||||||
|
),
|
||||||
|
ApResolver.get_random_accesspoint(),
|
||||||
|
)
|
||||||
session._connect()
|
session._connect()
|
||||||
session._authenticate(self.login_credentials)
|
session._authenticate(self.login_credentials)
|
||||||
return session
|
return session
|
||||||
@@ -731,12 +998,22 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
# Fetching
|
# Fetching
|
||||||
retry_on_chunk_error: bool
|
retry_on_chunk_error: bool
|
||||||
|
|
||||||
def __init__(self, proxy_enabled: bool, proxy_type: Proxy.Type,
|
def __init__(
|
||||||
proxy_address: str, proxy_port: int, proxy_auth: bool,
|
self,
|
||||||
proxy_username: str, proxy_password: str,
|
proxy_enabled: bool,
|
||||||
cache_enabled: bool, cache_dir: str,
|
proxy_type: Proxy.Type,
|
||||||
do_cache_clean_up: bool, store_credentials: bool,
|
proxy_address: str,
|
||||||
stored_credentials_file: str, retry_on_chunk_error: bool):
|
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.proxyEnabled = proxy_enabled
|
||||||
self.proxyType = proxy_type
|
self.proxyType = proxy_type
|
||||||
self.proxyAddress = proxy_address
|
self.proxyAddress = proxy_address
|
||||||
@@ -768,93 +1045,99 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
|
|
||||||
# Stored credentials
|
# Stored credentials
|
||||||
store_credentials: bool = True
|
store_credentials: bool = True
|
||||||
stored_credentials_file: str = os.path.join(
|
stored_credentials_file: str = os.path.join(os.getcwd(), "credentials.json")
|
||||||
os.getcwd(), "credentials.json")
|
|
||||||
|
|
||||||
# Fetching
|
# Fetching
|
||||||
retry_on_chunk_error: bool = None
|
retry_on_chunk_error: bool = None
|
||||||
|
|
||||||
def set_proxy_enabled(
|
def set_proxy_enabled(
|
||||||
self,
|
self, proxy_enabled: bool
|
||||||
proxy_enabled: bool) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.proxyEnabled = proxy_enabled
|
self.proxyEnabled = proxy_enabled
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_proxy_type(
|
def set_proxy_type(
|
||||||
self,
|
self, proxy_type: Proxy.Type
|
||||||
proxy_type: Proxy.Type) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.proxyType = proxy_type
|
self.proxyType = proxy_type
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_proxy_address(
|
def set_proxy_address(
|
||||||
self, proxy_address: str) -> Session.Configuration.Builder:
|
self, proxy_address: str
|
||||||
|
) -> Session.Configuration.Builder:
|
||||||
self.proxyAddress = proxy_address
|
self.proxyAddress = proxy_address
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_proxy_auth(
|
def set_proxy_auth(self, proxy_auth: bool) -> Session.Configuration.Builder:
|
||||||
self, proxy_auth: bool) -> Session.Configuration.Builder:
|
|
||||||
self.proxyAuth = proxy_auth
|
self.proxyAuth = proxy_auth
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_proxy_username(
|
def set_proxy_username(
|
||||||
self,
|
self, proxy_username: str
|
||||||
proxy_username: str) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.proxyUsername = proxy_username
|
self.proxyUsername = proxy_username
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_proxy_password(
|
def set_proxy_password(
|
||||||
self,
|
self, proxy_password: str
|
||||||
proxy_password: str) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.proxyPassword = proxy_password
|
self.proxyPassword = proxy_password
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_cache_enabled(
|
def set_cache_enabled(
|
||||||
self,
|
self, cache_enabled: bool
|
||||||
cache_enabled: bool) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.cache_enabled = cache_enabled
|
self.cache_enabled = cache_enabled
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_cache_dir(self,
|
def set_cache_dir(self, cache_dir: str) -> Session.Configuration.Builder:
|
||||||
cache_dir: str) -> Session.Configuration.Builder:
|
|
||||||
self.cache_dir = cache_dir
|
self.cache_dir = cache_dir
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_do_cache_clean_up(
|
def set_do_cache_clean_up(
|
||||||
self,
|
self, do_cache_clean_up: bool
|
||||||
do_cache_clean_up: bool) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.do_cache_clean_up = do_cache_clean_up
|
self.do_cache_clean_up = do_cache_clean_up
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_store_credentials(
|
def set_store_credentials(
|
||||||
self,
|
self, store_credentials: bool
|
||||||
store_credentials: bool) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.store_credentials = store_credentials
|
self.store_credentials = store_credentials
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_stored_credential_file(
|
def set_stored_credential_file(
|
||||||
self, stored_credential_file: str
|
self, stored_credential_file: str
|
||||||
) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.stored_credentials_file = stored_credential_file
|
self.stored_credentials_file = stored_credential_file
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_retry_on_chunk_error(
|
def set_retry_on_chunk_error(
|
||||||
self, retry_on_chunk_error: bool
|
self, retry_on_chunk_error: bool
|
||||||
) -> Session.Configuration.Builder:
|
) -> Session.Configuration.Builder:
|
||||||
self.retry_on_chunk_error = retry_on_chunk_error
|
self.retry_on_chunk_error = retry_on_chunk_error
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def build(self) -> Session.Configuration:
|
def build(self) -> Session.Configuration:
|
||||||
return Session.Configuration(
|
return Session.Configuration(
|
||||||
self.proxyEnabled, self.proxyType, self.proxyAddress,
|
self.proxyEnabled,
|
||||||
self.proxyPort, self.proxyAuth, self.proxyUsername,
|
self.proxyType,
|
||||||
self.proxyPassword, self.cache_enabled, self.cache_dir,
|
self.proxyAddress,
|
||||||
self.do_cache_clean_up, self.store_credentials,
|
self.proxyPort,
|
||||||
self.stored_credentials_file, self.retry_on_chunk_error)
|
self.proxyAuth,
|
||||||
|
self.proxyUsername,
|
||||||
|
self.proxyPassword,
|
||||||
|
self.cache_enabled,
|
||||||
|
self.cache_dir,
|
||||||
|
self.do_cache_clean_up,
|
||||||
|
self.store_credentials,
|
||||||
|
self.stored_credentials_file,
|
||||||
|
self.retry_on_chunk_error,
|
||||||
|
)
|
||||||
|
|
||||||
class SpotifyAuthenticationException(Exception):
|
class SpotifyAuthenticationException(Exception):
|
||||||
def __init__(self, login_failed: Keyexchange.APLoginFailed):
|
def __init__(self, login_failed: Keyexchange.APLoginFailed):
|
||||||
super().__init__(
|
super().__init__(Keyexchange.ErrorCode.Name(login_failed.error_code))
|
||||||
Keyexchange.ErrorCode.Name(login_failed.error_code))
|
|
||||||
|
|
||||||
class Accumulator:
|
class Accumulator:
|
||||||
buffer: bytes = bytes()
|
buffer: bytes = bytes()
|
||||||
@@ -881,8 +1164,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
self.sock = sock
|
self.sock = sock
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(addr: str,
|
def create(addr: str, conf: Session.Configuration) -> Session.ConnectionHolder:
|
||||||
conf: Session.Configuration) -> Session.ConnectionHolder:
|
|
||||||
ap_addr = addr.split(":")[0]
|
ap_addr = addr.split(":")[0]
|
||||||
ap_port = int(addr.split(":")[1])
|
ap_port = int(addr.split(":")[1])
|
||||||
if not conf.proxyEnabled or conf.proxyType is Proxy.Type.DIRECT:
|
if not conf.proxyEnabled or conf.proxyType is Proxy.Type.DIRECT:
|
||||||
@@ -894,11 +1176,9 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
sock = socket.socket()
|
sock = socket.socket()
|
||||||
sock.connect((conf.proxyAddress, conf.proxyPort))
|
sock.connect((conf.proxyAddress, conf.proxyPort))
|
||||||
|
|
||||||
sock.send("CONNECT {}:{} HTTP/1.0\n".format(ap_addr,
|
sock.send("CONNECT {}:{} HTTP/1.0\n".format(ap_addr, ap_port).encode())
|
||||||
ap_port).encode())
|
|
||||||
if conf.proxyAuth:
|
if conf.proxyAuth:
|
||||||
sock.send(
|
sock.send("Proxy-Authorization: {}\n".format(None).encode())
|
||||||
"Proxy-Authorization: {}\n".format(None).encode())
|
|
||||||
|
|
||||||
sock.send(b"\n")
|
sock.send(b"\n")
|
||||||
|
|
||||||
@@ -963,18 +1243,21 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
try:
|
try:
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
packet = self.session._cipherPair.receive_encoded(
|
packet = self.session._cipherPair.receive_encoded(
|
||||||
self.session._conn)
|
self.session._conn
|
||||||
|
)
|
||||||
cmd = Packet.Type.parse(packet.cmd)
|
cmd = Packet.Type.parse(packet.cmd)
|
||||||
if cmd is None:
|
if cmd is None:
|
||||||
self.session._LOGGER.info(
|
self.session._LOGGER.info(
|
||||||
"Skipping unknown command cmd: 0x{}, payload: {}".
|
"Skipping unknown command cmd: 0x{}, payload: {}".format(
|
||||||
format(Utils.bytes_to_hex(packet.cmd),
|
Utils.bytes_to_hex(packet.cmd), packet.payload
|
||||||
packet.payload))
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
except RuntimeError as ex:
|
except RuntimeError as ex:
|
||||||
if self.running:
|
if self.running:
|
||||||
self.session._LOGGER.fatal(
|
self.session._LOGGER.fatal(
|
||||||
"Failed reading packet! {}".format(ex))
|
"Failed reading packet! {}".format(ex)
|
||||||
|
)
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
self.session._reconnect()
|
self.session._reconnect()
|
||||||
break
|
break
|
||||||
@@ -985,17 +1268,18 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if self.session._scheduledReconnect is not None:
|
if self.session._scheduledReconnect is not None:
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
self.session._scheduler.cancel(
|
self.session._scheduler.cancel(self.session._scheduledReconnect)
|
||||||
self.session._scheduledReconnect)
|
|
||||||
|
|
||||||
def anonymous():
|
def anonymous():
|
||||||
self.session._LOGGER.warning(
|
self.session._LOGGER.warning(
|
||||||
"Socket timed out. Reconnecting...")
|
"Socket timed out. Reconnecting..."
|
||||||
|
)
|
||||||
self.session._reconnect()
|
self.session._reconnect()
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
self.session.scheduled_reconnect = self.session._scheduler.enter(
|
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)
|
self.session.send(Packet.Type.pong, packet.payload)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.pong_ack:
|
if cmd == Packet.Type.pong_ack:
|
||||||
@@ -1003,8 +1287,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
if cmd == Packet.Type.country_code:
|
if cmd == Packet.Type.country_code:
|
||||||
self.session.country_code = packet.payload.decode()
|
self.session.country_code = packet.payload.decode()
|
||||||
self.session._LOGGER.info(
|
self.session._LOGGER.info(
|
||||||
"Received country_code: {}".format(
|
"Received country_code: {}".format(self.session.country_code)
|
||||||
self.session.country_code))
|
)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.license_version:
|
if cmd == Packet.Type.license_version:
|
||||||
license_version = BytesInputStream(packet.payload)
|
license_version = BytesInputStream(packet.payload)
|
||||||
@@ -1013,34 +1297,40 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
|
|||||||
buffer = license_version.read()
|
buffer = license_version.read()
|
||||||
self.session._LOGGER.info(
|
self.session._LOGGER.info(
|
||||||
"Received license_version: {}, {}".format(
|
"Received license_version: {}, {}".format(
|
||||||
license_id, buffer.decode()))
|
license_id, buffer.decode()
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.session._LOGGER.info(
|
self.session._LOGGER.info(
|
||||||
"Received license_version: {}".format(license_id))
|
"Received license_version: {}".format(license_id)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.unknown_0x10:
|
if cmd == Packet.Type.unknown_0x10:
|
||||||
self.session._LOGGER.debug("Received 0x10: {}".format(
|
self.session._LOGGER.debug(
|
||||||
Utils.bytes_to_hex(packet.payload)))
|
"Received 0x10: {}".format(Utils.bytes_to_hex(packet.payload))
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.mercury_sub or \
|
if (
|
||||||
cmd == Packet.Type.mercury_unsub or \
|
cmd == Packet.Type.mercury_sub
|
||||||
cmd == Packet.Type.mercury_event or \
|
or cmd == Packet.Type.mercury_unsub
|
||||||
cmd == Packet.Type.mercury_req:
|
or cmd == Packet.Type.mercury_event
|
||||||
|
or cmd == Packet.Type.mercury_req
|
||||||
|
):
|
||||||
self.session.mercury().dispatch(packet)
|
self.session.mercury().dispatch(packet)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.aes_key or \
|
if cmd == Packet.Type.aes_key or cmd == Packet.Type.aes_key_error:
|
||||||
cmd == Packet.Type.aes_key_error:
|
|
||||||
self.session.audio_key().dispatch(packet)
|
self.session.audio_key().dispatch(packet)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.channel_error or \
|
if (
|
||||||
cmd == Packet.Type.stream_chunk_res:
|
cmd == Packet.Type.channel_error
|
||||||
|
or cmd == Packet.Type.stream_chunk_res
|
||||||
|
):
|
||||||
self.session.channel().dispatch(packet)
|
self.session.channel().dispatch(packet)
|
||||||
continue
|
continue
|
||||||
if cmd == Packet.Type.product_info:
|
if cmd == Packet.Type.product_info:
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
self.session._parse_product_info(packet.payload)
|
self.session._parse_product_info(packet.payload)
|
||||||
continue
|
continue
|
||||||
self.session._LOGGER.info("Skipping {}".format(
|
self.session._LOGGER.info("Skipping {}".format(Utils.bytes_to_hex(cmd)))
|
||||||
Utils.bytes_to_hex(cmd)))
|
|
||||||
|
|
||||||
self.session._LOGGER.debug("Session.Receiver stopped")
|
self.session._LOGGER.debug("Session.Receiver stopped")
|
||||||
|
|||||||
Reference in New Issue
Block a user