diff --git a/tests/parsers/data.py b/tests/parsers/data.py new file mode 100644 index 00000000..caf98a77 --- /dev/null +++ b/tests/parsers/data.py @@ -0,0 +1,33 @@ +OWNED_PLAYLIST = { + "thumbnailRenderer": {"musicThumbnailRenderer": {"thumbnail": {"thumbnails": []}}}, + "title": { + "runs": [ + { + "text": "Family playlist", + "navigationEndpoint": {"browseEndpoint": {"browseId": "VLPL_family"}}, + } + ] + }, + "subtitle": { + "runs": [ + { + "text": "Owner", + "navigationEndpoint": {"browseEndpoint": {"browseId": "UC_owner"}}, + }, + {"text": " • "}, + {"text": "1 track"}, + ] + }, + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": {"runs": [{"text": "Edit playlist"}]}, + "navigationEndpoint": {"playlistEditorEndpoint": {"playlistId": "PL_family"}}, + } + } + ] + } + }, +} diff --git a/tests/parsers/test_browsing.py b/tests/parsers/test_browsing.py index db43198b..662829b8 100644 --- a/tests/parsers/test_browsing.py +++ b/tests/parsers/test_browsing.py @@ -1,9 +1,29 @@ +from copy import deepcopy + import pytest +from tests.parsers.data import OWNED_PLAYLIST from ytmusicapi.navigation import MRLIR, MTRIR from ytmusicapi.parsers.browsing import parse_content_list, parse_playlist +def test_parse_playlist_marks_playlist_with_editor_endpoint_as_owned(): + playlist = parse_playlist(OWNED_PLAYLIST) + + assert playlist["owned"] is True + + +def test_parse_playlist_marks_playlist_without_editor_endpoint_as_not_owned(): + saved_playlist = deepcopy(OWNED_PLAYLIST) + saved_playlist["menu"]["menuRenderer"]["items"][0]["menuNavigationItemRenderer"]["navigationEndpoint"] = { + "watchPlaylistEndpoint": {"playlistId": "PL_family"} + } + + playlist = parse_playlist(saved_playlist) + + assert playlist["owned"] is False + + def _playlist_item(thumbnail_renderer: dict) -> dict: return { "title": { diff --git a/ytmusicapi/mixins/library.py b/ytmusicapi/mixins/library.py index b102f8a7..9c837604 100644 --- a/ytmusicapi/mixins/library.py +++ b/ytmusicapi/mixins/library.py @@ -23,15 +23,16 @@ def get_library_playlists(self, limit: int | None = 25) -> JsonList: Retrieves the playlists in the user's library. :param limit: Number of playlists to retrieve. ``None`` retrieves them all. - :return: List of owned playlists. + :return: List of playlists in the user's library. Each item is in the following format:: { 'playlistId': 'PLQwVIlKxHM6rz0fDJVv_0UlXGEWf-bFys', 'title': 'Playlist title', - 'thumbnails: [...], - 'count': 5 + 'thumbnails': [...], + 'count': '5', + 'owned': True } """ self._check_auth() diff --git a/ytmusicapi/parsers/browsing.py b/ytmusicapi/parsers/browsing.py index 2f69f45b..d171eb3d 100644 --- a/ytmusicapi/parsers/browsing.py +++ b/ytmusicapi/parsers/browsing.py @@ -165,14 +165,30 @@ def parse_video(result: JsonDict) -> JsonDict: def parse_playlist(data: JsonDict) -> JsonDict: + playlist_id = nav(data, TITLE + NAVIGATION_BROWSE_ID)[2:] + menu_items = nav(data, ["menu", "menuRenderer", "items"], True) or [] playlist = { "title": nav( data, TITLE_TEXT, none_if_absent=True, # rare but possible for playlist title to be missing ), - "playlistId": nav(data, TITLE + NAVIGATION_BROWSE_ID)[2:], + "playlistId": playlist_id, "thumbnails": nav(data, THUMBNAIL_RENDERER, True), + "owned": any( + nav( + item, + [ + "menuNavigationItemRenderer", + "navigationEndpoint", + "playlistEditorEndpoint", + "playlistId", + ], + True, + ) + == playlist_id + for item in menu_items + ), } subtitle = data["subtitle"] if "runs" in subtitle: