ytmapi_rs/parse/
search.rs

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