Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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(...)`.
8 changes: 4 additions & 4 deletions streamrip/client/deezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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. "
Expand Down
4 changes: 2 additions & 2 deletions streamrip/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down