getWatchPlaylist method

Future<JsonMap> getWatchPlaylist({
  1. String? videoId,
  2. String? playlistId,
  3. int limit = 25,
  4. bool radio = false,
  5. bool shuffle = false,
})

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.

  • videoId videoId of the played video.
  • playlistId playlistId of the played playlist or album.
  • limit Minimum number of watch playlist items to return.
  • radio Get a radio playlist (changes each time).
  • shuffle Shuffle the input playlist. Only works when the playlistId parameter is set at the same time. Does not work if radio=true.

Returns 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"
}

Implementation

Future<JsonMap> getWatchPlaylist({
  String? videoId,
  String? playlistId,
  int limit = 25,
  bool radio = false,
  bool shuffle = false,
}) async {
  final body = <String, dynamic>{
    'enablePersistentPlaylistPanel': true,
    'isAudioOnly': true,
    'tunerSettingValue': 'AUTOMIX_SETTING_NORMAL',
  };

  if (videoId == null && playlistId == null) {
    throw YTMusicUserError(
      'You must provide either a video id, a playlist id, or both',
    );
  }

  if (videoId != null) {
    body['videoId'] = videoId;
    playlistId ??= 'RDAMVM$videoId';

    if (!radio && !shuffle) {
      body['watchEndpointMusicSupportedConfigs'] = {
        'watchEndpointMusicConfig': {
          'hasPersistentPlaylistPanel': true,
          'musicVideoType': 'MUSIC_VIDEO_TYPE_ATV',
        },
      };
    }
  }

  var isPlaylist = false;
  if (playlistId != null) {
    final playlistIdValidated = validatePlaylistId(playlistId);
    isPlaylist =
        playlistIdValidated.startsWith('PL') ||
        playlistIdValidated.startsWith('OLA');
    body['playlistId'] = playlistIdValidated;
  }

  if (shuffle && playlistId != null) {
    body['params'] = 'wAEB8gECKAE%3D';
  }
  if (radio) {
    body['params'] = 'wAEB';
  }

  const endpoint = 'next';
  final response = await sendRequest(endpoint, body);

  final watchNextRenderer = nav(response, [
    'contents',
    'singleColumnMusicWatchNextResultsRenderer',
    'tabbedRenderer',
    'watchNextTabbedResultsRenderer',
  ]);

  final lyricsBrowseId = getTabBrowseId(watchNextRenderer as JsonMap, 1);
  final relatedBrowseId = getTabBrowseId(watchNextRenderer, 2);

  final results =
      nav(watchNextRenderer, [
            ...TAB_CONTENT,
            'musicQueueRenderer',
            'content',
            'playlistPanelRenderer',
          ], nullIfAbsent: true)
          as JsonMap?;

  if (results == null) {
    var msg = 'No content returned by the server.';
    if (playlistId != null) {
      msg +=
          '\nEnsure you have access to $playlistId - a private playlist may cause this.';
    }
    throw YTMusicServerError(msg);
  }

  final playlistIterable = (results['contents'] as List)
      .map(
        (x) => nav(x, [
          'playlistPanelVideoRenderer',
          ...NAVIGATION_PLAYLIST_ID,
        ], nullIfAbsent: true),
      )
      .where((x) => x != null && x != false);

  final playlist = playlistIterable.isEmpty ? null : playlistIterable.first;

  final tracks = parseWatchPlaylist(
    List<JsonMap>.from(results['contents'] as List),
  );

  if (results.containsKey('continuations')) {
    Future<JsonMap> requestFunc(dynamic additionalParams) => sendRequest(
      endpoint,
      body,
      additionalParams: additionalParams as String,
    );
    List parseFunc(dynamic contents) =>
        parseWatchPlaylist(List<JsonMap>.from(contents as List));

    tracks.addAll(
      await getContinuations(
        results,
        'playlistPanelContinuation',
        limit - tracks.length,
        requestFunc,
        parseFunc,
        additionalParams: isPlaylist ? '' : 'Radio',
      ),
    );
  }

  return {
    'tracks': tracks,
    'playlistId': playlist,
    'lyrics': lyricsBrowseId,
    'related': relatedBrowseId,
  };
}