From f2be19416bccb762ba27d1c72d4fb7a6f0ada664 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Wed, 10 Jan 2024 19:10:49 +0100 Subject: [PATCH 1/6] get_artist_albums: support continuations (#441) --- tests/mixins/test_browsing.py | 11 ++++++----- ytmusicapi/mixins/browsing.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/mixins/test_browsing.py b/tests/mixins/test_browsing.py index f626d50..73aabfc 100644 --- a/tests/mixins/test_browsing.py +++ b/tests/mixins/test_browsing.py @@ -26,12 +26,13 @@ def test_get_artist(self, yt): def test_get_artist_albums(self, yt): artist = yt.get_artist("UCj5ZiBBqpe0Tg4zfKGHEFuQ") results = yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"]) - assert len(results) > 0 - - def test_get_artist_singles(self, yt): - artist = yt.get_artist("UCAeLFBCQS7FvI8PvBrWvSBg") + assert len(results) == 100 results = yt.get_artist_albums(artist["singles"]["browseId"], artist["singles"]["params"]) - assert len(results) > 0 + assert len(results) < 100 + + artist = yt.get_artist("UC6LfFqHnWV8iF94n54jwYGw") + results = yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"], limit=None) + assert len(results) >= 300 def test_get_user(self, yt): results = yt.get_user("UC44hbeRoCZVVMVg5z0FfIww") diff --git a/ytmusicapi/mixins/browsing.py b/ytmusicapi/mixins/browsing.py index a013e45..6035d90 100644 --- a/ytmusicapi/mixins/browsing.py +++ b/ytmusicapi/mixins/browsing.py @@ -252,12 +252,13 @@ def get_artist(self, channelId: str) -> Dict: artist.update(self.parser.parse_artist_contents(results)) return artist - def get_artist_albums(self, channelId: str, params: str) -> List[Dict]: + def get_artist_albums(self, channelId: str, params: str, limit: int | None = 100) -> List[Dict]: """ Get the full list of an artist's albums or singles :param channelId: browseId of the artist as returned by :py:func:`get_artist` :param params: params obtained by :py:func:`get_artist` + :param limit: Number of albums to return. `None` retrieves them all. Default: 100 :return: List of albums in the format of :py:func:`get_library_albums`, except artists key is missing. @@ -266,8 +267,17 @@ def get_artist_albums(self, channelId: str, params: str) -> List[Dict]: endpoint = "browse" response = self._send_request(endpoint, body) results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM) - results = nav(results, GRID_ITEMS, True) or nav(results, CAROUSEL_CONTENTS) - albums = parse_albums(results) + contents = nav(results, GRID_ITEMS, True) or nav(results, CAROUSEL_CONTENTS) + albums = parse_albums(contents) + + results = nav(results, GRID, True) + if "continuations" in results: + request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams) + parse_func = lambda contents: parse_albums(contents) + remaining_limit = None if limit is None else (limit - len(albums)) + albums.extend( + get_continuations(results, "gridContinuation", remaining_limit, request_func, parse_func) + ) return albums From e864a6be5368dc9416e609d1c054a12dae8de3b7 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Wed, 10 Jan 2024 21:05:54 +0100 Subject: [PATCH 2/6] get_artist_albums: add order param (#441) --- tests/mixins/test_browsing.py | 17 ++++++--- ytmusicapi/mixins/browsing.py | 65 ++++++++++++++++++++++++++++++++--- ytmusicapi/navigation.py | 7 ++-- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/tests/mixins/test_browsing.py b/tests/mixins/test_browsing.py index 73aabfc..6545a82 100644 --- a/tests/mixins/test_browsing.py +++ b/tests/mixins/test_browsing.py @@ -24,15 +24,22 @@ def test_get_artist(self, yt): assert len(results) >= 11 def test_get_artist_albums(self, yt): - artist = yt.get_artist("UCj5ZiBBqpe0Tg4zfKGHEFuQ") + artist = yt.get_artist("UCAeLFBCQS7FvI8PvBrWvSBg") results = yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"]) assert len(results) == 100 results = yt.get_artist_albums(artist["singles"]["browseId"], artist["singles"]["params"]) - assert len(results) < 100 + assert len(results) == 100 + + results_unsorted = yt.get_artist_albums( + artist["albums"]["browseId"], artist["albums"]["params"], limit=None + ) + assert len(results_unsorted) >= 300 - artist = yt.get_artist("UC6LfFqHnWV8iF94n54jwYGw") - results = yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"], limit=None) - assert len(results) >= 300 + results_sorted = yt.get_artist_albums( + artist["albums"]["browseId"], artist["albums"]["params"], limit=None, order="alphabetical order" + ) + assert len(results_sorted) >= 300 + assert results_sorted != results_unsorted def test_get_user(self, yt): results = yt.get_user("UC44hbeRoCZVVMVg5z0FfIww") diff --git a/ytmusicapi/mixins/browsing.py b/ytmusicapi/mixins/browsing.py index 6035d90..3c9a084 100644 --- a/ytmusicapi/mixins/browsing.py +++ b/ytmusicapi/mixins/browsing.py @@ -1,7 +1,10 @@ import re from typing import Any, Dict, List, Optional -from ytmusicapi.continuations import get_continuations +from ytmusicapi.continuations import ( + get_continuations, + get_reloadable_continuation_params, +) from ytmusicapi.helpers import YTM_DOMAIN, sum_total_duration from ytmusicapi.parsers.albums import parse_album_header from ytmusicapi.parsers.browsing import parse_album, parse_content_list, parse_mixed_content, parse_playlist @@ -252,13 +255,16 @@ def get_artist(self, channelId: str) -> Dict: artist.update(self.parser.parse_artist_contents(results)) return artist - def get_artist_albums(self, channelId: str, params: str, limit: int | None = 100) -> List[Dict]: + def get_artist_albums( + self, channelId: str, params: str, limit: Optional[int] = 100, order: Optional[str] = None + ) -> List[Dict]: """ Get the full list of an artist's albums or singles :param channelId: browseId of the artist as returned by :py:func:`get_artist` :param params: params obtained by :py:func:`get_artist` :param limit: Number of albums to return. `None` retrieves them all. Default: 100 + :param order: Order of albums to return. Allowed values: 'Recency', 'Popularity', 'Alphabetical order'. Default: Default order. :return: List of albums in the format of :py:func:`get_library_albums`, except artists key is missing. @@ -266,14 +272,63 @@ def get_artist_albums(self, channelId: str, params: str, limit: int | None = 100 body = {"browseId": channelId, "params": params} endpoint = "browse" response = self._send_request(endpoint, body) - results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM) + + request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams) + parse_func = lambda contents: parse_albums(contents) + + if order: + # pick the correct continuation from response depending on the order chosen + sort_options = nav( + response, + SINGLE_COLUMN_TAB + + SECTION + + HEADER_SIDE + + [ + "endItems", + 0, + "musicSortFilterButtonRenderer", + "menu", + "musicMultiSelectMenuRenderer", + "options", + ], + ) + continuation = next( + ( + nav( + option, + MULTI_SELECT + + [ + "selectedCommand", + "commandExecutorCommand", + "commands", + -1, + "browseSectionListReloadEndpoint", + ], + ) + for option in sort_options + if nav(option, MULTI_SELECT + TITLE_TEXT).lower() == order.lower() + ), + None, + ) + # if a valid order was provided, request continuation and replace original response + if continuation: + additionalParams = get_reloadable_continuation_params( + {"continuations": [continuation["continuation"]]} + ) + response = request_func(additionalParams) + results = nav(response, SECTION_LIST_CONTINUATION + CONTENT) + else: + raise ValueError(f"Invalid order parameter {order}") + + else: + # just use the results from the first request + results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM) + contents = nav(results, GRID_ITEMS, True) or nav(results, CAROUSEL_CONTENTS) albums = parse_albums(contents) results = nav(results, GRID, True) if "continuations" in results: - request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams) - parse_func = lambda contents: parse_albums(contents) remaining_limit = None if limit is None else (limit - len(albums)) albums.extend( get_continuations(results, "gridContinuation", remaining_limit, request_func, parse_func) diff --git a/ytmusicapi/navigation.py b/ytmusicapi/navigation.py index 616add1..955d2ee 100644 --- a/ytmusicapi/navigation.py +++ b/ytmusicapi/navigation.py @@ -7,8 +7,9 @@ TAB_1_CONTENT = ["tabs", 1, "tabRenderer", "content"] SINGLE_COLUMN = ["contents", "singleColumnBrowseResultsRenderer"] SINGLE_COLUMN_TAB = SINGLE_COLUMN + TAB_CONTENT -SECTION_LIST = ["sectionListRenderer", "contents"] -SECTION_LIST_ITEM = ["sectionListRenderer"] + CONTENT +SECTION = ["sectionListRenderer"] +SECTION_LIST = SECTION + ["contents"] +SECTION_LIST_ITEM = SECTION + CONTENT ITEM_SECTION = ["itemSectionRenderer"] + CONTENT MUSIC_SHELF = ["musicShelfRenderer"] GRID = ["gridRenderer"] @@ -58,7 +59,9 @@ TASTE_PROFILE_ARTIST = ["title", "runs"] SECTION_LIST_CONTINUATION = ["continuationContents", "sectionListContinuation"] MENU_PLAYLIST_ID = MENU_ITEMS + [0, "menuNavigationItemRenderer"] + NAVIGATION_WATCH_PLAYLIST_ID +MULTI_SELECT = ["musicMultiSelectMenuItemRenderer"] HEADER_DETAIL = ["header", "musicDetailHeaderRenderer"] +HEADER_SIDE = ["header", "musicSideAlignedItemRenderer"] DESCRIPTION_SHELF = ["musicDescriptionShelfRenderer"] DESCRIPTION = ["description"] + RUN_TEXT CAROUSEL = ["musicCarouselShelfRenderer"] From b094345e447f77af4b20cb2489cd3f77adf9e1bf Mon Sep 17 00:00:00 2001 From: sigma67 Date: Wed, 10 Jan 2024 21:22:21 +0100 Subject: [PATCH 3/6] add test for order exception --- tests/mixins/test_browsing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/mixins/test_browsing.py b/tests/mixins/test_browsing.py index 6545a82..3f4fa62 100644 --- a/tests/mixins/test_browsing.py +++ b/tests/mixins/test_browsing.py @@ -41,6 +41,9 @@ def test_get_artist_albums(self, yt): assert len(results_sorted) >= 300 assert results_sorted != results_unsorted + with pytest.raises(ValueError, match="Invalid order"): + yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"], order="order") + def test_get_user(self, yt): results = yt.get_user("UC44hbeRoCZVVMVg5z0FfIww") assert len(results) == 3 From 590b74c29f05ac6c76e1dff12bb4e3a685b3e6a8 Mon Sep 17 00:00:00 2001 From: Yash Malik <37410163+codeblech@users.noreply.github.com> Date: Thu, 11 Jan 2024 01:39:15 +0530 Subject: [PATCH 4/6] hyperlink to "tests" doesn't actually link to tests in README Updated the hyperlink to "tests" in README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ab6019e..2c09c88 100644 --- a/README.rst +++ b/README.rst @@ -85,7 +85,7 @@ Usage search_results = yt.search('Oasis Wonderwall') yt.add_playlist_items(playlistId, [search_results[0]['videoId']]) -The `tests `_ are also a great source of usage examples. +The `tests `_ are also a great source of usage examples. .. end-features From 87b7ad62013ae3860100804c86d91b2df01b76d2 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Wed, 10 Jan 2024 21:30:14 +0100 Subject: [PATCH 5/6] make workflows conditional on changeset --- .github/workflows/coverage.yml | 3 +++ .github/workflows/docsbuild.yml | 7 +++++-- .github/workflows/lint.yml | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cef9522..fdfe5cc 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,6 +4,9 @@ on: push: branches: - main + paths: + - ytmusicapi + - tests pull_request: branches: - main diff --git a/.github/workflows/docsbuild.yml b/.github/workflows/docsbuild.yml index fe3ddb9..4bc7c94 100644 --- a/.github/workflows/docsbuild.yml +++ b/.github/workflows/docsbuild.yml @@ -1,7 +1,10 @@ name: Build Documentation on: - push + push: + paths: + - ytmusicapi + - docs jobs: build: @@ -19,4 +22,4 @@ jobs: - name: Build documentation run: | cd docs - make html \ No newline at end of file + make html diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9beb7ab..688398a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - main + paths: + - ytmusicapi + - tests jobs: ruff: From b60bcd7675671042127beadd971a6b104f506396 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Thu, 11 Jan 2024 18:00:57 +0100 Subject: [PATCH 6/6] add glob to workflow paths --- .github/workflows/coverage.yml | 4 ++-- .github/workflows/docsbuild.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fdfe5cc..f6990e3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -5,8 +5,8 @@ on: branches: - main paths: - - ytmusicapi - - tests + - ytmusicapi/** + - tests/** pull_request: branches: - main diff --git a/.github/workflows/docsbuild.yml b/.github/workflows/docsbuild.yml index 4bc7c94..a3ec932 100644 --- a/.github/workflows/docsbuild.yml +++ b/.github/workflows/docsbuild.yml @@ -3,8 +3,8 @@ name: Build Documentation on: push: paths: - - ytmusicapi - - docs + - ytmusicapi/** + - docs/** jobs: build: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 688398a..fc83090 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,8 +5,8 @@ on: branches: - main paths: - - ytmusicapi - - tests + - ytmusicapi/** + - tests/** jobs: ruff: