Merge pull request #32 from kokarare1212/restyled/deepsource-transform-c875c54e

Restyle Format code with yapf
This commit is contained in:
こうから
2021-05-21 12:52:03 +09:00
committed by GitHub
5 changed files with 526 additions and 170 deletions

View File

@@ -1,13 +1,15 @@
import os
import platform
import re
import subprocess
import time
import requests
from librespot.audio.decoders import AudioQuality from librespot.audio.decoders import AudioQuality
from librespot.core import Session from librespot.core import Session
from librespot.metadata import TrackId from librespot.metadata import TrackId
from librespot.player.codecs import VorbisOnlyAudioQuality from librespot.player.codecs import VorbisOnlyAudioQuality
import os
import platform
import re
import requests
import subprocess
import time
quality: AudioQuality = AudioQuality.VERY_HIGH quality: AudioQuality = AudioQuality.VERY_HIGH
session: Session = None session: Session = None
@@ -34,7 +36,8 @@ def client():
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 = (track_uri_search
if track_uri_search is not None else if track_uri_search is not None else
@@ -56,18 +59,24 @@ 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(
"https://api.spotify.com/v1/search",
{
"limit": "5", "limit": "5",
"offset": "0", "offset": "0",
"q": cmd[2:], "q": cmd[2:],
"type": "track" "type": "track"
}, },
headers={"Authorization": "Bearer %s" % token}) 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("%d, %s | %s" % (
[artist["name"] for artist in track["artists"]]))) i,
track["name"],
",".join([artist["name"] for artist in track["artists"]]),
))
i += 1 i += 1
position = -1 position = -1
while True: while True:
@@ -106,10 +115,12 @@ 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:

View File

@@ -1,10 +1,11 @@
from librespot.audio.HaltListener import HaltListener
from librespot.standard.InputStream import InputStream
import math import math
import threading import threading
import time import time
import typing import typing
from librespot.audio.HaltListener import HaltListener
from librespot.standard.InputStream import InputStream
class AbsChunkedInputStream(InputStream, HaltListener): class AbsChunkedInputStream(InputStream, HaltListener):
preload_ahead: typing.Final[int] = 3 preload_ahead: typing.Final[int] = 3
@@ -110,8 +111,8 @@ class AbsChunkedInputStream(InputStream, HaltListener):
for i in range(chunk + 1, for i in range(chunk + 1,
min(self.chunks() - 1, chunk + self.preload_ahead) + 1): min(self.chunks() - 1, chunk + self.preload_ahead) + 1):
if self.requested_chunks( if (self.requested_chunks()[i]
)[i] and self.retries[i] < self.preload_chunk_retries: 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

View File

@@ -1,13 +1,18 @@
from __future__ import annotations from __future__ import annotations
from librespot.audio import NormalizationData, PlayableContentFeeder, HaltListener
from librespot.common import Utils
from librespot.core import Session
from librespot.proto import Metadata, StorageResolve
import logging import logging
import random import random
import time import time
import typing import typing
from librespot.audio import HaltListener
from librespot.audio import NormalizationData
from librespot.audio import PlayableContentFeeder
from librespot.common import Utils
from librespot.core import Session
from librespot.proto import Metadata
from librespot.proto import StorageResolve
class CdnFeedHelper: class CdnFeedHelper:
_LOGGER: logging = logging.getLogger(__name__) _LOGGER: logging = logging.getLogger(__name__)
@@ -17,10 +22,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:
@@ -33,12 +42,15 @@ class CdnFeedHelper:
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(
@@ -57,15 +69,20 @@ class CdnFeedHelper:
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,
streamer,
None,
PlayableContentFeeder.PlayableContentFeeder.Metrics( PlayableContentFeeder.PlayableContentFeeder.Metrics(
None, False, -1)) 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 +95,12 @@ 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

@@ -1,11 +1,5 @@
from __future__ import annotations from __future__ import annotations
from librespot.audio.AbsChunkedInputStream import AbsChunkedInputStream
from librespot.audio import GeneralAudioStream, GeneralWritableStream, StreamId
from librespot.audio.decrypt import AesAudioDecrypt, NoopAudioDecrypt
from librespot.audio.format import SuperAudioFormat
from librespot.audio.storage import ChannelManager
from librespot.common import Utils
from librespot.proto import StorageResolve
import concurrent.futures import concurrent.futures
import logging import logging
import math import math
@@ -14,9 +8,20 @@ import time
import typing import typing
import urllib.parse import urllib.parse
from librespot.audio import GeneralAudioStream
from librespot.audio import GeneralWritableStream
from librespot.audio import StreamId
from librespot.audio.AbsChunkedInputStream import AbsChunkedInputStream
from librespot.audio.decrypt import AesAudioDecrypt
from librespot.audio.decrypt import NoopAudioDecrypt
from librespot.audio.format import SuperAudioFormat
from librespot.audio.storage import ChannelManager
from librespot.common import Utils
from librespot.proto import StorageResolve
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from librespot.audio.HaltListener import HaltListener
from librespot.audio.decrypt.AudioDecrypt import AudioDecrypt from librespot.audio.decrypt.AudioDecrypt import AudioDecrypt
from librespot.audio.HaltListener import HaltListener
from librespot.cache.CacheManager import CacheManager from librespot.cache.CacheManager import CacheManager
from librespot.core.Session import Session from librespot.core.Session import Session
from librespot.proto import Metadata from librespot.proto import Metadata
@@ -30,9 +35,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))
@@ -46,24 +53,41 @@ class CdnManager:
def stream_external_episode(self, episode: Metadata.Episode, def stream_external_episode(self, episode: Metadata.Episode,
external_url: str, 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)
@@ -159,8 +183,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 +201,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
@@ -251,12 +283,12 @@ 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(
self._cdnUrl._url,
headers={ headers={
"Range": "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)

View File

@@ -1,26 +1,9 @@
from __future__ import annotations from __future__ import annotations
from Crypto.Hash import HMAC, SHA1
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from librespot.audio import AudioKeyManager, PlayableContentFeeder
from librespot.audio.cdn import CdnManager
from librespot.audio.storage import ChannelManager
from librespot.cache import CacheManager
from librespot.common.Utils import Utils
from librespot.core import ApResolver, EventService, SearchManager, TokenProvider
from librespot.crypto import CipherPair, DiffieHellman, Packet
from librespot.dealer import ApiClient, DealerClient
from librespot.mercury import MercuryClient, SubListener
from librespot.proto import Authentication, Connect, Keyexchange
from librespot.proto.ExplicitContentPubsub import UserAttributesUpdate
from librespot.standard import BytesInputStream, Closeable, Proxy
from librespot.Version import Version
import base64 import base64
import defusedxml.ElementTree
import json import json
import logging import logging
import os import os
import requests
import sched import sched
import socket import socket
import struct import struct
@@ -28,32 +11,299 @@ import threading
import time import time
import typing import typing
import defusedxml.ElementTree
import requests
from Crypto.Hash import HMAC
from Crypto.Hash import SHA1
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from librespot.audio import AudioKeyManager
from librespot.audio import PlayableContentFeeder
from librespot.audio.cdn import CdnManager
from librespot.audio.storage import ChannelManager
from librespot.cache import CacheManager
from librespot.common.Utils import Utils
from librespot.core import ApResolver
from librespot.core import EventService
from librespot.core import SearchManager
from librespot.core import TokenProvider
from librespot.crypto import CipherPair
from librespot.crypto import DiffieHellman
from librespot.crypto import Packet
from librespot.dealer import ApiClient
from librespot.dealer import DealerClient
from librespot.mercury import MercuryClient
from librespot.mercury import SubListener
from librespot.proto import Authentication
from librespot.proto import Connect
from librespot.proto import Keyexchange
from librespot.proto.ExplicitContentPubsub import UserAttributesUpdate
from librespot.standard import BytesInputStream
from librespot.standard import Closeable
from librespot.standard import Proxy
from librespot.Version import Version
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, 0xAC,
0xbf, 0xbf, 0x86, 0x3d, 0xa1, 0x91, 0xc6, 0xcc, 0x33, 0x6c, 0x93, 0xa1, 0xE0,
0x4f, 0xb3, 0xb0, 0x16, 0x12, 0xac, 0xac, 0x6a, 0xf1, 0x80, 0xe7, 0xf6, 0x46,
0x14, 0xd9, 0x42, 0x9d, 0xbe, 0x2e, 0x34, 0x66, 0x43, 0xe3, 0x62, 0xd2, 0x0B,
0x32, 0x7a, 0x1a, 0x0d, 0x92, 0x3b, 0xae, 0xdd, 0x14, 0x02, 0xb1, 0x81, 0xFF,
0x55, 0x05, 0x61, 0x04, 0xd5, 0x2c, 0x96, 0xa4, 0x4c, 0x1e, 0xcc, 0x02, 0xC2,
0x4a, 0xd4, 0xb2, 0x0c, 0x00, 0x1f, 0x17, 0xed, 0xc2, 0x2f, 0xc4, 0x35, 0x30,
0x21, 0xc8, 0xf0, 0xcb, 0xae, 0xd2, 0xad, 0xd7, 0x2b, 0x0f, 0x9d, 0xb3, 0xAF,
0xc5, 0x32, 0x1a, 0x2a, 0xfe, 0x59, 0xf3, 0x5a, 0x0d, 0xac, 0x68, 0xf1, 0xF4,
0xfa, 0x62, 0x1e, 0xfb, 0x2c, 0x8d, 0x0c, 0xb7, 0x39, 0x2d, 0x92, 0x47, 0x6B,
0xe3, 0xd7, 0x35, 0x1a, 0x6d, 0xbd, 0x24, 0xc2, 0xae, 0x25, 0x5b, 0x88, 0xFE,
0xff, 0xab, 0x73, 0x29, 0x8a, 0x0b, 0xcc, 0xcd, 0x0c, 0x58, 0x67, 0x31, 0xC3,
0x89, 0xe8, 0xbd, 0x34, 0x80, 0x78, 0x4a, 0x5f, 0xc9, 0x6b, 0x89, 0x9d, 0xBF,
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, 0x86,
0xa3, 0x96, 0x13, 0xc4, 0x2f, 0x99, 0xc0, 0x27, 0xbf, 0xed, 0x04, 0x9c, 0x3D,
0x3c, 0x27, 0x58, 0x04, 0xb6, 0xb2, 0x19, 0xf9, 0xc1, 0x2f, 0x02, 0xe9, 0xA1,
0x48, 0x63, 0xec, 0xa1, 0xb6, 0x42, 0xa0, 0x9d, 0x48, 0x25, 0xf8, 0xb3, 0x91,
0x9d, 0xd0, 0xe8, 0x6a, 0xf9, 0x48, 0x4d, 0xa1, 0xc2, 0xba, 0x86, 0x30, 0xC6,
0x42, 0xea, 0x9d, 0xb3, 0x08, 0x6c, 0x19, 0x0e, 0x48, 0xb3, 0x9d, 0x66, 0xCC,
0xeb, 0x00, 0x06, 0xa2, 0x5a, 0xee, 0xa1, 0x1b, 0x13, 0x87, 0x3c, 0xd7, 0x33,
0x19, 0xe6, 0x55, 0xbd 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
@@ -101,14 +351,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 +371,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
@@ -140,7 +392,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
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)
@@ -178,8 +431,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
diffie_hellman.gs) 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
@@ -203,7 +458,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
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(
) )
@@ -216,8 +472,10 @@ 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[0] << 24)
scrap[2] << 8) | (scrap[3] & 0xff) | (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)
@@ -277,8 +535,10 @@ 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(Packet.Type.login,
client_response_encrypted.SerializeToString()) client_response_encrypted.SerializeToString())
@@ -318,8 +578,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()
@@ -536,7 +798,10 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
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("Re-authenticated as {}!".format(
self._apWelcome.canonical_username)) self._apWelcome.canonical_username))
@@ -576,8 +841,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
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 (self._userAttributes.get(key)
key) is not None else fallback 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":
@@ -612,18 +877,20 @@ 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 = (device_id if device_id is not None else
40) Utils.random_hex_string(40))
class AbsBuilder: class AbsBuilder:
conf = None conf = None
@@ -683,7 +950,8 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
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 +961,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 +970,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 +1005,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
@@ -845,11 +1129,20 @@ 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):
@@ -1022,18 +1315,17 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
self.session._LOGGER.debug("Received 0x10: {}".format( self.session._LOGGER.debug("Received 0x10: {}".format(
Utils.bytes_to_hex(packet.payload))) Utils.bytes_to_hex(packet.payload)))
continue continue
if cmd == Packet.Type.mercury_sub or \ if (cmd == Packet.Type.mercury_sub
cmd == Packet.Type.mercury_unsub or \ or cmd == Packet.Type.mercury_unsub
cmd == Packet.Type.mercury_event or \ or cmd == Packet.Type.mercury_event
cmd == Packet.Type.mercury_req: 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.channel_error
cmd == Packet.Type.stream_chunk_res: 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: