diff --git a/streamrip/client/deezer.py b/streamrip/client/deezer.py index 056463f9..7bd2d658 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),