getPlaylist method
Returns a list of playlist items.
playlistIdPlaylist id.limitHow many songs to return.nullretrieves them all. (Default:100).relatedWhether to fetch 10 related playlists or not. (Default:false).suggestionsLimitHow many suggestions to return. The result is a list of suggested playlist items (videos) contained in asuggestionskey. 7 items are retrieved in each internal request. (Default:0).
Returns Map with information about the playlist.
The key tracks contains a List of playlistItem Maps
The result is in the following format:
{
"id": "PLQwVIlKxHM6qv-o99iX9R85og7IzF9YS_",
"privacy": "PUBLIC",
"title": "New EDM This Week 03/13/2020",
"thumbnails": [...],
"description": "Weekly r/EDM new release roundup. Created with github.com/sigma67/spotifyplaylist_to_gmusic",
"author": {
"name": "sigmatics",
"id": "..."
},
"year": "2020",
"duration": "6+ hours",
"duration_seconds": 52651,
"trackCount": 237,
"suggestions": [
{
"videoId": "HLCsfOykA94",
"title": "Mambo (GATTÜSO Remix)",
"artists": [
{
"name": "Nikki Vianna",
"id": "UCMW5eSIO1moVlIBLQzq4PnQ"
}
],
"album": {
"name": "Mambo (GATTÜSO Remix)",
"id": "MPREb_jLeQJsd7U9w"
},
"likeStatus": "LIKE",
"thumbnails": [...],
"isAvailable": true,
"isExplicit": false,
"duration": "3:32",
"duration_seconds": 212,
"setVideoId": "to_be_updated_by_client"
}
],
"related": [
{
"title": "Presenting MYRNE",
"playlistId": "RDCLAK5uy_mbdO3_xdD4NtU1rWI0OmvRSRZ8NH4uJCM",
"thumbnails": [...],
"description": "Playlist • YouTube Music"
}
],
"tracks": [
{
"videoId": "bjGppZKiuFE",
"title": "Lost",
"artists": [
{
"name": "Guest Who",
"id": "UCkgCRdnnqWnUeIH7EIc3dBg"
},
{
"name": "Kate Wild",
"id": "UCwR2l3JfJbvB6aq0RnnJfWg"
}
],
"album": {
"name": "Lost",
"id": "MPREb_PxmzvDuqOnC"
},
"duration": "2:58",
"duration_seconds": 178,
"setVideoId": "748EE8...",
"likeStatus": "INDIFFERENT",
"thumbnails": [...],
"isAvailable": true,
"isExplicit": false,
"videoType": "MUSIC_VIDEO_TYPE_OMV",
"inLibrary": false,
"feedbackTokens": {
"add": "AB9zfpJxtvrU...",
"remove": "AB9zfpKTyZ..."
},
"pinnedToListenAgain": false,
"listenAgainFeedbackTokens": {
"pin": "AB9zfpImL2k...",
"unpin": "AB9zfpJt6pA..."
},
"communityVoteStatus": {
"netVoteValue": 12,
"status": "VOTE_STATUS_UPVOTED"
}
}
"creditsBrowseId": "MPTCekz1IJ9I0sw"
}
]
}
The setVideoId is the unique id of this playlist item and
needed for moving/removing playlist items.
Note that communityVoteStatus can be null if the information is not available (i.e. playlist does not have that setting enabled, the request is not authenticated, ...)
Collaborative playlists replace author with limited data about collaborators:
{
"collaborators": {
"text": "by Sample Author and 1 other",
"avatars":
{
"url": "https://yt3.ggpht.com/sample-author-photo"
},
{
"url": "https://yt3.ggpht.com/sample-collaborator-photo"
}
}
}
Implementation
Future<JsonMap> getPlaylist(
String playlistId, {
int? limit = 100,
bool related = false,
int suggestionsLimit = 0,
}) async {
// checkAuth(); // TODO this was in original code, can be removed?
final browseId = playlistId.startsWith('VL') ? playlistId : 'VL$playlistId';
final JsonMap body = {'browseId': browseId};
const endpoint = 'browse';
Future<JsonMap> requestFunc(String additionalParams) =>
sendRequest(endpoint, body, additionalParams: additionalParams);
final response = await requestFunc('');
Future<JsonMap> requestFuncContinuations(JsonMap body) =>
sendRequest(endpoint, body);
final isOla =
playlistId.startsWith('OLA') || playlistId.startsWith('VLOLA');
final hasPlaylistHeader =
nav(response, [
...TWO_COLUMN_RENDERER,
...TAB_CONTENT,
...SECTION_LIST_ITEM,
], nullIfAbsent: true) !=
null;
if (isOla && !hasPlaylistHeader) {
return parseAudioPlaylist(response, limit, requestFuncContinuations);
}
final headerData =
nav(response, [
...TWO_COLUMN_RENDERER,
...TAB_CONTENT,
...SECTION_LIST_ITEM,
])
as JsonMap;
final sectionList =
nav(response, [...TWO_COLUMN_RENDERER, 'secondaryContents', ...SECTION])
as JsonMap;
final playlist = <String, dynamic>{};
playlist['owned'] = headerData.containsKey(
EDITABLE_PLAYLIST_DETAIL_HEADER[0],
);
late JsonMap header;
if (!(playlist['owned'] as bool)) {
header = nav(headerData, RESPONSIVE_HEADER) as JsonMap;
playlist['id'] = nav(header, [
'buttons',
1,
'musicPlayButtonRenderer',
'playNavigationEndpoint',
...WATCH_PLAYLIST_ID,
], nullIfAbsent: true);
playlist['privacy'] = 'PUBLIC';
} else {
playlist['id'] = nav(headerData, [
...EDITABLE_PLAYLIST_DETAIL_HEADER,
...PLAYLIST_ID,
]);
header =
nav(headerData, [
...EDITABLE_PLAYLIST_DETAIL_HEADER,
...HEADER,
...RESPONSIVE_HEADER,
])
as JsonMap;
playlist['privacy'] =
(((headerData[EDITABLE_PLAYLIST_DETAIL_HEADER[0]]
as JsonMap)['editHeader']
as JsonMap)['musicPlaylistEditHeaderRenderer']
as JsonMap)['privacy'];
}
final descriptionShelf =
nav(header, ['description', ...DESCRIPTION_SHELF], nullIfAbsent: true)
as JsonMap?;
playlist['description'] =
descriptionShelf != null
? List<JsonMap>.from(
(descriptionShelf['description'] as JsonMap)['runs'] as List,
).map((run) => run['text']).join()
: null;
playlist.addAll(parsePlaylistHeaderMeta(header));
final isCollaborative = playlist.containsKey('collaborators');
playlist.addAll(
parseSongRuns(
(nav(header, SUBTITLE_RUNS) as List).sublist(
2 + ((playlist['owned'] as bool) ? 2 : 0),
),
),
);
// suggestions and related are missing e.g. on liked songs
playlist['related'] = [];
if (sectionList.containsKey('continuations')) {
var additionalParams = getContinuationParams(sectionList);
if ((playlist['owned'] as bool) && (suggestionsLimit > 0 || related)) {
List parseFunc(results) =>
parsePlaylistItems(List<JsonMap>.from(results as List));
final suggested = await requestFunc(additionalParams);
final continuation = nav(suggested, SECTION_LIST_CONTINUATION);
additionalParams = getContinuationParams(continuation as JsonMap);
final suggestionsShelf = nav(continuation, [
...CONTENT,
...MUSIC_SHELF,
]);
playlist['suggestions'] = getContinuationContents(
suggestionsShelf as JsonMap,
parseFunc,
);
(playlist['suggestions'] as List).addAll(
await getReloadableContinuations(
suggestionsShelf,
'musicShelfContinuation',
suggestionsLimit -
((playlist['suggestions'] as List).length), // TODO
requestFunc,
parseFunc,
),
);
}
if (related) {
final relatedResponse = await requestFunc(additionalParams);
final continuation = nav(
relatedResponse,
SECTION_LIST_CONTINUATION,
nullIfAbsent: true,
);
if (continuation != null) {
Future<List> parseFunc(results) => parseContentList(
List<JsonMap>.from(results as List),
parsePlaylist,
);
playlist['related'] = getContinuationContents(
nav(continuation, [...CONTENT, ...CAROUSEL]) as JsonMap,
parseFunc,
);
}
}
}
playlist['tracks'] = [];
final contentData =
nav(sectionList, [...CONTENT, 'musicPlaylistShelfRenderer']) as JsonMap;
if (contentData.containsKey('contents')) {
playlist['tracks'] = parsePlaylistItems(
List<JsonMap>.from(contentData['contents'] as List),
isCollaborative: isCollaborative,
);
List parseFunc(contents) => parsePlaylistItems(
List<JsonMap>.from(contents as List),
isCollaborative: isCollaborative,
);
(playlist['tracks'] as List).addAll(
List<JsonMap>.from(
await getContinuations2025(
contentData,
limit,
requestFuncContinuations,
parseFunc,
),
),
);
}
playlist['duration_seconds'] = sumTotalDuration(playlist);
return playlist;
}