diff --git a/.jules/bolt.md b/.jules/bolt.md index 2d2b3192..babd2024 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2025-03-20 - Database Connection Pooling **Learning:** `streamrip/db.py` creates a new `sqlite3.connect` for *every single database operation* (`contains`, `add`, `all`). In bulk operations like resolving a playlist with hundreds of tracks, this introduces significant overhead. **Action:** Use a single persistent connection per database instance instead of creating a new one on every method call. + +## 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(...)`. diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index 056463f9..9eebf449 100644 --- a/streamrip/client/deezer.py +++ b/streamrip/client/deezer.py @@ -148,7 +148,7 @@ async def get_downloadable( # TODO: optimize such that all of the ids are requested at once dl_info: dict = {"quality": quality, "id": item_id} - track_info = self.client.gw.get_track(item_id) + track_info = await asyncio.to_thread(self.client.gw.get_track, item_id) fallback_id = track_info.get("FALLBACK", {}).get("SNG_ID") @@ -161,7 +161,7 @@ async def get_downloadable( int(track_info.get(f"FILESIZE_{format}", 0)) for _, format in quality_map ] dl_info["quality_to_size"] = size_map - + # Check if requested quality is available if size_map[quality] == 0: if self.config.lower_quality_if_not_available: @@ -178,7 +178,7 @@ async def get_downloadable( raise NonStreamableError( f"The requested quality {quality} is not available and fallback is disabled." ) - + # Update the quality in dl_info to reflect the final quality used dl_info["quality"] = quality @@ -187,7 +187,7 @@ async def get_downloadable( token = track_info["TRACK_TOKEN"] try: logger.debug("Fetching deezer url with token %s", token) - url = self.client.get_track_url(token, format_str) + url = await asyncio.to_thread(self.client.get_track_url, token, format_str) except deezer.WrongLicense: raise NonStreamableError( "The requested quality is not available with your subscription. " diff --git a/streamrip/db.py b/streamrip/db.py index 76373144..a3558559 100644 --- a/streamrip/db.py +++ b/streamrip/db.py @@ -82,7 +82,7 @@ def __init__(self, path: str): def __del__(self): """Ensure connection is closed on exit.""" - if hasattr(self, 'conn') and self.conn: + if hasattr(self, "conn") and self.conn: self.conn.close() def _table_exists(self) -> bool: @@ -168,7 +168,7 @@ def all(self): def reset(self): """Delete the database file.""" - if hasattr(self, 'conn') and self.conn: + if hasattr(self, "conn") and self.conn: self.conn.close() self.conn = None try: