Restyled by black

This commit is contained in:
Restyled.io
2021-05-21 03:40:40 +00:00
parent 1e0e47cced
commit 5132c9e023
5 changed files with 648 additions and 297 deletions

View File

@@ -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", "-"], )
ffplay = subprocess.Popen(
["ffplay", "-"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=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(
"=================================\n"
"| Librespot-Python Player |\n" "| Librespot-Python Player |\n"
"| |\n" "| |\n"
"| by kokarare1212 |\n" "| by kokarare1212 |\n"
"=================================\n\n\n") "=================================\n\n\n"
)
def main(): def main():

View File

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

View File

@@ -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(
session: Session,
track: Metadata.Track,
file: Metadata.AudioFile,
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str], resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str],
preload: bool, halt_listener: HaltListener)\ preload: bool,
-> PlayableContentFeeder.PlayableContentFeeder.LoadedStream: 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
),
)

View File

@@ -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(
self._session,
StreamId(episode),
SuperAudioFormat.MP3, SuperAudioFormat.MP3,
CdnManager.CdnUrl(self, None, external_url), CdnManager.CdnUrl(self, None, external_url),
self._session.cache(), NoopAudioDecrypt(), self._session.cache(),
halt_listener) 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,
key: bytes,
url: str,
halt_listener: HaltListener,
):
return CdnManager.Streamer(
self._session,
StreamId.StreamId(file),
SuperAudioFormat.get(file.format), SuperAudioFormat.get(file.format),
CdnManager.CdnUrl(self, file.file_id, url), CdnManager.CdnUrl(self, file.file_id, url),
self._session.cache(), AesAudioDecrypt(key), self._session.cache(),
halt_listener) 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
)
)

View File

@@ -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(),
crypto_response=Keyexchange.CryptoResponseUnion())
client_response_plaintext_bytes = client_response_plaintext.SerializeToString(
) )
),
pow_response=Keyexchange.PoWResponseUnion(),
crypto_response=Keyexchange.CryptoResponseUnion(),
)
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__(
self,
device_type: Connect.DeviceType, device_type: Connect.DeviceType,
device_name: str, device_name: str,
preferred_locale: str, preferred_locale: str,
conf: Session.Configuration, conf: Session.Configuration,
device_id: str = None): 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,66 +1045,64 @@ 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
@@ -845,16 +1120,24 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
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")