search method

Future<List> search(
  1. String query, {
  2. String? filter,
  3. String? scope,
  4. int limit = 20,
  5. bool ignoreSpelling = false,
})

Search YouTube music.

Returns results within the provided category.

  • query Query string, i.e. 'Oasis Wonderwall'.
  • filter Filter for item types. Allowed values: songs, videos, albums, artists, playlists, community_playlists, featured_playlists, uploads. (Default: Default search, including all types of items).
  • scope Search scope. Allowed values: library, uploads. (Default: Search the public YouTube Music catalogue). Changing scope from the default will reduce the number of settable filters. Setting a filter that is not permitted will throw an exception. For uploads, no filter can be set. For library, community_playlists and featured_playlists filter cannot be set.
  • limit Number of search results to return. (Default: 20).
  • ignoreSpelling Whether to ignore YTM spelling suggestions. If true, the exact search term will be searched for, and will not be corrected. This does not have any effect when the filter is set to uploads. (Default: false, will use YTM's default behavior of autocorrecting the search.

Returns List of results depending on filter.

  • resultType specifies the type of item (important for default search). Albums, artists and playlists additionally contain a browseId, corresponding to albumId, channelId and playlistId (browseId=VL+playlistId).

Example list for default search with one result per resultType for brevity. Normally there are 3 results per resultType and an additional thumbnails key:

[
  {
    "category": "Top result",
    "resultType": "video",
    "videoId": "vU05Eksc_iM",
    "title": "Wonderwall",
    "artists": [
      {
        "name": "Oasis",
        "id": "UCmMUZbaYdNH0bEd1PAlAqsA"
      }
    ],
    "views": "1.4M",
    "videoType": "MUSIC_VIDEO_TYPE_OMV",
    "duration": "4:38",
    "duration_seconds": 278
  },
  {
    "category": "Songs",
    "resultType": "song",
    "videoId": "ZrOKjDZOtkA",
    "title": "Wonderwall",
    "artists": [
      {
        "name": "Oasis",
        "id": "UCmMUZbaYdNH0bEd1PAlAqsA"
      }
    ],
    "album": {
      "name": "(What's The Story) Morning Glory? (Remastered)",
      "id": "MPREb_9nqEki4ZDpp"
    },
    "duration": "4:19",
    "duration_seconds": 259,
    "isExplicit": false,
    "feedbackTokens": {
      "add": null,
      "remove": null
    }
  },
  {
    "category": "Albums",
    "resultType": "album",
    "browseId": "MPREb_IInSY5QXXrW",
    "playlistId": "OLAK5uy_kunInnOpcKECWIBQGB0Qj6ZjquxDvfckg",
    "title": "(What's The Story) Morning Glory?",
    "type": "Album",
    "artist": "Oasis",
    "year": "1995",
    "isExplicit": false
  },
  {
    "category": "Community playlists",
    "resultType": "playlist",
    "browseId": "VLPLK1PkWQlWtnNfovRdGWpKffO1Wdi2kvDx",
    "title": "Wonderwall - Oasis",
    "author": "Tate Henderson",
    "itemCount": "174"
  },
  {
    "category": "Videos",
    "resultType": "video",
    "videoId": "bx1Bh8ZvH84",
    "title": "Wonderwall",
    "artists": [
      {
        "name": "Oasis",
        "id": "UCmMUZbaYdNH0bEd1PAlAqsA"
      }
    ],
    "views": "386M",
    "duration": "4:38",
    "duration_seconds": 278
  },
  {
    "category": "Artists",
    "resultType": "artist",
    "browseId": "UCmMUZbaYdNH0bEd1PAlAqsA",
    "artist": "Oasis",
    "shuffleId": "RDAOkjHYJjL1a3xspEyVkhHAsg",
    "radioId": "RDEMkjHYJjL1a3xspEyVkhHAsg"
  },
  {
    "category": "Profiles",
    "resultType": "profile",
    "title": "Taylor Swift Time",
    "name": "@TaylorSwiftTime",
    "browseId": "UCSCRK7XlVQ6fBdEl00kX6pQ",
    "thumbnails": ...
  }
]

Implementation

Future<List> search(
  String query, {
  String? filter,
  String? scope,
  int limit = 20,
  bool ignoreSpelling = false,
}) async {
  final body = <String, dynamic>{'query': query};
  const endpoint = 'search';
  final searchResults = <dynamic>[];

  const filters = [
    'albums',
    'artists',
    'playlists',
    'community_playlists',
    'featured_playlists',
    'songs',
    'videos',
    'profiles',
    'podcasts',
    'episodes',
  ];
  if (filter != null && !filters.contains(filter)) {
    throw YTMusicUserError(
      'Invalid filter provided. Please use one of the following filters or leave out the parameter: '
      '${filters.join(', ')}',
    );
  }

  const scopes = ['library', 'uploads'];
  if (scope != null && !scopes.contains(scope)) {
    throw YTMusicUserError(
      'Invalid scope provided. Please use one of the following scopes or leave out the parameter: '
      '${scopes.join(', ')}',
    );
  }

  if (scope == 'uploads' && filter != null) {
    throw YTMusicUserError(
      'No filter can be set when searching uploads. Please unset the filter parameter when scope is set to uploads.',
    );
  }

  if (scope == 'library' &&
      filter != null &&
      ['community_playlists', 'featured_playlists'].contains(filter)) {
    throw YTMusicUserError(
      '$filter cannot be set when searching library. Please use one of the following filters or leave out the parameter: '
      '${filters.sublist(0, 3).followedBy(filters.sublist(5)).join(', ')}',
    );
  }

  final params = getSearchParams(filter, scope, ignoreSpelling);
  if (params != null) {
    body['params'] = params;
  }

  final response = await sendRequest(endpoint, body);

  // no results
  if (!response.containsKey('contents')) return searchResults;

  dynamic results;
  if ((response['contents'] as JsonMap).containsKey(
    'tabbedSearchResultsRenderer',
  )) {
    final tabIndex =
        (scope == null || filter == null) ? 0 : scopes.indexOf(scope) + 1;
    results =
        (((((response['contents'] as JsonMap)['tabbedSearchResultsRenderer']
                        as JsonMap)['tabs']
                    as List)[tabIndex]
                as JsonMap)['tabRenderer']
            as JsonMap)['content'];
  } else {
    results = response['contents'];
  }

  final sectionList = List<JsonMap>.from(nav(results, SECTION_LIST) as List);

  // no results
  if (sectionList.length == 1 &&
      sectionList.first.containsKey('itemSectionRenderer')) {
    return searchResults;
  }

  // set filter for parser
  String? resultType;
  final String? realFilter;
  if (filter != null && filter.contains('playlists')) {
    realFilter = 'playlists';
  } else if (scope == 'uploads') {
    realFilter = 'uploads';
    resultType = 'upload';
  } else {
    realFilter = filter;
  }

  for (final res in sectionList) {
    String? category;
    List<JsonMap> shelfContents;

    if (res.containsKey('musicCardShelfRenderer')) {
      final topResult = parseTopResult(
        res['musicCardShelfRenderer'] as JsonMap,
        parser.getSearchResultTypes(),
      );
      searchResults.add(topResult);

      shelfContents = List<JsonMap>.from(
        (nav(res, [
                  'musicCardShelfRenderer',
                  'contents',
                ], nullIfAbsent: true) ??
                [])
            as List,
      );
      if (shelfContents.isEmpty) continue;

      // if "more from youtube" is present, remove it - it's not parseable
      if (shelfContents.first.containsKey('messageRenderer')) {
        category =
            nav(shelfContents.removeAt(0), [
                  'messageRenderer',
                  ...TEXT_RUN_TEXT,
                ])
                as String?;
      }
    } else if (res.containsKey('musicShelfRenderer')) {
      shelfContents = List<JsonMap>.from(
        (res['musicShelfRenderer'] as JsonMap)['contents'] as List,
      );
      category =
          nav(res, MUSIC_SHELF + TITLE_TEXT, nullIfAbsent: true) as String?;

      // if we know the filter it's easy to set the result type
      // unfortunately uploads is modeled as a filter (historical reasons),
      // so we take care to not set the result type for that scope
      if (realFilter != null && scope != 'uploads') {
        resultType =
            realFilter.substring(0, realFilter.length - 1).toLowerCase();
      }
    } else {
      continue;
    }

    searchResults.addAll(
      parseSearchResults(
        shelfContents,
        resultType: resultType,
        category: category,
      ),
    );

    if (realFilter != null) {
      // if filter is set, there are continuations
      Future<JsonMap> requestFunc(dynamic additionalParams) => sendRequest(
        endpoint,
        body,
        additionalParams: additionalParams as String,
      );
      List parseFunc(contents) => parseSearchResults(
        List<JsonMap>.from(contents as List),
        resultType: resultType,
        category: category,
      );

      searchResults.addAll(
        await getContinuations(
          res['musicShelfRenderer'] as JsonMap,
          'musicShelfContinuation',
          limit - searchResults.length,
          requestFunc,
          parseFunc,
        ),
      );
    }
  }

  return searchResults;
}