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",
      "feedbackTokens": {
        "add": "AB9zfpJxtvrU...",
        "remove": "AB9zfpKTyZ..."
      }
    }
  ]
}

The setVideoId is the unique id of this playlist item and needed for moving/removing playlist items.

Implementation

Future<JsonMap> getPlaylist(
  String playlistId, {
  int? limit = 100,
  bool related = false,
  int suggestionsLimit = 0,
}) async {
  checkAuth();
  final browseId = playlistId.startsWith('VL') ? playlistId : 'VL$playlistId';
  final 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);

  if (playlistId.startsWith('OLA') || playlistId.startsWith('VLOLA')) {
    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
          ? ((descriptionShelf['description'] as JsonMap)['runs']
                  as List<JsonMap>)
              .map((run) => run['text'])
              .join()
          : null;

  playlist.addAll(parsePlaylistHeaderMeta(header));

  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(results as List<JsonMap>);
      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(results as List<JsonMap>, 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(
      contentData['contents'] as List<JsonMap>,
    );
    List parseFunc(contents) => parsePlaylistItems(contents as List<JsonMap>);
    (playlist['tracks'] as List).addAll(
      await getContinuations2025(
        contentData,
        limit,
        requestFuncContinuations,
        parseFunc,
      ),
    );
  }

  playlist['duration_seconds'] = sumTotalDuration(playlist);

  return playlist;
}