diff --git a/.jules/bolt.md b/.jules/bolt.md index babd2024..89a1eeb0 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2026-03-20 - Non-blocking I/O in Deezer client **Learning:** `streamrip/client/deezer.py` uses the synchronous `deezer-python` library. Direct calls like `client.gw.get_track()` and `client.get_track_url()` block the entire `asyncio` event loop. While the metadata fetching methods (`get_track`, `get_album`, etc.) correctly wrapped these calls in `await asyncio.to_thread(...)`, `get_downloadable` missed this, causing heavy blocking during concurrent downloads. **Action:** Ensure all synchronous third-party API calls in async methods are wrapped with `await asyncio.to_thread(...)`. + +## 2026-04-02 - Short-circuiting Async Requests +**Learning:** In `streamrip/client/qobuz.py`, `_get_valid_secret` was using `asyncio.gather` to test multiple potential API secrets. This forced the application to wait for the slowest secret test to complete before proceeding, causing unnecessary slow-tail latency and wasted network I/O. +**Action:** Use `asyncio.as_completed` to yield the first successful valid secret immediately, and explicitly cancel the remaining pending tasks in a `finally` block to prevent background task leakage. diff --git a/streamrip/client/qobuz.py b/streamrip/client/qobuz.py index 734e2b82..32548131 100644 --- a/streamrip/client/qobuz.py +++ b/streamrip/client/qobuz.py @@ -409,14 +409,20 @@ async def _test_secret(self, secret: str) -> Optional[str]: return None async def _get_valid_secret(self, secrets: list[str]) -> str: - results = await asyncio.gather( - *[self._test_secret(secret) for secret in secrets], - ) - working_secrets = [r for r in results if r is not None] - if len(working_secrets) == 0: - raise InvalidAppSecretError(secrets) - - return working_secrets[0] + # ⚡ Bolt: Use asyncio.as_completed to return immediately when a valid secret is found. + # This prevents waiting for all requests to finish and cancels the remaining slow-tail tasks. + tasks = [asyncio.create_task(self._test_secret(secret)) for secret in secrets] + try: + for future in asyncio.as_completed(tasks): + result = await future + if result is not None: + return result + finally: + for task in tasks: + if not task.done(): + task.cancel() + + raise InvalidAppSecretError(secrets) async def _request_file_url( self,