diff --git a/tests/mixins/test_playlists.py b/tests/mixins/test_playlists.py index 1482121..3de2985 100644 --- a/tests/mixins/test_playlists.py +++ b/tests/mixins/test_playlists.py @@ -61,7 +61,8 @@ def test_get_playlist_foreign(self, yt_oauth, playlist_id, tracks_len, related_l playlist = yt_oauth.get_playlist(playlist_id, limit=None, related=True) assert len(playlist["duration"]) > 5 assert playlist["trackCount"] > tracks_len - assert len(playlist["tracks"]) > tracks_len + # serialize each track to detect duplicates + assert len(set(json.dumps(track) for track in playlist["tracks"])) > tracks_len assert len(playlist["related"]) == related_len assert "suggestions" not in playlist assert playlist["owned"] is False diff --git a/ytmusicapi/continuations.py b/ytmusicapi/continuations.py index 2049798..0279c4b 100644 --- a/ytmusicapi/continuations.py +++ b/ytmusicapi/continuations.py @@ -1,5 +1,32 @@ +from typing import Any, Optional + from ytmusicapi.navigation import nav +CONTINUATION_TOKEN = ["continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token"] +CONTINUATION_ITEMS = ["onResponseReceivedActions", 0, "appendContinuationItemsAction", "continuationItems"] + + +def get_continuation_token(results: list[dict[str, Any]]) -> Optional[str]: + return nav(results[-1], CONTINUATION_TOKEN, True) + + +def get_continuations_2025(results, limit, request_func, parse_func): + items = [] + continuation_token = get_continuation_token(results["contents"]) + while continuation_token and (limit is None or len(items) < limit): + response = request_func({"continuation": continuation_token}) + continuation_items = nav(response, CONTINUATION_ITEMS, True) + if not continuation_items: + break + + contents = parse_func(continuation_items) + if len(contents) == 0: + break + items.extend(contents) + continuation_token = get_continuation_token(continuation_items) + + return items + def get_continuations( results, continuation_type, limit, request_func, parse_func, ctoken_path="", reloadable=False diff --git a/ytmusicapi/mixins/playlists.py b/ytmusicapi/mixins/playlists.py index 34567ba..1eccfec 100644 --- a/ytmusicapi/mixins/playlists.py +++ b/ytmusicapi/mixins/playlists.py @@ -110,8 +110,9 @@ def get_playlist( request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams) response = request_func("") + request_func_continuations = lambda body: self._send_request(endpoint, body) if playlistId.startswith("OLA") or playlistId.startswith("VLOLA"): - return parse_audio_playlist(response, limit, request_func) + return parse_audio_playlist(response, limit, request_func_continuations) header_data = nav(response, [*TWO_COLUMN_RENDERER, *TAB_CONTENT, *SECTION_LIST_ITEM]) section_list = nav(response, [*TWO_COLUMN_RENDERER, "secondaryContents", *SECTION]) @@ -182,12 +183,9 @@ def get_playlist( playlist["tracks"] = parse_playlist_items(content_data["contents"]) parse_func = lambda contents: parse_playlist_items(contents) - if "continuations" in content_data: - playlist["tracks"].extend( - get_continuations( - content_data, "musicPlaylistShelfContinuation", limit, request_func, parse_func - ) - ) + playlist["tracks"].extend( + get_continuations_2025(content_data, limit, request_func_continuations, parse_func) + ) playlist["duration_seconds"] = sum_total_duration(playlist) return playlist diff --git a/ytmusicapi/parsers/playlists.py b/ytmusicapi/parsers/playlists.py index b4bc35a..b06bba7 100644 --- a/ytmusicapi/parsers/playlists.py +++ b/ytmusicapi/parsers/playlists.py @@ -89,12 +89,7 @@ def parse_audio_playlist(response: dict, limit: Optional[int], request_func) -> playlist["tracks"] = parse_playlist_items(content_data["contents"]) parse_func = lambda contents: parse_playlist_items(contents) - if "continuations" in content_data: - playlist["tracks"].extend( - get_continuations( - content_data, "musicPlaylistShelfContinuation", limit, request_func, parse_func - ) - ) + playlist["tracks"].extend(get_continuations_2025(content_data, limit, request_func, parse_func)) playlist["title"] = playlist["tracks"][0]["album"]["name"]