Skip to content

Commit f6932c2

Browse files
authored
get_episodes_playlist (sigma67#561)
* get_episodes_playlist * fix minor issues
1 parent 90d172b commit f6932c2

File tree

7 files changed

+174
-137
lines changed

7 files changed

+174
-137
lines changed

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Features
6363
* get podcasts
6464
* get episodes
6565
* get channels
66+
* get episodes playlists
6667

6768
| **Uploads**:
6869

docs/source/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Podcasts
8181
.. automethod:: YTMusic.get_channel_episodes
8282
.. automethod:: YTMusic.get_podcast
8383
.. automethod:: YTMusic.get_episode
84+
.. automethod:: YTMusic.get_episodes_playlist
8485

8586
Uploads
8687
-------

tests/mixins/test_podcasts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ def test_many_episodes(self, yt):
4343
for result in results:
4444
result = yt.get_episode(result["videoId"])
4545
assert len(result["description"].text) > 0
46+
47+
def test_get_episodes_playlist(self, yt_brand):
48+
playlist = yt_brand.get_episodes_playlist()
49+
assert len(playlist["episodes"]) > 90

ytmusicapi/mixins/playlists.py

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Dict, List, Optional, Tuple, Union
22

33
from ytmusicapi.continuations import *
4-
from ytmusicapi.helpers import sum_total_duration, to_int
4+
from ytmusicapi.helpers import sum_total_duration
55
from ytmusicapi.navigation import *
66
from ytmusicapi.parsers.browsing import parse_content_list, parse_playlist
77
from ytmusicapi.parsers.playlists import *
@@ -108,44 +108,9 @@ def get_playlist(
108108
response = self._send_request(endpoint, body)
109109
results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM + ["musicPlaylistShelfRenderer"])
110110
playlist = {"id": results["playlistId"]}
111-
own_playlist = "musicEditablePlaylistDetailHeaderRenderer" in response["header"]
112-
if not own_playlist:
113-
header = response["header"]["musicDetailHeaderRenderer"]
114-
playlist["privacy"] = "PUBLIC"
115-
else:
116-
header = response["header"]["musicEditablePlaylistDetailHeaderRenderer"]
117-
playlist["privacy"] = header["editHeader"]["musicPlaylistEditHeaderRenderer"]["privacy"]
118-
header = header["header"]["musicDetailHeaderRenderer"]
119-
playlist["owned"] = own_playlist
120-
121-
playlist["title"] = nav(header, TITLE_TEXT)
122-
playlist["thumbnails"] = nav(header, THUMBNAIL_CROPPED)
123-
playlist["description"] = nav(header, DESCRIPTION, True)
124-
run_count = len(nav(header, SUBTITLE_RUNS))
125-
if run_count > 1:
126-
playlist["author"] = {
127-
"name": nav(header, SUBTITLE2),
128-
"id": nav(header, [*SUBTITLE_RUNS, 2, *NAVIGATION_BROWSE_ID], True),
129-
}
130-
if run_count == 5:
131-
playlist["year"] = nav(header, SUBTITLE3)
132-
133-
playlist["views"] = None
134-
playlist["duration"] = None
135-
if "runs" in header["secondSubtitle"]:
136-
second_subtitle_runs = header["secondSubtitle"]["runs"]
137-
has_views = (len(second_subtitle_runs) > 3) * 2
138-
playlist["views"] = None if not has_views else to_int(second_subtitle_runs[0]["text"])
139-
has_duration = (len(second_subtitle_runs) > 1) * 2
140-
playlist["duration"] = (
141-
None if not has_duration else second_subtitle_runs[has_views + has_duration]["text"]
142-
)
143-
song_count = second_subtitle_runs[has_views + 0]["text"].split(" ")
144-
song_count = to_int(song_count[0]) if len(song_count) > 1 else 0
145-
else:
146-
song_count = len(results["contents"])
147-
148-
playlist["trackCount"] = song_count
111+
playlist.update(parse_playlist_header(response))
112+
if playlist["trackCount"] is None:
113+
playlist["trackCount"] = len(results["contents"])
149114

150115
request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
151116

@@ -154,6 +119,7 @@ def get_playlist(
154119
playlist["related"] = []
155120
if "continuations" in section_list:
156121
additionalParams = get_continuation_params(section_list)
122+
own_playlist = "musicEditablePlaylistDetailHeaderRenderer" in response["header"]
157123
if own_playlist and (suggestions_limit > 0 or related):
158124
parse_func = lambda results: parse_playlist_items(results)
159125
suggested = request_func(additionalParams)

ytmusicapi/mixins/podcasts.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ytmusicapi.mixins._protocol import MixinProtocol
55
from ytmusicapi.navigation import *
66
from ytmusicapi.parsers.browsing import parse_content_list
7+
from ytmusicapi.parsers.playlists import parse_playlist_header
78
from ytmusicapi.parsers.podcasts import *
89

910
from ._utils import *
@@ -228,3 +229,23 @@ def get_episode(self, videoId: str) -> Dict:
228229
episode["description"] = Description.from_runs(description_runs)
229230

230231
return episode
232+
233+
def get_episodes_playlist(self, playlist_id: str = "RDPN") -> Dict:
234+
"""
235+
Get all episodes in an episodes playlist. Currently the only known playlist is the
236+
"New Episodes" auto-generated playlist
237+
238+
:param playlist_id: Playlist ID, defaults to "RDPN", the id of the New Episodes playlist
239+
:return: Dictionary in the format of :py:func:`get_podcast`
240+
"""
241+
browseId = "VL" + playlist_id if not playlist_id.startswith("VL") else playlist_id
242+
body = {"browseId": browseId}
243+
endpoint = "browse"
244+
response = self._send_request(endpoint, body)
245+
playlist = parse_playlist_header(response)
246+
247+
results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM + MUSIC_SHELF)
248+
parse_func = lambda contents: parse_content_list(contents, parse_episode, MMRIR)
249+
playlist["episodes"] = parse_func(results["contents"])
250+
251+
return playlist

ytmusicapi/parsers/playlists.py

Lines changed: 141 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,158 @@
11
from typing import List, Optional
22

3+
from ..helpers import to_int
34
from .songs import *
45

56

7+
def parse_playlist_header(response: Dict) -> Dict[str, Any]:
8+
playlist: Dict[str, Any] = {}
9+
own_playlist = "musicEditablePlaylistDetailHeaderRenderer" in response["header"]
10+
if not own_playlist:
11+
header = response["header"]["musicDetailHeaderRenderer"]
12+
playlist["privacy"] = "PUBLIC"
13+
else:
14+
header = response["header"]["musicEditablePlaylistDetailHeaderRenderer"]
15+
playlist["privacy"] = header["editHeader"]["musicPlaylistEditHeaderRenderer"]["privacy"]
16+
header = header["header"]["musicDetailHeaderRenderer"]
17+
playlist["owned"] = own_playlist
18+
19+
playlist["title"] = nav(header, TITLE_TEXT)
20+
playlist["thumbnails"] = nav(header, THUMBNAIL_CROPPED)
21+
playlist["description"] = nav(header, DESCRIPTION, True)
22+
run_count = len(nav(header, SUBTITLE_RUNS))
23+
if run_count > 1:
24+
playlist["author"] = {
25+
"name": nav(header, SUBTITLE2),
26+
"id": nav(header, [*SUBTITLE_RUNS, 2, *NAVIGATION_BROWSE_ID], True),
27+
}
28+
if run_count == 5:
29+
playlist["year"] = nav(header, SUBTITLE3)
30+
31+
playlist["views"] = None
32+
playlist["duration"] = None
33+
playlist["trackCount"] = None
34+
if "runs" in header["secondSubtitle"]:
35+
second_subtitle_runs = header["secondSubtitle"]["runs"]
36+
has_views = (len(second_subtitle_runs) > 3) * 2
37+
playlist["views"] = None if not has_views else to_int(second_subtitle_runs[0]["text"])
38+
has_duration = (len(second_subtitle_runs) > 1) * 2
39+
playlist["duration"] = (
40+
None if not has_duration else second_subtitle_runs[has_views + has_duration]["text"]
41+
)
42+
song_count = second_subtitle_runs[has_views + 0]["text"].split(" ")
43+
song_count = to_int(song_count[0]) if len(song_count) > 1 else 0
44+
playlist["trackCount"] = song_count
45+
46+
return playlist
47+
48+
649
def parse_playlist_items(results, menu_entries: Optional[List[List]] = None, is_album=False):
750
songs = []
851
for result in results:
952
if MRLIR not in result:
1053
continue
1154
data = result[MRLIR]
55+
song = parse_playlist_item(data, menu_entries, is_album)
56+
if song:
57+
songs.append(song)
1258

13-
videoId = setVideoId = None
14-
like = None
15-
feedback_tokens = None
16-
library_status = None
17-
18-
# if the item has a menu, find its setVideoId
19-
if "menu" in data:
20-
for item in nav(data, MENU_ITEMS):
21-
if "menuServiceItemRenderer" in item:
22-
menu_service = nav(item, MENU_SERVICE)
23-
if "playlistEditEndpoint" in menu_service:
24-
setVideoId = nav(
25-
menu_service, ["playlistEditEndpoint", "actions", 0, "setVideoId"], True
26-
)
27-
videoId = nav(
28-
menu_service, ["playlistEditEndpoint", "actions", 0, "removedVideoId"], True
29-
)
30-
31-
if TOGGLE_MENU in item:
32-
feedback_tokens = parse_song_menu_tokens(item)
33-
library_status = parse_song_library_status(item)
34-
35-
# if item is not playable, the videoId was retrieved above
36-
if nav(data, PLAY_BUTTON, none_if_absent=True) is not None:
37-
if "playNavigationEndpoint" in nav(data, PLAY_BUTTON):
38-
videoId = nav(data, PLAY_BUTTON)["playNavigationEndpoint"]["watchEndpoint"]["videoId"]
39-
40-
if "menu" in data:
41-
like = nav(data, MENU_LIKE_STATUS, True)
42-
43-
title = get_item_text(data, 0)
44-
if title == "Song deleted":
45-
continue
46-
47-
flex_column_count = len(data["flexColumns"])
48-
49-
artists = parse_song_artists(data, 1)
50-
51-
album = parse_song_album(data, flex_column_count - 1) if not is_album else None
52-
53-
views = get_item_text(data, 2) if flex_column_count == 4 or is_album else None
54-
55-
duration = None
56-
if "fixedColumns" in data:
57-
if "simpleText" in get_fixed_column_item(data, 0)["text"]:
58-
duration = get_fixed_column_item(data, 0)["text"]["simpleText"]
59-
else:
60-
duration = get_fixed_column_item(data, 0)["text"]["runs"][0]["text"]
61-
62-
thumbnails = None
63-
if "thumbnail" in data:
64-
thumbnails = nav(data, THUMBNAILS)
65-
66-
isAvailable = True
67-
if "musicItemRendererDisplayPolicy" in data:
68-
isAvailable = (
69-
data["musicItemRendererDisplayPolicy"] != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT"
70-
)
71-
72-
isExplicit = nav(data, BADGE_LABEL, True) is not None
73-
74-
videoType = nav(
75-
data,
76-
[*MENU_ITEMS, 0, MNIR, "navigationEndpoint", *NAVIGATION_VIDEO_TYPE],
77-
True,
78-
)
79-
80-
song = {
81-
"videoId": videoId,
82-
"title": title,
83-
"artists": artists,
84-
"album": album,
85-
"likeStatus": like,
86-
"inLibrary": library_status,
87-
"thumbnails": thumbnails,
88-
"isAvailable": isAvailable,
89-
"isExplicit": isExplicit,
90-
"videoType": videoType,
91-
"views": views,
92-
}
93-
94-
if is_album:
95-
song["trackNumber"] = int(nav(data, ["index", "runs", 0, "text"])) if isAvailable else None
96-
97-
if duration:
98-
song["duration"] = duration
99-
song["duration_seconds"] = parse_duration(duration)
100-
if setVideoId:
101-
song["setVideoId"] = setVideoId
102-
if feedback_tokens:
103-
song["feedbackTokens"] = feedback_tokens
104-
105-
if menu_entries:
106-
for menu_entry in menu_entries:
107-
song[menu_entry[-1]] = nav(data, MENU_ITEMS + menu_entry)
59+
return songs
10860

109-
songs.append(song)
11061

111-
return songs
62+
def parse_playlist_item(
63+
data: Dict, menu_entries: Optional[List[List]] = None, is_album=False
64+
) -> Optional[Dict]:
65+
videoId = setVideoId = None
66+
like = None
67+
feedback_tokens = None
68+
library_status = None
69+
70+
# if the item has a menu, find its setVideoId
71+
if "menu" in data:
72+
for item in nav(data, MENU_ITEMS):
73+
if "menuServiceItemRenderer" in item:
74+
menu_service = nav(item, MENU_SERVICE)
75+
if "playlistEditEndpoint" in menu_service:
76+
setVideoId = nav(menu_service, ["playlistEditEndpoint", "actions", 0, "setVideoId"], True)
77+
videoId = nav(
78+
menu_service, ["playlistEditEndpoint", "actions", 0, "removedVideoId"], True
79+
)
80+
81+
if TOGGLE_MENU in item:
82+
feedback_tokens = parse_song_menu_tokens(item)
83+
library_status = parse_song_library_status(item)
84+
85+
# if item is not playable, the videoId was retrieved above
86+
if nav(data, PLAY_BUTTON, none_if_absent=True) is not None:
87+
if "playNavigationEndpoint" in nav(data, PLAY_BUTTON):
88+
videoId = nav(data, PLAY_BUTTON)["playNavigationEndpoint"]["watchEndpoint"]["videoId"]
89+
90+
if "menu" in data:
91+
like = nav(data, MENU_LIKE_STATUS, True)
92+
93+
title = get_item_text(data, 0)
94+
if title == "Song deleted":
95+
return None
96+
97+
flex_column_count = len(data["flexColumns"])
98+
99+
artists = parse_song_artists(data, 1)
100+
101+
album = parse_song_album(data, flex_column_count - 1) if not is_album else None
102+
103+
views = get_item_text(data, 2) if flex_column_count == 4 or is_album else None
104+
105+
duration = None
106+
if "fixedColumns" in data:
107+
if "simpleText" in get_fixed_column_item(data, 0)["text"]:
108+
duration = get_fixed_column_item(data, 0)["text"]["simpleText"]
109+
else:
110+
duration = get_fixed_column_item(data, 0)["text"]["runs"][0]["text"]
111+
112+
thumbnails = nav(data, THUMBNAILS, True)
113+
114+
isAvailable = True
115+
if "musicItemRendererDisplayPolicy" in data:
116+
isAvailable = data["musicItemRendererDisplayPolicy"] != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT"
117+
118+
isExplicit = nav(data, BADGE_LABEL, True) is not None
119+
120+
videoType = nav(
121+
data,
122+
[*MENU_ITEMS, 0, MNIR, "navigationEndpoint", *NAVIGATION_VIDEO_TYPE],
123+
True,
124+
)
125+
126+
song = {
127+
"videoId": videoId,
128+
"title": title,
129+
"artists": artists,
130+
"album": album,
131+
"likeStatus": like,
132+
"inLibrary": library_status,
133+
"thumbnails": thumbnails,
134+
"isAvailable": isAvailable,
135+
"isExplicit": isExplicit,
136+
"videoType": videoType,
137+
"views": views,
138+
}
139+
140+
if is_album:
141+
song["trackNumber"] = int(nav(data, ["index", "runs", 0, "text"])) if isAvailable else None
142+
143+
if duration:
144+
song["duration"] = duration
145+
song["duration_seconds"] = parse_duration(duration)
146+
if setVideoId:
147+
song["setVideoId"] = setVideoId
148+
if feedback_tokens:
149+
song["feedbackTokens"] = feedback_tokens
150+
151+
if menu_entries:
152+
for menu_entry in menu_entries:
153+
song[menu_entry[-1]] = nav(data, MENU_ITEMS + menu_entry)
154+
155+
return song
112156

113157

114158
def validate_playlist_id(playlistId: str) -> str:

ytmusicapi/parsers/podcasts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def parse_episode(data):
111111
videoId = nav(data, ["onTap", *WATCH_VIDEO_ID], True)
112112
browseId = nav(data, [*TITLE, *NAVIGATION_BROWSE_ID], True)
113113
videoType = nav(data, ["onTap", *NAVIGATION_VIDEO_TYPE], True)
114-
index = nav(data, ["onTap", "watchEndpoint", "index"])
114+
index = nav(data, ["onTap", "watchEndpoint", "index"], True)
115115
return {
116116
"index": index,
117117
"title": title,

0 commit comments

Comments
 (0)