Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into jcbirdwell-track_posi…
Browse files Browse the repository at this point in the history
…tions
  • Loading branch information
sigma67 committed Jan 11, 2024
2 parents 0aff162 + b60bcd7 commit ec82396
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches:
- main
paths:
- ytmusicapi/**
- tests/**
pull_request:
branches:
- main
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/docsbuild.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
name: Build Documentation

on:
push
push:
paths:
- ytmusicapi/**
- docs/**

jobs:
build:
Expand All @@ -19,4 +22,4 @@ jobs:
- name: Build documentation
run: |
cd docs
make html
make html
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
pull_request:
branches:
- main
paths:
- ytmusicapi/**
- tests/**

jobs:
ruff:
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Usage
search_results = yt.search('Oasis Wonderwall')
yt.add_playlist_items(playlistId, [search_results[0]['videoId']])
The `tests <https://github.com/sigma67/ytmusicapi/blob/master/tests/test.py>`_ are also a great source of usage examples.
The `tests <https://github.com/sigma67/ytmusicapi/blob/master/tests/>`_ are also a great source of usage examples.

.. end-features
Expand Down
23 changes: 17 additions & 6 deletions tests/mixins/test_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,25 @@ def test_get_artist(self, yt):
assert len(results) >= 11

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")
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) > 0
assert len(results) == 100

results_unsorted = yt.get_artist_albums(
artist["albums"]["browseId"], artist["albums"]["params"], limit=None
)
assert len(results_unsorted) >= 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

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")
Expand Down
75 changes: 70 additions & 5 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -252,22 +255,84 @@ 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: 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.
"""
body = {"browseId": channelId, "params": params}
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)

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:
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

Expand Down
7 changes: 5 additions & 2 deletions ytmusicapi/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down

0 comments on commit ec82396

Please sign in to comment.