|
1 | 1 | from typing import List, Optional |
2 | 2 |
|
| 3 | +from ..helpers import to_int |
3 | 4 | from .songs import * |
4 | 5 |
|
5 | 6 |
|
| 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 | + |
6 | 49 | def parse_playlist_items(results, menu_entries: Optional[List[List]] = None, is_album=False): |
7 | 50 | songs = [] |
8 | 51 | for result in results: |
9 | 52 | if MRLIR not in result: |
10 | 53 | continue |
11 | 54 | data = result[MRLIR] |
| 55 | + song = parse_playlist_item(data, menu_entries, is_album) |
| 56 | + if song: |
| 57 | + songs.append(song) |
12 | 58 |
|
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 |
108 | 60 |
|
109 | | - songs.append(song) |
110 | 61 |
|
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 |
112 | 156 |
|
113 | 157 |
|
114 | 158 | def validate_playlist_id(playlistId: str) -> str: |
|
0 commit comments