getPlaylist method

Future<JsonMap> getPlaylist(
  1. String playlistId, {
  2. int? limit = 100,
  3. bool related = false,
  4. int suggestionsLimit = 0,
})

Returns a list of playlist items.

  • playlistId Playlist id.
  • limit How many songs to return. null retrieves them all. (Default: 100).
  • related Whether to fetch 10 related playlists or not. (Default: false).
  • suggestionsLimit How many suggestions to return. The result is a list of suggested playlist items (videos) contained in a suggestions key. 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;
}