1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
from typing import Optional, Union
from ytmusicapi.continuations import get_continuations
from ytmusicapi.exceptions import YTMusicServerError, YTMusicUserError
from ytmusicapi.mixins._protocol import MixinProtocol
from ytmusicapi.parsers.playlists import validate_playlist_id
from ytmusicapi.parsers.watch import *
class WatchMixin(MixinProtocol):
def get_watch_playlist(
self,
videoId: Optional[str] = None,
playlistId: Optional[str] = None,
limit=25,
radio: bool = False,
shuffle: bool = False,
) -> dict[str, Union[list[dict], str, None]]:
"""
Get a watch list of tracks. This watch playlist appears when you press
play on a track in YouTube Music.
Please note that the ``INDIFFERENT`` likeStatus of tracks returned by this
endpoint may be either ``INDIFFERENT`` or ``DISLIKE``, due to ambiguous data
returned by YouTube Music.
:param videoId: videoId of the played video
:param playlistId: playlistId of the played playlist or album
:param limit: minimum number of watch playlist items to return
:param radio: get a radio playlist (changes each time)
:param shuffle: shuffle the input playlist. only works when the playlistId parameter
is set at the same time. does not work if radio=True
:return: List of watch playlist items. The counterpart key is optional and only
appears if a song has a corresponding video counterpart (UI song/video
switcher).
Example::
{
"tracks": [
{
"videoId": "9mWr4c_ig54",
"title": "Foolish Of Me (feat. Jonathan Mendelsohn)",
"length": "3:07",
"thumbnail": [
{
"url": "https://lh3.googleusercontent.com/ulK2YaLtOW0PzcN7ufltG6e4ae3WZ9Bvg8CCwhe6LOccu1lCKxJy2r5AsYrsHeMBSLrGJCNpJqXgwczk=w60-h60-l90-rj",
"width": 60,
"height": 60
}...
],
"feedbackTokens": {
"add": "AB9zfpIGg9XN4u2iJ...",
"remove": "AB9zfpJdzWLcdZtC..."
},
"likeStatus": "INDIFFERENT",
"videoType": "MUSIC_VIDEO_TYPE_ATV",
"artists": [
{
"name": "Seven Lions",
"id": "UCYd2yzYRx7b9FYnBSlbnknA"
},
{
"name": "Jason Ross",
"id": "UCVCD9Iwnqn2ipN9JIF6B-nA"
},
{
"name": "Crystal Skies",
"id": "UCTJZESxeZ0J_M7JXyFUVmvA"
}
],
"album": {
"name": "Foolish Of Me",
"id": "MPREb_C8aRK1qmsDJ"
},
"year": "2020",
"counterpart": {
"videoId": "E0S4W34zFMA",
"title": "Foolish Of Me [ABGT404] (feat. Jonathan Mendelsohn)",
"length": "3:07",
"thumbnail": [...],
"feedbackTokens": null,
"likeStatus": "LIKE",
"artists": [
{
"name": "Jason Ross",
"id": null
},
{
"name": "Seven Lions",
"id": null
},
{
"name": "Crystal Skies",
"id": null
}
],
"views": "6.6K"
}
},...
],
"playlistId": "RDAMVM4y33h81phKU",
"lyrics": "MPLYt_HNNclO0Ddoc-17"
}
"""
body = {
"enablePersistentPlaylistPanel": True,
"isAudioOnly": True,
"tunerSettingValue": "AUTOMIX_SETTING_NORMAL",
}
if not videoId and not playlistId:
raise YTMusicUserError("You must provide either a video id, a playlist id, or both")
if videoId:
body["videoId"] = videoId
if not playlistId:
playlistId = "RDAMVM" + videoId
if not (radio or shuffle):
body["watchEndpointMusicSupportedConfigs"] = {
"watchEndpointMusicConfig": {
"hasPersistentPlaylistPanel": True,
"musicVideoType": "MUSIC_VIDEO_TYPE_ATV",
}
}
is_playlist = False
if playlistId:
playlist_id = validate_playlist_id(playlistId)
is_playlist = playlist_id.startswith("PL") or playlist_id.startswith("OLA")
body["playlistId"] = playlist_id
if shuffle and playlistId is not None:
body["params"] = "wAEB8gECKAE%3D"
if radio:
body["params"] = "wAEB"
endpoint = "next"
response = self._send_request(endpoint, body)
watchNextRenderer = nav(
response,
[
"contents",
"singleColumnMusicWatchNextResultsRenderer",
"tabbedRenderer",
"watchNextTabbedResultsRenderer",
],
)
lyrics_browse_id = get_tab_browse_id(watchNextRenderer, 1)
related_browse_id = get_tab_browse_id(watchNextRenderer, 2)
results = nav(
watchNextRenderer, [*TAB_CONTENT, "musicQueueRenderer", "content", "playlistPanelRenderer"], True
)
if not results:
msg = "No content returned by the server."
if playlistId:
msg += f"\nEnsure you have access to {playlistId} - a private playlist may cause this."
raise YTMusicServerError(msg)
playlist = next(
filter(
bool,
map(
lambda x: nav(x, ["playlistPanelVideoRenderer", *NAVIGATION_PLAYLIST_ID], True),
results["contents"],
),
),
None,
)
tracks = parse_watch_playlist(results["contents"])
if "continuations" in results:
request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
parse_func = lambda contents: parse_watch_playlist(contents)
tracks.extend(
get_continuations(
results,
"playlistPanelContinuation",
limit - len(tracks),
request_func,
parse_func,
"" if is_playlist else "Radio",
)
)
return dict(tracks=tracks, playlistId=playlist, lyrics=lyrics_browse_id, related=related_browse_id)
|