diff --git a/librespot/audio/__init__.py b/librespot/audio/__init__.py index 7704e4a..dcc6d33 100644 --- a/librespot/audio/__init__.py +++ b/librespot/audio/__init__.py @@ -53,7 +53,7 @@ spoticlub_client_serial: typing.Optional[str] = None spoticlub_loaded_logged: bool = False _spoticlub_audio_key_loader_enabled: bool = False _spoticlub_audio_key_loader_lock = threading.Lock() -######################################## +###################################################### def _spoticlub_notify_session_done() -> None: global spoticlub_user, spoticlub_password, spoticlub_client_serial @@ -289,6 +289,7 @@ class AbsChunkedInputStream(io.BytesIO, HaltListener): class AudioKeyManager(PacketsReceiver, Closeable): audio_key_request_timeout = 20 + max_spotify_audio_key_retries = 2 logger = logging.getLogger("Librespot:AudioKeyManager") __callbacks: typing.Dict[int, Callback] = {} __seq_holder = 0 @@ -347,43 +348,88 @@ class AudioKeyManager(PacketsReceiver, Closeable): except Exception: return False - def _get_spotify_audio_key(self, gid: bytes, file_id: bytes, retry: bool = True) -> bytes: - """Request the audio key directly from Spotify (Premium accounts).""" - tries = 0 + def _get_spotify_audio_key( + self, + 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 - while True: - tries += 1 - callback = AudioKeyManager.SyncCallback(self) + try: + out = io.BytesIO() + 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: - AudioKeyManager.__seq_holder += 1 - seq = AudioKeyManager.__seq_holder - AudioKeyManager.__callbacks[seq] = callback + # Send the key request to Spotify. + self.__session.send(Packet.Type.request_key, out.read()) - try: - payload = struct.pack(">i", seq) + gid + file_id - self.__session.send(Packet.Type.request_key, payload) - - key = callback.wait_response() - if key is None: - raise RuntimeError("Audio key request failed") + key = callback.wait_response() + if key is not None: 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( "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, ) )