diff --git a/core/qobuz_client.py b/core/qobuz_client.py index c2cf4e13..cf4fbf9b 100644 --- a/core/qobuz_client.py +++ b/core/qobuz_client.py @@ -887,11 +887,14 @@ class QobuzClient(DownloadSourcePlugin): logger.info(f"Retrieved Qobuz playlist '{normalized['name']}' with {len(tracks)} tracks") return normalized - def get_user_favorite_tracks(self, limit: int = 500) -> List[Dict[str, Any]]: + def get_user_favorite_tracks(self, limit: Optional[int] = None) -> List[Dict[str, Any]]: """Fetch the authenticated user's favorited tracks. Mirrors ``TidalClient.get_collection_tracks`` — the Sync page's - Favorite Tracks card pulls from here on click. + Favorite Tracks card pulls from here on click. By default this + fetches the full favorites collection so the card count and the + discovered track list cannot silently diverge. Pass ``limit`` for + explicit capped callers. """ if not self.is_authenticated(): logger.warning("Qobuz not authenticated — cannot list favorite tracks") @@ -899,8 +902,11 @@ class QobuzClient(DownloadSourcePlugin): tracks: List[Dict[str, Any]] = [] offset = 0 - while len(tracks) < limit: - page_size = min(self._PLAYLIST_PAGE_SIZE, limit - len(tracks)) + while True: + page_size = self._PLAYLIST_PAGE_SIZE if limit is None else min(self._PLAYLIST_PAGE_SIZE, limit - len(tracks)) + if page_size <= 0: + break + data = self._api_request('favorite/getUserFavorites', { 'type': 'tracks', 'limit': page_size, @@ -924,6 +930,8 @@ class QobuzClient(DownloadSourcePlugin): offset += len(items) if offset >= total or len(items) < page_size: break + if limit is not None and len(tracks) >= limit: + break logger.info(f"Retrieved {len(tracks)} Qobuz favorite tracks") return tracks diff --git a/tests/test_qobuz_playlists.py b/tests/test_qobuz_playlists.py index 3a3a9bea..828a9f97 100644 --- a/tests/test_qobuz_playlists.py +++ b/tests/test_qobuz_playlists.py @@ -331,6 +331,60 @@ def test_get_user_favorite_tracks_paginates(authed_client): assert tracks[-1]['name'] == 'F129' +def test_get_user_favorite_tracks_fetches_all_by_default(authed_client): + def make_items(start, count): + return [ + {'id': start + i, 'title': f'F{start + i}', 'duration': 200, + 'performer': {'name': 'Fav Artist'}, + 'album': {'title': 'Fav Album', 'image': {}}} + for i in range(count) + ] + + offsets: List[int] = [] + + def responder(endpoint, params=None): + assert endpoint == 'favorite/getUserFavorites' + offsets.append(params['offset']) + start = params['offset'] + remaining = max(0, 625 - start) + return {'tracks': {'items': make_items(start, min(params['limit'], remaining)), 'total': 625}} + + _install_api_responder(authed_client, responder) + tracks = authed_client.get_user_favorite_tracks() + + assert len(tracks) == 625 + assert offsets == [0, 100, 200, 300, 400, 500, 600] + assert tracks[-1]['name'] == 'F624' + + +def test_get_user_favorite_tracks_honors_explicit_limit(authed_client): + def make_items(start, count): + return [ + {'id': start + i, 'title': f'F{start + i}', 'duration': 200, + 'performer': {'name': 'Fav Artist'}, + 'album': {'title': 'Fav Album', 'image': {}}} + for i in range(count) + ] + + requests: List[Dict[str, Any]] = [] + + def responder(endpoint, params=None): + assert endpoint == 'favorite/getUserFavorites' + requests.append(dict(params)) + start = params['offset'] + return {'tracks': {'items': make_items(start, params['limit']), 'total': 625}} + + _install_api_responder(authed_client, responder) + tracks = authed_client.get_user_favorite_tracks(limit=150) + + assert len(tracks) == 150 + assert requests == [ + {'type': 'tracks', 'limit': 100, 'offset': 0}, + {'type': 'tracks', 'limit': 50, 'offset': 100}, + ] + assert tracks[-1]['name'] == 'F149' + + def test_get_user_favorite_tracks_count_uses_cheap_call(authed_client): captured: Dict[str, Any] = {}