diff --git a/tests/test_user.py b/tests/test_user.py index 3733d3c..c38a3da 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -65,6 +65,14 @@ def test_get_user_playlists(session): assert playlist_ids | favourite_ids == both_ids +def test_get_user_playlists_paginated(session): + expected_count = session.user.favorites.get_playlists_count() + all_playlists = session.user.favorites.playlists_paginated() + assert len(all_playlists) == expected_count + unique_ids = set(x.id for x in all_playlists) + assert len(unique_ids) == expected_count + + def test_get_playlist_folders(session): folder = session.user.create_folder(title="testfolder") assert folder diff --git a/tidalapi/user.py b/tidalapi/user.py index 1e534e8..2a07335 100644 --- a/tidalapi/user.py +++ b/tidalapi/user.py @@ -671,16 +671,31 @@ def playlists_paginated( order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, ) -> List["Playlist"]: - """Get the users favorite playlists, using pagination. + """Get the users favorite playlists, using cursor-based pagination. + + The v2 my-collection/playlists/folders endpoint uses cursor-based + pagination. Each response includes a ``cursor`` field that must be + passed to the next request to retrieve the following page. :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" :return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite playlists. """ - count = self.session.user.favorites.get_playlists_count() - return get_items( - self.session.user.favorites.playlists, count, order, order_direction - ) + playlists: List["Playlist"] = [] + cursor: Optional[str] = None + + while True: + items = self.playlists( + cursor=cursor, + order=order, + order_direction=order_direction, + ) + playlists.extend(items) + cursor = self._last_playlists_cursor + if not cursor or not items: + break + + return playlists def playlists( self, @@ -688,14 +703,18 @@ def playlists( offset: int = 0, order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, + cursor: Optional[str] = None, ) -> List["Playlist"]: - """Get the users favorite playlists (v2 endpoint), relative to the root folder + """Get the users favorite playlists (v2 endpoint), relative to the root folder. This function is limited to 50 by TIDAL, requiring pagination. :param limit: The number of playlists you want returned (Note: Cannot exceed 50) - :param offset: The index of the first playlist to fetch + :param offset: The index of the first playlist to fetch. Note: this parameter is + ignored by the TIDAL API for this endpoint. Use ``cursor`` for pagination. :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" + :param cursor: Cursor for fetching the next page of results. Obtained from + :attr:`_last_playlists_cursor` after a previous call to this method. :return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite playlists. """ params = { @@ -704,6 +723,8 @@ def playlists( "limit": limit, "includeOnly": "PLAYLIST", # Include only PLAYLIST types, FOLDER will be ignored } + if cursor: + params["cursor"] = cursor if order: params["order"] = order.value else: @@ -714,17 +735,13 @@ def playlists( params["orderDirection"] = OrderDirection.Descending.value endpoint = "my-collection/playlists/folders" - return cast( - List["Playlist"], - self.session.request.map_request( - url=urljoin( - self.session.config.api_v2_location, - endpoint, - ), - params=params, - parse=self.session.parse_playlist, - ), + url = urljoin(self.session.config.api_v2_location, endpoint) + json_obj = self.session.request.request("GET", url, params).json() + items = self.session.request.map_json( + json_obj, parse=self.session.parse_playlist ) + self._last_playlists_cursor = json_obj.get("cursor") + return cast(List["Playlist"], items) def playlist_folders( self, @@ -733,14 +750,19 @@ def playlist_folders( order: Optional[PlaylistOrder] = None, order_direction: Optional[OrderDirection] = None, parent_folder_id: str = "root", + cursor: Optional[str] = None, ) -> List["Folder"]: """Get a list of folders created by the user. :param limit: The number of playlists you want returned (Note: Cannot exceed 50) - :param offset: The index of the first playlist folder to fetch + :param offset: The index of the first playlist folder to fetch. Note: this + parameter is ignored by the TIDAL API for this endpoint. Use ``cursor`` + for pagination. :param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE" :param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC" :param parent_folder_id: Parent folder ID. Default: 'root' playlist folder + :param cursor: Cursor for fetching the next page of results. Obtained from + :attr:`_last_folders_cursor` after a previous call to this method. :return: Returns a list of :class:`~tidalapi.playlist.Folder` objects containing the Folders. """ params = { @@ -750,23 +772,19 @@ def playlist_folders( "order": "NAME", "includeOnly": "FOLDER", } + if cursor: + params["cursor"] = cursor if order: params["order"] = order.value if order_direction: params["orderDirection"] = order_direction.value endpoint = "my-collection/playlists/folders" - return cast( - List["Folder"], - self.session.request.map_request( - url=urljoin( - self.session.config.api_v2_location, - endpoint, - ), - params=params, - parse=self.session.parse_folder, - ), - ) + url = urljoin(self.session.config.api_v2_location, endpoint) + json_obj = self.session.request.request("GET", url, params).json() + items = self.session.request.map_json(json_obj, parse=self.session.parse_folder) + self._last_folders_cursor = json_obj.get("cursor") + return cast(List["Folder"], items) def get_playlists_count(self) -> int: """Get the total number of playlists in the user's root collection.