Fix Spotify Premium Account Audio Key Retrieval
Some checks failed
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
unknown
2025-12-19 03:25:48 +01:00
parent 36d08aae85
commit 82b4b40e6b

View File

@@ -53,7 +53,7 @@ spoticlub_client_serial: typing.Optional[str] = None
spoticlub_loaded_logged: bool = False spoticlub_loaded_logged: bool = False
_spoticlub_audio_key_loader_enabled: bool = False _spoticlub_audio_key_loader_enabled: bool = False
_spoticlub_audio_key_loader_lock = threading.Lock() _spoticlub_audio_key_loader_lock = threading.Lock()
######################################## ######################################################
def _spoticlub_notify_session_done() -> None: def _spoticlub_notify_session_done() -> None:
global spoticlub_user, spoticlub_password, spoticlub_client_serial global spoticlub_user, spoticlub_password, spoticlub_client_serial
@@ -289,6 +289,7 @@ class AbsChunkedInputStream(io.BytesIO, HaltListener):
class AudioKeyManager(PacketsReceiver, Closeable): class AudioKeyManager(PacketsReceiver, Closeable):
audio_key_request_timeout = 20 audio_key_request_timeout = 20
max_spotify_audio_key_retries = 2
logger = logging.getLogger("Librespot:AudioKeyManager") logger = logging.getLogger("Librespot:AudioKeyManager")
__callbacks: typing.Dict[int, Callback] = {} __callbacks: typing.Dict[int, Callback] = {}
__seq_holder = 0 __seq_holder = 0
@@ -347,43 +348,88 @@ class AudioKeyManager(PacketsReceiver, Closeable):
except Exception: except Exception:
return False return False
def _get_spotify_audio_key(self, gid: bytes, file_id: bytes, retry: bool = True) -> bytes: def _get_spotify_audio_key(
"""Request the audio key directly from Spotify (Premium accounts).""" self,
tries = 0 gid: bytes,
file_id: bytes,
retry: bool = True,
_attempt: int = 1,
) -> 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:
seq = AudioKeyManager.__seq_holder
AudioKeyManager.__seq_holder += 1
callback = AudioKeyManager.SyncCallback(self)
AudioKeyManager.__callbacks[seq] = callback
last_err: typing.Optional[Exception] = None last_err: typing.Optional[Exception] = None
while True: try:
tries += 1 out = io.BytesIO()
callback = AudioKeyManager.SyncCallback(self) out.write(file_id)
out.write(gid)
out.write(struct.pack(">i", seq))
out.write(self.__zero_short)
out.seek(0)
with self.__seq_holder_lock: # Send the key request to Spotify.
AudioKeyManager.__seq_holder += 1 self.__session.send(Packet.Type.request_key, out.read())
seq = AudioKeyManager.__seq_holder
AudioKeyManager.__callbacks[seq] = callback
try: key = callback.wait_response()
payload = struct.pack(">i", seq) + gid + file_id if key is not None:
self.__session.send(Packet.Type.request_key, payload)
key = callback.wait_response()
if key is None:
raise RuntimeError("Audio key request failed")
return key return key
except Exception as exc: # noqa: BLE001
last_err = exc
self.logger.warning("Spotify audio key request failed (try %d): %s", tries, exc)
if not retry or tries >= 3:
break
time.sleep(1)
finally:
try:
AudioKeyManager.__callbacks.pop(seq, None)
except Exception:
pass
# No key returned; treat this like a transient failure.
last_err = RuntimeError("Audio key request returned no key")
except Exception as exc: # noqa: BLE001
last_err = exc
finally:
# Ensure we don't leak callbacks if anything goes wrong.
try:
AudioKeyManager.__callbacks.pop(seq, None)
except Exception:
pass
# Decide whether to retry or give up.
if retry and _attempt < self.max_spotify_audio_key_retries:
self.logger.warning(
"Spotify audio key request failed (attempt %d/%d): %s",
_attempt,
self.max_spotify_audio_key_retries,
last_err,
)
# Simple linear backoff to avoid hammering the server.
time.sleep(5 * _attempt)
return self._get_spotify_audio_key(
gid,
file_id,
retry=True,
_attempt=_attempt + 1,
)
# Give up after the configured number of attempts.
self.logger.error(
"Giving up fetching audio key from Spotify after %d attempts; gid=%s fileId=%s (last error: %s)",
_attempt,
util.bytes_to_hex(gid),
util.bytes_to_hex(file_id),
last_err,
)
raise RuntimeError( raise RuntimeError(
"Failed fetching Audio Key from Spotify for gid: {}, fileId: {} (last error: {})".format( "Failed fetching Audio Key from Spotify for gid: {}, fileId: {} (last error: {})".format(
util.bytes_to_hex(gid), util.bytes_to_hex(file_id), last_err util.bytes_to_hex(gid),
util.bytes_to_hex(file_id),
last_err,
) )
) )