From fc462882e5428f32618668b06c52fd7867ecbb04 Mon Sep 17 00:00:00 2001 From: berettavexee Date: Sun, 14 Jun 2026 00:30:16 +0200 Subject: [PATCH 1/2] fix(deezer): wrap gw.get_track in asyncio.to_thread, cache user id, add loved tracks support - Fix: gw.get_track() was a blocking synchronous call inside an async context; wrap it in asyncio.to_thread() to avoid stalling the event loop during concurrent downloads. - Perf: cache the logged-in user ID in login() (_logged_in_user_id) so get_user_favorites() does not make an extra get_user_data() API round-trip on every call. - Feat: add DeezerFavoriteURL to support profile liked-track URLs of the form https://www.deezer.com/fr/profile/USER_ID/loved. - Refactor: replace the hardcoded 2000 favorites limit with a class constant _MAX_FAVORITES = 10_000. Co-Authored-By: Claude Sonnet 4.6 --- streamrip/client/deezer.py | 25 ++++++++++++++++++++++++- streamrip/rip/parse_url.py | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index 056463f9..abf15216 100644 --- a/streamrip/client/deezer.py +++ b/streamrip/client/deezer.py @@ -33,12 +33,14 @@ class DeezerClient(Client): source = "deezer" max_quality = 2 + _MAX_FAVORITES = 10_000 def __init__(self, config: Config): self.global_config = config self.client = deezer.Deezer() self.logged_in = False self.config = config.session.deezer + self._logged_in_user_id: int | None = None async def login(self): # Used for track downloads @@ -51,6 +53,7 @@ async def login(self): success = self.client.login_via_arl(arl) if not success: raise AuthenticationError + self._logged_in_user_id = self.client.gw.get_user_data()["USER"]["USER_ID"] self.logged_in = True async def get_metadata(self, item_id: str, media_type: str) -> dict: @@ -98,6 +101,9 @@ async def get_album(self, item_id: str) -> dict: return album_metadata async def get_playlist(self, item_id: str) -> dict: + if item_id.startswith("favorites:"): + return await self.get_user_favorites(item_id[len("favorites:"):]) + pl_metadata, pl_tracks = await asyncio.gather( asyncio.to_thread(self.client.api.get_playlist, item_id), asyncio.to_thread(self.client.api.get_playlist_tracks, item_id), @@ -106,6 +112,23 @@ async def get_playlist(self, item_id: str) -> dict: pl_metadata["track_total"] = len(pl_tracks["data"]) return pl_metadata + async def get_user_favorites(self, user_id: str) -> dict: + # deezer-py's get_user_tracks() drops the limit arg when routing to + # get_my_favorite_tracks(), so we detect own profile and call it directly. + if int(user_id) == self._logged_in_user_id: + tracks = await asyncio.to_thread( + self.client.gw.get_my_favorite_tracks, self._MAX_FAVORITES + ) + else: + tracks = await asyncio.to_thread( + self.client.gw.get_user_tracks, int(user_id), self._MAX_FAVORITES + ) + return { + "title": "Loved Tracks", + "tracks": tracks, + "track_total": len(tracks), + } + async def get_artist(self, item_id: str) -> dict: artist, albums = await asyncio.gather( asyncio.to_thread(self.client.api.get_artist, item_id), @@ -148,7 +171,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") diff --git a/streamrip/rip/parse_url.py b/streamrip/rip/parse_url.py index bdfa0b03..e10babc6 100644 --- a/streamrip/rip/parse_url.py +++ b/streamrip/rip/parse_url.py @@ -187,6 +187,28 @@ async def _extract_info_from_dynamic_link( raise Exception("Unable to extract Deezer dynamic link.") +class DeezerFavoriteURL(URL): + favorite_re = re.compile( + r"https://(?:www\.)?deezer\.com/[a-z]{2}/profile/(\d+)/loved" + ) + + @classmethod + def from_str(cls, url: str) -> URL | None: + match = cls.favorite_re.match(url) + if match is None: + return None + return cls(match, "deezer") + + async def into_pending( + self, + client: Client, + config: Config, + db: Database, + ) -> Pending: + user_id = self.match.group(1) + return PendingPlaylist(f"favorites:{user_id}", client, config, db) + + class SoundcloudURL(URL): source = "soundcloud" @@ -228,6 +250,7 @@ def parse_url(url: str) -> URL | None: """ url = url.strip() parsed_urls: list[URL | None] = [ + DeezerFavoriteURL.from_str(url), GenericURL.from_str(url), QobuzInterpreterURL.from_str(url), SoundcloudURL.from_str(url), From f32ee3119a8901021932eb8d71559d3b530223bb Mon Sep 17 00:00:00 2001 From: berettavexee Date: Sun, 14 Jun 2026 00:38:13 +0200 Subject: [PATCH 2/2] style(deezer): align new attribute names with project conventions Rename _MAX_FAVORITES -> max_favorites (matches max_quality style) and _logged_in_user_id -> logged_in_user_id (matches logged_in style). Co-Authored-By: Claude Sonnet 4.6 --- streamrip/client/deezer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index abf15216..7bd2d658 100644 --- a/streamrip/client/deezer.py +++ b/streamrip/client/deezer.py @@ -33,14 +33,14 @@ class DeezerClient(Client): source = "deezer" max_quality = 2 - _MAX_FAVORITES = 10_000 + max_favorites = 10_000 def __init__(self, config: Config): self.global_config = config self.client = deezer.Deezer() self.logged_in = False self.config = config.session.deezer - self._logged_in_user_id: int | None = None + self.logged_in_user_id: int | None = None async def login(self): # Used for track downloads @@ -53,7 +53,7 @@ async def login(self): success = self.client.login_via_arl(arl) if not success: raise AuthenticationError - self._logged_in_user_id = self.client.gw.get_user_data()["USER"]["USER_ID"] + self.logged_in_user_id = self.client.gw.get_user_data()["USER"]["USER_ID"] self.logged_in = True async def get_metadata(self, item_id: str, media_type: str) -> dict: @@ -115,13 +115,13 @@ async def get_playlist(self, item_id: str) -> dict: async def get_user_favorites(self, user_id: str) -> dict: # deezer-py's get_user_tracks() drops the limit arg when routing to # get_my_favorite_tracks(), so we detect own profile and call it directly. - if int(user_id) == self._logged_in_user_id: + if int(user_id) == self.logged_in_user_id: tracks = await asyncio.to_thread( - self.client.gw.get_my_favorite_tracks, self._MAX_FAVORITES + self.client.gw.get_my_favorite_tracks, self.max_favorites ) else: tracks = await asyncio.to_thread( - self.client.gw.get_user_tracks, int(user_id), self._MAX_FAVORITES + self.client.gw.get_user_tracks, int(user_id), self.max_favorites ) return { "title": "Loved Tracks",