Skip to main content

ytmapi_rs/parse/
search.rs

1use super::{
2    DISPLAY_POLICY, ParseFrom, ProcessedResult, flex_column_item_pointer, parse_flex_column_item,
3};
4use crate::common::{
5    AlbumID, AlbumType, ArtistChannelID, ContinuationParams, EpisodeID, Explicit, PlaylistID,
6    PodcastID, SearchSuggestion, SuggestionType, TextRun, Thumbnail, UserChannelID, VideoID,
7};
8use crate::continuations::ParseFromContinuable;
9use crate::nav_consts::{
10    BADGE_LABEL, CONTINUATION_PARAMS, LIVE_BADGE_LABEL, MRLIR, MUSIC_CARD_SHELF, MUSIC_SHELF,
11    MUSIC_SHELF_CONTINUATION, NAVIGATION_BROWSE, NAVIGATION_BROWSE_ID, PAGE_TYPE, PLAY_BUTTON,
12    PLAYLIST_ITEM_VIDEO_ID, SECTION_LIST, SUBTITLE, SUBTITLE2, TAB_CONTENT, THUMBNAILS, TITLE_TEXT,
13};
14use crate::parse::{EpisodeDate, ParsedSongAlbum};
15use crate::query::search::UnfilteredSearchType;
16use crate::query::search::filteredsearch::{
17    AlbumsFilter, ArtistsFilter, CommunityPlaylistsFilter, EpisodesFilter, FeaturedPlaylistsFilter,
18    FilteredSearch, FilteredSearchType, PlaylistsFilter, PodcastsFilter, ProfilesFilter,
19    SongsFilter, VideosFilter,
20};
21use crate::query::*;
22use crate::youtube_enums::{PlaylistEndpointParams, YoutubeMusicPageType};
23use crate::{Error, Result};
24use const_format::concatcp;
25use itertools::Itertools;
26use json_crawler::{JsonCrawler, JsonCrawlerBorrowed, JsonCrawlerIterator, JsonCrawlerOwned};
27use serde::de::IntoDeserializer;
28use serde::{Deserialize, Serialize};
29
30#[cfg(test)]
31mod tests;
32
33#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct SearchResults {
36    pub top_results: Vec<TopResult>,
37    pub artists: Vec<SearchResultArtist>,
38    pub albums: Vec<SearchResultAlbum>,
39    pub featured_playlists: Vec<SearchResultFeaturedPlaylist>,
40    pub community_playlists: Vec<BasicSearchResultCommunityPlaylist>,
41    pub songs: Vec<SearchResultSong>,
42    pub videos: Vec<SearchResultVideo>,
43    pub podcasts: Vec<SearchResultPodcast>,
44    pub episodes: Vec<SearchResultEpisode>,
45    pub profiles: Vec<SearchResultProfile>,
46}
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48/// Each Top Result has it's own type.
49pub enum TopResultType {
50    Artist,
51    Playlist,
52    Song,
53    Video,
54    Station,
55    Podcast,
56    #[serde(untagged)]
57    Album(AlbumType),
58}
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60// Helper enum for parsing different search result types.
61enum SearchResultType {
62    #[serde(alias = "Top result")]
63    TopResult,
64    Artists,
65    Albums,
66    #[serde(alias = "Featured playlists")]
67    FeaturedPlaylists,
68    #[serde(alias = "Community playlists")]
69    CommunityPlaylists,
70    Songs,
71    Videos,
72    Podcasts,
73    Episodes,
74    Profiles,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[non_exhaustive]
79/// Dynamically defined top result.
80/// Some fields are optional as they are not defined for all result types.
81// In future, may be possible to make this type safe.
82// TODO: Add endpoint id.
83pub struct TopResult {
84    pub result_name: String,
85    /// Both Videos and Songs can have this left out.
86    pub result_type: Option<TopResultType>,
87    pub thumbnails: Vec<Thumbnail>,
88    pub artist: Option<String>,
89    pub album: Option<String>,
90    pub duration: Option<String>,
91    pub year: Option<String>,
92    pub subscribers: Option<String>,
93    pub plays: Option<String>,
94    /// Podcast publisher.
95    pub publisher: Option<String>,
96    /// Generic tagline that can appear on top results
97    pub byline: Option<String>,
98}
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100#[non_exhaustive]
101/// An artist search result.
102pub struct SearchResultArtist {
103    pub artist: String,
104    /// An artist with no subscribers won't contain this field.
105    pub subscribers: Option<String>,
106    pub browse_id: ArtistChannelID<'static>,
107    pub thumbnails: Vec<Thumbnail>,
108}
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110#[non_exhaustive]
111/// A podcast search result.
112pub struct SearchResultPodcast {
113    pub title: String,
114    pub publisher: String,
115    pub podcast_id: PodcastID<'static>,
116    pub thumbnails: Vec<Thumbnail>,
117}
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[non_exhaustive]
120/// A podcast episode search result.
121pub struct SearchResultEpisode {
122    pub title: String,
123    pub date: EpisodeDate,
124    pub channel_name: String,
125    pub episode_id: EpisodeID<'static>,
126    // Potentially can include link to channel.
127    pub thumbnails: Vec<Thumbnail>,
128}
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130/// A video search result. May be a video or a video episode of a podcast.
131pub enum SearchResultVideo {
132    #[non_exhaustive]
133    Video {
134        title: String,
135        /// Note: Either Youtube channel name, or artist name.
136        // Potentially can include link to channel.
137        channel_name: String,
138        video_id: VideoID<'static>,
139        views: String,
140        length: String,
141        thumbnails: Vec<Thumbnail>,
142    },
143    #[non_exhaustive]
144    VideoEpisode {
145        // Potentially asame as SearchResultEpisode
146        title: String,
147        date: EpisodeDate,
148        channel_name: String,
149        episode_id: EpisodeID<'static>,
150        // Potentially can include link to channel.
151        thumbnails: Vec<Thumbnail>,
152    },
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156#[non_exhaustive]
157/// A profile search result.
158pub struct SearchResultProfile {
159    pub title: String,
160    pub username: String,
161    pub profile_id: UserChannelID<'static>,
162    pub thumbnails: Vec<Thumbnail>,
163}
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165#[non_exhaustive]
166/// An album search result.
167pub struct SearchResultAlbum {
168    pub title: String,
169    pub artist: String,
170    pub year: String,
171    pub explicit: Explicit,
172    pub album_id: AlbumID<'static>,
173    pub album_type: AlbumType,
174    pub thumbnails: Vec<Thumbnail>,
175}
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[non_exhaustive]
178pub struct SearchResultSong {
179    // Potentially can include links to artist and album.
180    pub title: String,
181    pub artist: String,
182    // Album field can be optional - see https://github.com/nick42d/youtui/issues/174
183    pub album: Option<ParsedSongAlbum>,
184    pub duration: String,
185    pub plays: String,
186    pub explicit: Explicit,
187    pub video_id: VideoID<'static>,
188    pub thumbnails: Vec<Thumbnail>,
189}
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191#[non_exhaustive]
192// A playlist search result may be a featured or community playlist or even a
193// podcast.
194pub enum SearchResultPlaylist {
195    Featured(SearchResultFeaturedPlaylist),
196    Community(SearchResultCommunityPlaylist),
197    Podcast(SearchResultPodcast),
198}
199
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201#[non_exhaustive]
202// When doing a basic search, community playlists might actually be podcasts.
203pub enum BasicSearchResultCommunityPlaylist {
204    Podcast(SearchResultPodcast),
205    Playlist(SearchResultCommunityPlaylist),
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209#[non_exhaustive]
210/// A community playlist search result.
211pub struct SearchResultCommunityPlaylist {
212    pub title: String,
213    pub author: String,
214    pub views: String,
215    pub playlist_id: PlaylistID<'static>,
216    pub thumbnails: Vec<Thumbnail>,
217}
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[non_exhaustive]
220/// A featured playlist search result.
221pub struct SearchResultFeaturedPlaylist {
222    pub title: String,
223    pub author: String,
224    pub songs: String,
225    pub playlist_id: PlaylistID<'static>,
226    pub thumbnails: Vec<Thumbnail>,
227}
228
229// TODO: Type safety
230fn parse_basic_search_result_from_section_list_contents(
231    mut section_list_contents: BasicSearchSectionListContents,
232) -> Result<SearchResults> {
233    // Imperative solution, may be able to make more functional.
234    let mut top_results = Vec::new();
235    let mut artists = Vec::new();
236    let mut albums = Vec::new();
237    let mut featured_playlists = Vec::new();
238    let mut community_playlists = Vec::new();
239    let mut songs = Vec::new();
240    let mut videos = Vec::new();
241    let mut podcasts = Vec::new();
242    let mut episodes = Vec::new();
243    let mut profiles = Vec::new();
244
245    let music_card_shelf = section_list_contents
246        .0
247        .try_iter_mut()?
248        .find_path(MUSIC_CARD_SHELF)
249        .ok();
250    if let Some(music_card_shelf) = music_card_shelf {
251        top_results = parse_top_results_from_music_card_shelf_contents(music_card_shelf)?
252    }
253    let results_iter = section_list_contents
254        .0
255        .try_into_iter()?
256        .filter_map(|item| item.navigate_pointer(MUSIC_SHELF).ok());
257
258    for mut category in results_iter {
259        match category.take_value_pointer::<SearchResultType>(TITLE_TEXT)? {
260            SearchResultType::TopResult => {
261                top_results = category
262                    .navigate_pointer("/contents")?
263                    .try_iter_mut()?
264                    .filter_map(|r| parse_top_result_from_music_shelf_contents(r).transpose())
265                    .collect::<Result<Vec<TopResult>>>()?;
266            }
267            // TODO: Use a navigation constant
268            SearchResultType::Artists => {
269                artists = category
270                    .navigate_pointer("/contents")?
271                    .try_iter_mut()?
272                    .map(|r| parse_artist_search_result_from_music_shelf_contents(r))
273                    .collect::<Result<Vec<SearchResultArtist>>>()?;
274            }
275            SearchResultType::Albums => {
276                albums = category
277                    .navigate_pointer("/contents")?
278                    .try_iter_mut()?
279                    .map(|r| parse_album_search_result_from_music_shelf_contents(r))
280                    .collect::<Result<Vec<SearchResultAlbum>>>()?
281            }
282            SearchResultType::FeaturedPlaylists => {
283                featured_playlists = category
284                    .navigate_pointer("/contents")?
285                    .try_iter_mut()?
286                    .map(|r| parse_featured_playlist_search_result_from_music_shelf_contents(r))
287                    .collect::<Result<Vec<SearchResultFeaturedPlaylist>>>()?
288            }
289            SearchResultType::CommunityPlaylists => {
290                community_playlists = category
291                    .navigate_pointer("/contents")?
292                    .try_iter_mut()?
293                    .map(|r| {
294                        parse_community_playlist_basic_search_result_from_music_shelf_contents(r)
295                    })
296                    .collect::<Result<Vec<BasicSearchResultCommunityPlaylist>>>()?
297            }
298            SearchResultType::Songs => {
299                songs = category
300                    .navigate_pointer("/contents")?
301                    .try_iter_mut()?
302                    .map(|r| parse_song_search_result_from_music_shelf_contents(r))
303                    .collect::<Result<Vec<SearchResultSong>>>()?
304            }
305            SearchResultType::Videos => {
306                videos = category
307                    .navigate_pointer("/contents")?
308                    .try_iter_mut()?
309                    .filter_map(|r| {
310                        parse_video_search_result_from_music_shelf_contents(r).transpose()
311                    })
312                    .collect::<Result<Vec<SearchResultVideo>>>()?
313            }
314            SearchResultType::Podcasts => {
315                podcasts = category
316                    .navigate_pointer("/contents")?
317                    .try_iter_mut()?
318                    .map(|r| parse_podcast_search_result_from_music_shelf_contents(r))
319                    .collect::<Result<Vec<SearchResultPodcast>>>()?
320            }
321            SearchResultType::Episodes => {
322                episodes = category
323                    .navigate_pointer("/contents")?
324                    .try_iter_mut()?
325                    .map(|r| parse_episode_search_result_from_music_shelf_contents(r))
326                    .collect::<Result<Vec<SearchResultEpisode>>>()?
327            }
328            SearchResultType::Profiles => {
329                profiles = category
330                    .navigate_pointer("/contents")?
331                    .try_iter_mut()?
332                    .map(|r| parse_profile_search_result_from_music_shelf_contents(r))
333                    .collect::<Result<Vec<SearchResultProfile>>>()?
334            }
335        }
336    }
337    Ok(SearchResults {
338        top_results,
339        artists,
340        albums,
341        featured_playlists,
342        community_playlists,
343        songs,
344        videos,
345        podcasts,
346        episodes,
347        profiles,
348    })
349}
350
351fn parse_top_results_from_music_card_shelf_contents(
352    mut music_shelf_contents: JsonCrawlerBorrowed<'_>,
353) -> Result<Vec<TopResult>> {
354    let mut results = Vec::new();
355    // Begin - first result parsing
356    let result_name = music_shelf_contents.take_value_pointer(TITLE_TEXT)?;
357    // NOTE: Parse this before value at SUBTITLE is taken (below).
358    let result_type = music_shelf_contents
359        .borrow_value_pointer::<TopResultType>(SUBTITLE)
360        .ok();
361    let subtitle: String = music_shelf_contents.take_value_pointer(SUBTITLE)?;
362    let subtitle_2: Option<String> = music_shelf_contents.take_value_pointer(SUBTITLE2).ok();
363    // Possibly artists only.
364    let subscribers = subtitle_2;
365    let byline = match result_type {
366        Some(_) => None,
367        None => Some(subtitle),
368    };
369    // Imperative solution, may be able to make more functional.
370    let publisher = None;
371    let artist = None;
372    let album = None;
373    let duration = None;
374    let year = None;
375    let plays = None;
376    let thumbnails: Vec<Thumbnail> = music_shelf_contents.take_value_pointer(THUMBNAILS)?;
377    let first_result = TopResult {
378        // Assuming that in non-card case top result always has a result type.
379        result_type,
380        subscribers,
381        thumbnails,
382        result_name,
383        publisher,
384        artist,
385        album,
386        duration,
387        year,
388        plays,
389        byline,
390    };
391    // End - first result parsing.
392    results.push(first_result);
393    // Other results may not exist.
394    if let Ok(mut contents) = music_shelf_contents.navigate_pointer("/contents") {
395        contents
396            .try_iter_mut()?
397            .filter_map(|r| parse_top_result_from_music_shelf_contents(r).transpose())
398            .try_for_each(|r| -> Result<()> {
399                results.push(r?);
400                Ok(())
401            })?;
402    }
403    Ok(results)
404}
405// TODO: Tests
406fn parse_top_result_from_music_shelf_contents(
407    music_shelf_contents: JsonCrawlerBorrowed<'_>,
408) -> Result<Option<TopResult>> {
409    // This is the "More from YouTube" seperator
410    if music_shelf_contents.path_exists("/messageRenderer") {
411        return Ok(None);
412    };
413    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
414    let result_name = parse_flex_column_item(&mut mrlir, 0, 0)?;
415    // It's possible to have artist name in the first position instead of a
416    // TopResultType. There may be a way to differentiate this even further.
417    let flex_1_0: String = parse_flex_column_item(&mut mrlir, 1, 0)?;
418    // Deserialize without taking ownership of flex_1_0 - not possible with
419    // JsonCrawler::take_value_pointer().
420    // TODO: add methods like borrow_value_pointer() to JsonCrawler.
421    let result_type_result: std::result::Result<_, serde::de::value::Error> =
422        TopResultType::deserialize(flex_1_0.as_str().into_deserializer());
423    let result_type = result_type_result.ok();
424    // Imperative solution, may be able to make more functional.
425    let mut subscribers = None;
426    let mut publisher = None;
427    let mut artist = None;
428    let mut album = None;
429    let mut duration = None;
430    let mut year = None;
431    let mut plays = None;
432    match result_type {
433        // XXX: Perhaps also populate Artist field.
434        Some(TopResultType::Artist) => {
435            subscribers = Some(parse_flex_column_item(&mut mrlir, 1, 2)?)
436        }
437        Some(TopResultType::Album(_)) => {
438            // XXX: Perhaps also populate Album field.
439            artist = Some(parse_flex_column_item(&mut mrlir, 1, 2)?);
440            year = Some(parse_flex_column_item(&mut mrlir, 1, 4)?);
441        }
442        Some(TopResultType::Playlist) => todo!(),
443        Some(TopResultType::Song) => {
444            artist = Some(parse_flex_column_item(&mut mrlir, 1, 2)?);
445            album = Some(parse_flex_column_item(&mut mrlir, 1, 4)?);
446            duration = Some(parse_flex_column_item(&mut mrlir, 1, 6)?);
447            // This does not show up in all Card renderer results and so we'll define it as
448            // optional. TODO: Could make this more type safe in future.
449            plays = parse_flex_column_item(&mut mrlir, 1, 8).ok();
450        }
451        Some(TopResultType::Video) => todo!(),
452        Some(TopResultType::Station) => todo!(),
453        Some(TopResultType::Podcast) => publisher = Some(parse_flex_column_item(&mut mrlir, 1, 2)?),
454        None => {
455            artist = Some(flex_1_0);
456            let flex_1_2 = parse_flex_column_item(&mut mrlir, 1, 2)?;
457            // If this does not show up, album isn't included in the results.
458            if let Ok(flex_1_4) = parse_flex_column_item(&mut mrlir, 1, 4) {
459                album = Some(flex_1_2);
460                duration = Some(flex_1_4);
461            } else {
462                duration = Some(flex_1_2);
463            }
464            // This does not show up in all Card renderer results and so we'll define it as
465            // optional. TODO: Could make this more type safe in future.
466            plays = parse_flex_column_item(&mut mrlir, 1, 6).ok();
467        }
468    }
469    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
470    Ok(Some(TopResult {
471        result_type,
472        subscribers,
473        thumbnails,
474        result_name,
475        publisher,
476        artist,
477        album,
478        duration,
479        year,
480        plays,
481        byline: None,
482    }))
483}
484// TODO: Type safety
485// TODO: Tests
486fn parse_artist_search_result_from_music_shelf_contents(
487    music_shelf_contents: JsonCrawlerBorrowed<'_>,
488) -> Result<SearchResultArtist> {
489    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
490    let artist = parse_flex_column_item(&mut mrlir, 0, 0)?;
491    let subscribers = parse_flex_column_item(&mut mrlir, 1, 2).ok();
492    let browse_id = mrlir.take_value_pointer(NAVIGATION_BROWSE_ID)?;
493    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
494    Ok(SearchResultArtist {
495        artist,
496        subscribers,
497        thumbnails,
498        browse_id,
499    })
500}
501// TODO: Type safety
502// TODO: Tests
503fn parse_profile_search_result_from_music_shelf_contents(
504    music_shelf_contents: JsonCrawlerBorrowed<'_>,
505) -> Result<SearchResultProfile> {
506    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
507    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
508    let username = parse_flex_column_item(&mut mrlir, 1, 2)?;
509    let profile_id = mrlir.take_value_pointer(NAVIGATION_BROWSE_ID)?;
510    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
511    Ok(SearchResultProfile {
512        title,
513        username,
514        profile_id,
515        thumbnails,
516    })
517}
518// TODO: Type safety
519// TODO: Tests
520fn parse_album_search_result_from_music_shelf_contents(
521    music_shelf_contents: JsonCrawlerBorrowed<'_>,
522) -> Result<SearchResultAlbum> {
523    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
524    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
525    let album_type = parse_flex_column_item(&mut mrlir, 1, 0)?;
526
527    // Artist can comprise of multiple runs, delimited by " • ".
528    // See https://github.com/nick42d/youtui/issues/171
529    let (artist, year) = mrlir
530        .borrow_pointer(format!("{}/text/runs", flex_column_item_pointer(1)))?
531        .try_expect(
532            "album result should contain 3 string fields delimited by ' • '",
533            |flex_column_1| {
534                Ok(flex_column_1
535                    .try_iter_mut()?
536                    // First field is album_type which we parsed above, so skip it and the
537                    // delimiter.
538                    .skip(2)
539                    .map(|mut field| field.take_value_pointer::<String>("/text"))
540                    .collect::<json_crawler::CrawlerResult<String>>()?
541                    .split(" • ")
542                    .map(ToString::to_string)
543                    .collect_tuple::<(String, String)>())
544            },
545        )?;
546
547    let explicit = if mrlir.path_exists(BADGE_LABEL) {
548        Explicit::IsExplicit
549    } else {
550        Explicit::NotExplicit
551    };
552    let browse_id = mrlir.take_value_pointer(NAVIGATION_BROWSE_ID)?;
553    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
554    Ok(SearchResultAlbum {
555        artist,
556        thumbnails,
557        album_id: browse_id,
558        title,
559        year,
560        album_type,
561        explicit,
562    })
563}
564fn parse_song_search_result_from_music_shelf_contents(
565    music_shelf_contents: JsonCrawlerBorrowed<'_>,
566) -> Result<SearchResultSong> {
567    // The byline comprises multiple fields delimited by " • ".
568    // See https://github.com/nick42d/youtui/issues/171.
569    // Album field is optional. See https://github.com/nick42d/youtui/issues/174
570    /// Tuple makeup: (artist, album, duration)
571    fn parse_song_fields(
572        mrlir: &mut impl JsonCrawler,
573    ) -> json_crawler::CrawlerResult<Option<(String, Option<ParsedSongAlbum>, String)>> {
574        // NOTE: We are looping twice here, may be able to be improved.
575        let num_runs = mrlir.try_iter_mut()?.count();
576        let mut fields_vec = mrlir
577            .try_iter_mut()?
578            .map(|mut field| field.take_value_pointer::<String>("/text"))
579            .collect::<json_crawler::CrawlerResult<String>>()?
580            .rsplit(" • ")
581            .map(ToString::to_string)
582            .collect::<Vec<_>>();
583        let Some(artist) = fields_vec.pop() else {
584            return Ok(None);
585        };
586        let Some(album_or_duration) = fields_vec.pop() else {
587            return Ok(None);
588        };
589        if let Some(duration) = fields_vec.pop() {
590            let album_idx = num_runs - 3;
591            let album = ParsedSongAlbum {
592                name: album_or_duration,
593                id: mrlir.take_value_pointer(format!("/{album_idx}{NAVIGATION_BROWSE_ID}"))?,
594            };
595            return Ok(Some((artist, Some(album), duration)));
596        }
597        Ok(Some((artist, None, album_or_duration)))
598    }
599
600    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
601    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
602
603    let (artist, album, duration) = mrlir
604        .borrow_pointer(format!("{}/text/runs", flex_column_item_pointer(1)))?
605        .try_expect(
606            "Song result should contain 2 or 3 string fields delimited by ' • '",
607            parse_song_fields,
608        )?;
609
610    let plays = parse_flex_column_item(&mut mrlir, 2, 0)?;
611
612    let explicit = if mrlir.path_exists(BADGE_LABEL) {
613        Explicit::IsExplicit
614    } else {
615        Explicit::NotExplicit
616    };
617    let video_id = mrlir.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
618    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
619    Ok(SearchResultSong {
620        artist,
621        thumbnails,
622        title,
623        explicit,
624        plays,
625        album,
626        video_id,
627        duration,
628    })
629}
630// TODO: Type safety
631// TODO: Tests
632fn parse_video_search_result_from_music_shelf_contents(
633    music_shelf_contents: JsonCrawlerBorrowed<'_>,
634) -> Result<Option<SearchResultVideo>> {
635    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
636    // Handle not available case
637    if let Ok("MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT") = mrlir
638        .take_value_pointer::<String>(DISPLAY_POLICY)
639        .as_deref()
640    {
641        return Ok(None);
642    };
643    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
644    let first_field: String = parse_flex_column_item(&mut mrlir, 1, 0)?;
645    // Handle video podcasts - seems to be 2 different ways to display these.
646    match first_field.as_str() {
647        "Video" => {
648            let channel_name = parse_flex_column_item(&mut mrlir, 1, 2)?;
649            let views = parse_flex_column_item(&mut mrlir, 1, 4)?;
650            let length = parse_flex_column_item(&mut mrlir, 1, 6)?;
651            let video_id = mrlir.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
652            let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
653            Ok(Some(SearchResultVideo::Video {
654                title,
655                channel_name,
656                views,
657                length,
658                thumbnails,
659                video_id,
660            }))
661        }
662        "Episode" => {
663            //TODO: Handle live episode
664            let date = EpisodeDate::Recorded {
665                date: parse_flex_column_item(&mut mrlir, 1, 2)?,
666            };
667            let channel_name = parse_flex_column_item(&mut mrlir, 1, 4)?;
668            let video_id = mrlir.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
669            let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
670            Ok(Some(SearchResultVideo::VideoEpisode {
671                title,
672                channel_name,
673                date,
674                thumbnails,
675                episode_id: video_id,
676            }))
677        }
678        _ => {
679            // Assume that if a watch endpoint exists, it's a video.
680            if mrlir.path_exists("/flexColumns/0/musicResponsiveListItemFlexColumnRenderer/text/runs/0/navigationEndpoint/watchEndpoint") {
681
682            let views = parse_flex_column_item(&mut mrlir, 1, 2)?;
683            let length = parse_flex_column_item(&mut mrlir, 1, 4)?;
684            let video_id = mrlir.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
685            let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
686            Ok(Some(SearchResultVideo::Video {
687                            title,
688                            channel_name: first_field,
689                            views,
690                            length,
691                            thumbnails,
692                            video_id,
693                        }))
694            } else {
695            let channel_name = parse_flex_column_item(&mut mrlir, 1, 2)?;
696            let video_id = mrlir.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
697            let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
698            Ok(Some(SearchResultVideo::VideoEpisode {
699                            title,
700                            channel_name,
701                        //TODO: Handle live episode
702                            date: EpisodeDate::Recorded { date: first_field },
703                            thumbnails,
704                            episode_id: video_id,
705                        }))
706            }
707        }
708    }
709}
710// TODO: Type safety
711// TODO: Tests
712fn parse_podcast_search_result_from_music_shelf_contents(
713    music_shelf_contents: JsonCrawlerBorrowed<'_>,
714) -> Result<SearchResultPodcast> {
715    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
716    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
717    let publisher = parse_flex_column_item(&mut mrlir, 1, 0)?;
718    let podcast_id = mrlir.take_value_pointer(NAVIGATION_BROWSE_ID)?;
719    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
720    Ok(SearchResultPodcast {
721        title,
722        publisher,
723        podcast_id,
724        thumbnails,
725    })
726}
727// TODO: Type safety
728// TODO: Tests
729fn parse_episode_search_result_from_music_shelf_contents(
730    music_shelf_contents: JsonCrawlerBorrowed<'_>,
731) -> Result<SearchResultEpisode> {
732    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
733    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
734    let date = if mrlir.path_exists(LIVE_BADGE_LABEL) {
735        EpisodeDate::Live
736    } else {
737        EpisodeDate::Recorded {
738            date: parse_flex_column_item(&mut mrlir, 1, 0)?,
739        }
740    };
741    let channel_name = match date {
742        EpisodeDate::Live => parse_flex_column_item(&mut mrlir, 1, 0)?,
743        EpisodeDate::Recorded { .. } => parse_flex_column_item(&mut mrlir, 1, 2)?,
744    };
745    let video_id = mrlir.take_value_pointer(PLAYLIST_ITEM_VIDEO_ID)?;
746    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
747    Ok(SearchResultEpisode {
748        title,
749        date,
750        episode_id: video_id,
751        channel_name,
752        thumbnails,
753    })
754}
755// TODO: Type safety
756// TODO: Tests
757fn parse_featured_playlist_search_result_from_music_shelf_contents(
758    music_shelf_contents: JsonCrawlerBorrowed<'_>,
759) -> Result<SearchResultFeaturedPlaylist> {
760    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
761    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
762    let author = parse_flex_column_item(&mut mrlir, 1, 0)?;
763    let songs = parse_flex_column_item(&mut mrlir, 1, 2)?;
764    let playlist_id = mrlir.take_value_pointer(NAVIGATION_BROWSE_ID)?;
765    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
766    Ok(SearchResultFeaturedPlaylist {
767        title,
768        author,
769        playlist_id,
770        songs,
771        thumbnails,
772    })
773}
774fn parse_community_playlist_basic_search_result_from_music_shelf_contents(
775    music_shelf_contents: JsonCrawlerBorrowed<'_>,
776) -> Result<BasicSearchResultCommunityPlaylist> {
777    let result_type: YoutubeMusicPageType = music_shelf_contents
778        .borrow_value_pointer(concatcp!(MRLIR, NAVIGATION_BROWSE, PAGE_TYPE))?;
779    let result = match result_type {
780        YoutubeMusicPageType::Podcast => BasicSearchResultCommunityPlaylist::Podcast(
781            parse_podcast_search_result_from_music_shelf_contents(music_shelf_contents)?,
782        ),
783        YoutubeMusicPageType::Playlist => BasicSearchResultCommunityPlaylist::Playlist(
784            parse_community_playlist_search_result_from_music_shelf_contents(music_shelf_contents)?,
785        ),
786    };
787    Ok(result)
788}
789fn parse_community_playlist_search_result_from_music_shelf_contents(
790    music_shelf_contents: JsonCrawlerBorrowed<'_>,
791) -> Result<SearchResultCommunityPlaylist> {
792    let mut mrlir = music_shelf_contents.navigate_pointer("/musicResponsiveListItemRenderer")?;
793    let title = parse_flex_column_item(&mut mrlir, 0, 0)?;
794    let author = parse_flex_column_item(&mut mrlir, 1, 0)?;
795    let views = parse_flex_column_item(&mut mrlir, 1, 2)?;
796    let playlist_id = mrlir.take_value_pointer(NAVIGATION_BROWSE_ID)?;
797    let thumbnails: Vec<Thumbnail> = mrlir.take_value_pointer(THUMBNAILS)?;
798    Ok(SearchResultCommunityPlaylist {
799        title,
800        author,
801        playlist_id,
802        views,
803        thumbnails,
804    })
805}
806
807fn parse_playlist_search_result_from_music_shelf_contents(
808    mut music_shelf_contents: JsonCrawlerBorrowed<'_>,
809) -> Result<SearchResultPlaylist> {
810    let result_type: YoutubeMusicPageType = music_shelf_contents
811        .borrow_value_pointer(concatcp!(MRLIR, NAVIGATION_BROWSE, PAGE_TYPE))?;
812
813    // Search result for this query can be Podcast or Playlist.
814    match result_type {
815        YoutubeMusicPageType::Podcast => {
816            let res = parse_podcast_search_result_from_music_shelf_contents(music_shelf_contents)?;
817            Ok(SearchResultPlaylist::Podcast(res))
818        }
819        YoutubeMusicPageType::Playlist => {
820            // The playlist search contains a mix of Community and Featured playlists.
821            let playlist_params: PlaylistEndpointParams =
822                music_shelf_contents.take_value_pointer(concatcp!(
823                    MRLIR,
824                    PLAY_BUTTON,
825                    "/playNavigationEndpoint/watchPlaylistEndpoint/params"
826                ))?;
827            let playlist = match playlist_params {
828                PlaylistEndpointParams::Featured => {
829                    let res = parse_featured_playlist_search_result_from_music_shelf_contents(
830                        music_shelf_contents,
831                    )?;
832                    SearchResultPlaylist::Featured(res)
833                }
834                PlaylistEndpointParams::Community => {
835                    let res = parse_community_playlist_search_result_from_music_shelf_contents(
836                        music_shelf_contents,
837                    )?;
838                    SearchResultPlaylist::Community(res)
839                }
840            };
841            Ok(playlist)
842        }
843    }
844}
845
846struct FilteredSearchSectionContents(JsonCrawlerOwned);
847struct FilteredSearchMusicShelfContents(JsonCrawlerOwned);
848struct BasicSearchSectionListContents(JsonCrawlerOwned);
849// In this case, we've searched and had no results found.
850// We are being quite explicit here to avoid a false positive.
851// See tests for an example.
852// TODO: Test this function itself.
853fn section_contents_is_empty(section_contents: &mut FilteredSearchSectionContents) -> Result<bool> {
854    Ok(section_contents
855        .0
856        .try_iter_mut()?
857        .any(|item| item.path_exists("/itemSectionRenderer/contents/0/didYouMeanRenderer")))
858}
859
860fn take_continuation_params_from_section_contents(
861    section_contents: &mut FilteredSearchSectionContents,
862) -> Result<Option<ContinuationParams<'static>>> {
863    section_contents
864        .0
865        .try_iter_mut()
866        .and_then(|contents| contents.find_path(concatcp!(MUSIC_SHELF, CONTINUATION_PARAMS)))
867        .map(|mut continuation_params| continuation_params.take_value())
868        .ok()
869        .transpose()
870        .map_err(Into::into)
871}
872fn get_filtered_search_continuation_music_shelf_contents_and_params(
873    crawler: JsonCrawlerOwned,
874) -> Result<(
875    FilteredSearchMusicShelfContents,
876    Option<ContinuationParams<'static>>,
877)> {
878    let mut music_shelf = crawler.navigate_pointer(MUSIC_SHELF_CONTINUATION)?;
879    let continuation_params = music_shelf.take_value_pointer(CONTINUATION_PARAMS).ok();
880    let contents = music_shelf.navigate_pointer("/contents")?;
881    Ok((
882        FilteredSearchMusicShelfContents(contents),
883        continuation_params,
884    ))
885}
886// TODO: Consolidate these two functions into single function.
887// TODO: This could be implemented with a non-mutable array also.
888fn section_list_contents_is_empty(
889    section_contents: &mut BasicSearchSectionListContents,
890) -> Result<bool> {
891    let is_empty = section_contents
892        .0
893        .try_iter_mut()?
894        .filter(|item| item.path_exists(MUSIC_CARD_SHELF) || item.path_exists(MUSIC_SHELF))
895        .count()
896        == 0;
897    Ok(is_empty)
898}
899impl<'a, S: UnfilteredSearchType> TryFrom<ProcessedResult<'a, SearchQuery<'a, S>>>
900    for BasicSearchSectionListContents
901{
902    type Error = Error;
903    fn try_from(value: ProcessedResult<SearchQuery<'a, S>>) -> Result<Self> {
904        let json_crawler: JsonCrawlerOwned = value.into();
905        let section_list_contents = json_crawler.navigate_pointer(concatcp!(
906            "/contents/tabbedSearchResultsRenderer",
907            TAB_CONTENT,
908            SECTION_LIST
909        ))?;
910        Ok(BasicSearchSectionListContents(section_list_contents))
911    }
912}
913impl<'a, F: FilteredSearchType> TryFrom<ProcessedResult<'a, SearchQuery<'a, FilteredSearch<F>>>>
914    for FilteredSearchSectionContents
915{
916    type Error = Error;
917    fn try_from(value: ProcessedResult<SearchQuery<'a, FilteredSearch<F>>>) -> Result<Self> {
918        let json_crawler: JsonCrawlerOwned = value.into();
919        let section_contents = json_crawler.navigate_pointer(concatcp!(
920            "/contents/tabbedSearchResultsRenderer",
921            TAB_CONTENT,
922            SECTION_LIST,
923        ))?;
924        Ok(FilteredSearchSectionContents(section_contents))
925    }
926}
927impl TryFrom<FilteredSearchSectionContents> for FilteredSearchMusicShelfContents {
928    type Error = Error;
929    fn try_from(
930        value: FilteredSearchSectionContents,
931    ) -> std::prelude::v1::Result<Self, Self::Error> {
932        let music_shelf_contents = value
933            .0
934            .try_into_iter()?
935            .find_path(concatcp!(MUSIC_SHELF, "/contents"))?;
936        Ok(FilteredSearchMusicShelfContents(music_shelf_contents))
937    }
938}
939impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultAlbum> {
940    type Error = Error;
941    fn try_from(
942        mut value: FilteredSearchMusicShelfContents,
943    ) -> std::prelude::v1::Result<Self, Self::Error> {
944        // TODO: Make this a From method.
945        value
946            .0
947            .try_iter_mut()?
948            .map(|a| parse_album_search_result_from_music_shelf_contents(a))
949            .collect()
950    }
951}
952impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultProfile> {
953    type Error = Error;
954    fn try_from(
955        mut value: FilteredSearchMusicShelfContents,
956    ) -> std::prelude::v1::Result<Self, Self::Error> {
957        // TODO: Make this a From method.
958        value
959            .0
960            .try_iter_mut()?
961            .map(|a| parse_profile_search_result_from_music_shelf_contents(a))
962            .collect()
963    }
964}
965impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultArtist> {
966    type Error = Error;
967    fn try_from(
968        mut value: FilteredSearchMusicShelfContents,
969    ) -> std::prelude::v1::Result<Self, Self::Error> {
970        // TODO: Make this a From method.
971        value
972            .0
973            .try_iter_mut()?
974            .map(|a| parse_artist_search_result_from_music_shelf_contents(a))
975            .collect()
976    }
977}
978impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultSong> {
979    type Error = Error;
980    fn try_from(
981        mut value: FilteredSearchMusicShelfContents,
982    ) -> std::prelude::v1::Result<Self, Self::Error> {
983        // TODO: Make this a From method.
984        value
985            .0
986            .try_iter_mut()?
987            .map(|a| parse_song_search_result_from_music_shelf_contents(a))
988            .collect()
989    }
990}
991impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultVideo> {
992    type Error = Error;
993    fn try_from(
994        mut value: FilteredSearchMusicShelfContents,
995    ) -> std::prelude::v1::Result<Self, Self::Error> {
996        // TODO: Make this a From method.
997        value
998            .0
999            .try_iter_mut()?
1000            .filter_map(|a| parse_video_search_result_from_music_shelf_contents(a).transpose())
1001            .collect()
1002    }
1003}
1004impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultEpisode> {
1005    type Error = Error;
1006    fn try_from(
1007        mut value: FilteredSearchMusicShelfContents,
1008    ) -> std::prelude::v1::Result<Self, Self::Error> {
1009        // TODO: Make this a From method.
1010        value
1011            .0
1012            .try_iter_mut()?
1013            .map(|a| parse_episode_search_result_from_music_shelf_contents(a))
1014            .collect()
1015    }
1016}
1017impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultPodcast> {
1018    type Error = Error;
1019    fn try_from(
1020        mut value: FilteredSearchMusicShelfContents,
1021    ) -> std::prelude::v1::Result<Self, Self::Error> {
1022        // TODO: Make this a From method.
1023        value
1024            .0
1025            .try_iter_mut()?
1026            .map(|a| parse_podcast_search_result_from_music_shelf_contents(a))
1027            .collect()
1028    }
1029}
1030impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultPlaylist> {
1031    type Error = Error;
1032    fn try_from(
1033        mut value: FilteredSearchMusicShelfContents,
1034    ) -> std::prelude::v1::Result<Self, Self::Error> {
1035        // TODO: Make this a From method.
1036        value
1037            .0
1038            .try_iter_mut()?
1039            .map(|a| parse_playlist_search_result_from_music_shelf_contents(a))
1040            .collect()
1041    }
1042}
1043impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultCommunityPlaylist> {
1044    type Error = Error;
1045    fn try_from(
1046        mut value: FilteredSearchMusicShelfContents,
1047    ) -> std::prelude::v1::Result<Self, Self::Error> {
1048        // TODO: Make this a From method.
1049        value
1050            .0
1051            .try_iter_mut()?
1052            .map(|a| parse_community_playlist_search_result_from_music_shelf_contents(a))
1053            .collect()
1054    }
1055}
1056impl TryFrom<FilteredSearchMusicShelfContents> for Vec<SearchResultFeaturedPlaylist> {
1057    type Error = Error;
1058    fn try_from(
1059        mut value: FilteredSearchMusicShelfContents,
1060    ) -> std::prelude::v1::Result<Self, Self::Error> {
1061        // TODO: Make this a From method.
1062        value
1063            .0
1064            .try_iter_mut()?
1065            .map(|a| parse_featured_playlist_search_result_from_music_shelf_contents(a))
1066            .collect()
1067    }
1068}
1069impl<'a, S: UnfilteredSearchType> ParseFrom<SearchQuery<'a, S>> for SearchResults {
1070    fn parse_from(p: ProcessedResult<SearchQuery<'a, S>>) -> crate::Result<Self> {
1071        let mut section_list_contents = BasicSearchSectionListContents::try_from(p)?;
1072        if section_list_contents_is_empty(&mut section_list_contents)? {
1073            return Ok(Self::default());
1074        }
1075        parse_basic_search_result_from_section_list_contents(section_list_contents)
1076    }
1077}
1078
1079impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<ArtistsFilter>>>
1080    for Vec<SearchResultArtist>
1081{
1082    fn parse_from_continuable(
1083        p: ProcessedResult<SearchQuery<'a, FilteredSearch<ArtistsFilter>>>,
1084    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1085        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1086        if section_contents_is_empty(&mut section_contents)? {
1087            return Ok((Vec::new(), None));
1088        }
1089        let continuation_params =
1090            take_continuation_params_from_section_contents(&mut section_contents)?;
1091        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1092        Ok((results, continuation_params))
1093    }
1094    fn parse_continuation(
1095        p: ProcessedResult<
1096            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<ArtistsFilter>>>,
1097        >,
1098    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1099        let crawler: JsonCrawlerOwned = p.into();
1100        let (contents, continuation_params) =
1101            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1102        let results = contents.try_into()?;
1103        Ok((results, continuation_params))
1104    }
1105}
1106impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<ProfilesFilter>>>
1107    for Vec<SearchResultProfile>
1108{
1109    fn parse_from_continuable(
1110        p: ProcessedResult<SearchQuery<'a, FilteredSearch<ProfilesFilter>>>,
1111    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1112        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1113        if section_contents_is_empty(&mut section_contents)? {
1114            return Ok((Vec::new(), None));
1115        }
1116        let continuation_params =
1117            take_continuation_params_from_section_contents(&mut section_contents)?;
1118        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1119        Ok((results, continuation_params))
1120    }
1121    fn parse_continuation(
1122        p: ProcessedResult<
1123            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<ProfilesFilter>>>,
1124        >,
1125    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1126        let crawler: JsonCrawlerOwned = p.into();
1127        let (contents, continuation_params) =
1128            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1129        let results = contents.try_into()?;
1130        Ok((results, continuation_params))
1131    }
1132}
1133impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<AlbumsFilter>>>
1134    for Vec<SearchResultAlbum>
1135{
1136    fn parse_from_continuable(
1137        p: ProcessedResult<SearchQuery<'a, FilteredSearch<AlbumsFilter>>>,
1138    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1139        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1140        if section_contents_is_empty(&mut section_contents)? {
1141            return Ok((Vec::new(), None));
1142        }
1143        let continuation_params =
1144            take_continuation_params_from_section_contents(&mut section_contents)?;
1145        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1146        Ok((results, continuation_params))
1147    }
1148    fn parse_continuation(
1149        p: ProcessedResult<
1150            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<AlbumsFilter>>>,
1151        >,
1152    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1153        let crawler: JsonCrawlerOwned = p.into();
1154        let (contents, continuation_params) =
1155            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1156        let results = contents.try_into()?;
1157        Ok((results, continuation_params))
1158    }
1159}
1160impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<SongsFilter>>>
1161    for Vec<SearchResultSong>
1162{
1163    fn parse_from_continuable(
1164        p: ProcessedResult<SearchQuery<'a, FilteredSearch<SongsFilter>>>,
1165    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1166        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1167        if section_contents_is_empty(&mut section_contents)? {
1168            return Ok((Vec::new(), None));
1169        }
1170        let continuation_params =
1171            take_continuation_params_from_section_contents(&mut section_contents)?;
1172        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1173        Ok((results, continuation_params))
1174    }
1175    fn parse_continuation(
1176        p: ProcessedResult<GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<SongsFilter>>>>,
1177    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1178        let crawler: JsonCrawlerOwned = p.into();
1179        let (contents, continuation_params) =
1180            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1181        let results = contents.try_into()?;
1182        Ok((results, continuation_params))
1183    }
1184}
1185impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<VideosFilter>>>
1186    for Vec<SearchResultVideo>
1187{
1188    fn parse_from_continuable(
1189        p: ProcessedResult<SearchQuery<'a, FilteredSearch<VideosFilter>>>,
1190    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1191        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1192        if section_contents_is_empty(&mut section_contents)? {
1193            return Ok((Vec::new(), None));
1194        }
1195        let continuation_params =
1196            take_continuation_params_from_section_contents(&mut section_contents)?;
1197        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1198        Ok((results, continuation_params))
1199    }
1200    fn parse_continuation(
1201        p: ProcessedResult<
1202            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<VideosFilter>>>,
1203        >,
1204    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1205        let crawler: JsonCrawlerOwned = p.into();
1206        let (contents, continuation_params) =
1207            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1208        let results = contents.try_into()?;
1209        Ok((results, continuation_params))
1210    }
1211}
1212impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<EpisodesFilter>>>
1213    for Vec<SearchResultEpisode>
1214{
1215    fn parse_from_continuable(
1216        p: ProcessedResult<SearchQuery<'a, FilteredSearch<EpisodesFilter>>>,
1217    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1218        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1219        if section_contents_is_empty(&mut section_contents)? {
1220            return Ok((Vec::new(), None));
1221        }
1222        let continuation_params =
1223            take_continuation_params_from_section_contents(&mut section_contents)?;
1224        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1225        Ok((results, continuation_params))
1226    }
1227    fn parse_continuation(
1228        p: ProcessedResult<
1229            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<EpisodesFilter>>>,
1230        >,
1231    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1232        let crawler: JsonCrawlerOwned = p.into();
1233        let (contents, continuation_params) =
1234            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1235        let results = contents.try_into()?;
1236        Ok((results, continuation_params))
1237    }
1238}
1239impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<PodcastsFilter>>>
1240    for Vec<SearchResultPodcast>
1241{
1242    fn parse_from_continuable(
1243        p: ProcessedResult<SearchQuery<'a, FilteredSearch<PodcastsFilter>>>,
1244    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1245        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1246        if section_contents_is_empty(&mut section_contents)? {
1247            return Ok((Vec::new(), None));
1248        }
1249        let continuation_params =
1250            take_continuation_params_from_section_contents(&mut section_contents)?;
1251        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1252        Ok((results, continuation_params))
1253    }
1254    fn parse_continuation(
1255        p: ProcessedResult<
1256            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<PodcastsFilter>>>,
1257        >,
1258    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1259        let crawler: JsonCrawlerOwned = p.into();
1260        let (contents, continuation_params) =
1261            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1262        let results = contents.try_into()?;
1263        Ok((results, continuation_params))
1264    }
1265}
1266impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<CommunityPlaylistsFilter>>>
1267    for Vec<SearchResultPlaylist>
1268{
1269    fn parse_from_continuable(
1270        p: ProcessedResult<SearchQuery<'a, FilteredSearch<CommunityPlaylistsFilter>>>,
1271    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1272        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1273        if section_contents_is_empty(&mut section_contents)? {
1274            return Ok((Vec::new(), None));
1275        }
1276        let continuation_params =
1277            take_continuation_params_from_section_contents(&mut section_contents)?;
1278        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1279        Ok((results, continuation_params))
1280    }
1281    fn parse_continuation(
1282        p: ProcessedResult<
1283            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<CommunityPlaylistsFilter>>>,
1284        >,
1285    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1286        let crawler: JsonCrawlerOwned = p.into();
1287        let (contents, continuation_params) =
1288            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1289        let results = contents.try_into()?;
1290        Ok((results, continuation_params))
1291    }
1292}
1293impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<FeaturedPlaylistsFilter>>>
1294    for Vec<SearchResultFeaturedPlaylist>
1295{
1296    fn parse_from_continuable(
1297        p: ProcessedResult<SearchQuery<'a, FilteredSearch<FeaturedPlaylistsFilter>>>,
1298    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1299        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1300        if section_contents_is_empty(&mut section_contents)? {
1301            return Ok((Vec::new(), None));
1302        }
1303        let continuation_params =
1304            take_continuation_params_from_section_contents(&mut section_contents)?;
1305        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1306        Ok((results, continuation_params))
1307    }
1308    fn parse_continuation(
1309        p: ProcessedResult<
1310            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<FeaturedPlaylistsFilter>>>,
1311        >,
1312    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1313        let crawler: JsonCrawlerOwned = p.into();
1314        let (contents, continuation_params) =
1315            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1316        let results = contents.try_into()?;
1317        Ok((results, continuation_params))
1318    }
1319}
1320impl<'a> ParseFromContinuable<SearchQuery<'a, FilteredSearch<PlaylistsFilter>>>
1321    for Vec<SearchResultPlaylist>
1322{
1323    fn parse_from_continuable(
1324        p: ProcessedResult<SearchQuery<'a, FilteredSearch<PlaylistsFilter>>>,
1325    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1326        let mut section_contents = FilteredSearchSectionContents::try_from(p)?;
1327        if section_contents_is_empty(&mut section_contents)? {
1328            return Ok((Vec::new(), None));
1329        }
1330        let continuation_params =
1331            take_continuation_params_from_section_contents(&mut section_contents)?;
1332        let results = FilteredSearchMusicShelfContents::try_from(section_contents)?.try_into()?;
1333        Ok((results, continuation_params))
1334    }
1335    fn parse_continuation(
1336        p: ProcessedResult<
1337            GetContinuationsQuery<'_, SearchQuery<'a, FilteredSearch<PlaylistsFilter>>>,
1338        >,
1339    ) -> crate::Result<(Self, Option<crate::common::ContinuationParams<'static>>)> {
1340        let crawler: JsonCrawlerOwned = p.into();
1341        let (contents, continuation_params) =
1342            get_filtered_search_continuation_music_shelf_contents_and_params(crawler)?;
1343        let results = contents.try_into()?;
1344        Ok((results, continuation_params))
1345    }
1346}
1347
1348impl<'a> ParseFrom<GetSearchSuggestionsQuery<'a>> for Vec<SearchSuggestion> {
1349    fn parse_from(p: ProcessedResult<GetSearchSuggestionsQuery<'a>>) -> crate::Result<Self> {
1350        let json_crawler: JsonCrawlerOwned = p.into();
1351        let mut suggestions = json_crawler
1352            .navigate_pointer("/contents/0/searchSuggestionsSectionRenderer/contents")?;
1353        let mut results = Vec::new();
1354        for mut s in suggestions.try_iter_mut()? {
1355            let mut runs = Vec::new();
1356            if let Ok(mut search_suggestion) =
1357                s.borrow_pointer("/searchSuggestionRenderer/suggestion/runs")
1358            {
1359                for mut r in search_suggestion.try_iter_mut()? {
1360                    if let Ok(true) = r.take_value_pointer("/bold") {
1361                        runs.push(r.take_value_pointer("/text").map(TextRun::Bold)?)
1362                    } else {
1363                        runs.push(r.take_value_pointer("/text").map(TextRun::Normal)?)
1364                    }
1365                }
1366                results.push(SearchSuggestion::new(SuggestionType::Prediction, runs))
1367            } else {
1368                for mut r in s
1369                    .borrow_pointer("/historySuggestionRenderer/suggestion/runs")?
1370                    .try_iter_mut()?
1371                {
1372                    if let Ok(true) = r.take_value_pointer("/bold") {
1373                        runs.push(r.take_value_pointer("/text").map(TextRun::Bold)?)
1374                    } else {
1375                        runs.push(r.take_value_pointer("/text").map(TextRun::Normal)?)
1376                    }
1377                }
1378                results.push(SearchSuggestion::new(SuggestionType::History, runs))
1379            }
1380        }
1381        Ok(results)
1382    }
1383}