New Audio Key flow for both Premium/Free plans
Some checks failed
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
unknown
2025-12-19 03:36:53 +01:00
parent 82b4b40e6b
commit afed515855

View File

@@ -321,12 +321,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
packet.cmd, len(packet.payload))) packet.cmd, len(packet.payload)))
def _is_premium_user(self) -> bool: def _is_premium_user(self) -> bool:
"""Best-effort check whether the current Spotify account is Premium.
We rely on Session user attributes populated from the AP product_info packet.
Historically, the attribute named "type" contains values like "premium", "free",
"open", etc.
"""
try: try:
raw = ( raw = (
self.__session.get_user_attribute("type") self.__session.get_user_attribute("type")
@@ -355,16 +349,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
retry: bool = True, retry: bool = True,
_attempt: int = 1, _attempt: int = 1,
) -> bytes: ) -> bytes:
"""Request the audio key directly from Spotify (Premium accounts).
This mirrors the working logic from the standalone AudioKeyManager
helper in temp.py: it sends a request_key packet with the payload
[file_id][gid][seq][zero_short], waits for a response, and retries
a limited number of times with simple backoff.
"""
# Allocate a new sequence number for this request and register
# the callback that will receive the response.
with self.__seq_holder_lock: with self.__seq_holder_lock:
seq = AudioKeyManager.__seq_holder seq = AudioKeyManager.__seq_holder
AudioKeyManager.__seq_holder += 1 AudioKeyManager.__seq_holder += 1
@@ -389,18 +373,15 @@ class AudioKeyManager(PacketsReceiver, Closeable):
if key is not None: if key is not None:
return key return key
# No key returned; treat this like a transient failure.
last_err = RuntimeError("Audio key request returned no key") last_err = RuntimeError("Audio key request returned no key")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
last_err = exc last_err = exc
finally: finally:
# Ensure we don't leak callbacks if anything goes wrong.
try: try:
AudioKeyManager.__callbacks.pop(seq, None) AudioKeyManager.__callbacks.pop(seq, None)
except Exception: except Exception:
pass pass
# Decide whether to retry or give up.
if retry and _attempt < self.max_spotify_audio_key_retries: if retry and _attempt < self.max_spotify_audio_key_retries:
self.logger.warning( self.logger.warning(
"Spotify audio key request failed (attempt %d/%d): %s", "Spotify audio key request failed (attempt %d/%d): %s",
@@ -408,7 +389,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
self.max_spotify_audio_key_retries, self.max_spotify_audio_key_retries,
last_err, last_err,
) )
# Simple linear backoff to avoid hammering the server.
time.sleep(5 * _attempt) time.sleep(5 * _attempt)
return self._get_spotify_audio_key( return self._get_spotify_audio_key(
gid, gid,
@@ -417,7 +397,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
_attempt=_attempt + 1, _attempt=_attempt + 1,
) )
# Give up after the configured number of attempts.
self.logger.error( self.logger.error(
"Giving up fetching audio key from Spotify after %d attempts; gid=%s fileId=%s (last error: %s)", "Giving up fetching audio key from Spotify after %d attempts; gid=%s fileId=%s (last error: %s)",
_attempt, _attempt,
@@ -433,14 +412,27 @@ class AudioKeyManager(PacketsReceiver, Closeable):
) )
) )
def get_audio_key(self, def get_audio_key(
gid: bytes, self,
file_id: bytes, gid: bytes,
retry: bool = True) -> bytes: file_id: bytes,
# If the user is Premium, Spotify will return audio keys directly. retry: bool = True,
# In that case, do not use the SpotiClub API. ) -> bytes:
if self._is_premium_user():
return self._get_spotify_audio_key(gid, file_id, retry=retry) is_premium = self._is_premium_user()
if is_premium:
try:
return self._get_spotify_audio_key(gid, file_id, retry=retry)
except Exception as exc: # noqa: BLE001
self.logger.warning(
"Spotify audio key fetch failed for premium user; falling back to SpotiClub API: %s",
exc,
)
print(
"\n[AudioKey] Spotify refused or failed to provide the audio key. "
"Falling back to SpotiClub API...\n"
)
global spoticlub_user, spoticlub_password, spoticlub_client_serial, spoticlub_loaded_logged global spoticlub_user, spoticlub_password, spoticlub_client_serial, spoticlub_loaded_logged
if not spoticlub_user or not spoticlub_password or spoticlub_user == "anonymous": if not spoticlub_user or not spoticlub_password or spoticlub_user == "anonymous":
@@ -499,9 +491,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
resp = requests.post(server_url, json=payload, timeout=AudioKeyManager.audio_key_request_timeout) resp = requests.post(server_url, json=payload, timeout=AudioKeyManager.audio_key_request_timeout)
# If another client instance is already active for this
# SpotiClub user, we will will reply with HTTP 423 and
# instruct this client to wait before retrying.
if resp.status_code == 423: if resp.status_code == 423:
try: try:
data = resp.json() data = resp.json()
@@ -549,7 +538,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
if len(key_bytes) != 16: if len(key_bytes) != 16:
raise RuntimeError("[SpotiClub API] Woops, received Audio Key must be 16 bytes long") raise RuntimeError("[SpotiClub API] Woops, received Audio Key must be 16 bytes long")
# After the first successful SpotiClub key fetch, enable the loader for future calls.
with _spoticlub_audio_key_loader_lock: with _spoticlub_audio_key_loader_lock:
_spoticlub_audio_key_loader_enabled = True _spoticlub_audio_key_loader_enabled = True
return key_bytes return key_bytes