use serde::Deserialize; use serde_with::{ rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError, }; use time::OffsetDateTime; use super::{ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, Thumbnails}; use crate::{ model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem}, param::Language, serializer::{ text::{AttributedText, Text, TextComponent}, MapResult, }, util::{self, timeago, TryRemove}, }; #[cfg(feature = "userdata")] use crate::{client::response::SimpleHeaderRenderer, model::HistoryItem}; #[cfg(feature = "userdata")] use time::UtcOffset; #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) enum YouTubeListItem { #[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")] VideoRenderer(VideoRenderer), ReelItemRenderer(ReelItemRenderer), ShortsLockupViewModel(ShortsLockupViewModel), PlaylistVideoRenderer(PlaylistVideoRenderer), #[serde(alias = "gridPlaylistRenderer")] PlaylistRenderer(PlaylistRenderer), ChannelRenderer(ChannelRenderer), LockupViewModel(LockupViewModel), /// Continauation items are located at the end of a list /// and contain the continuation token for progressive loading #[serde(rename_all = "camelCase")] ContinuationItemRenderer { continuation_endpoint: ContinuationEndpoint, }, /// Corrected search query #[serde(rename_all = "camelCase")] ShowingResultsForRenderer { #[serde_as(as = "Text")] corrected_query: String, }, /// Contains video on startpage /// /// Seems to be currently A/B tested on the channel page, /// as of 11.10.2022 #[serde(alias = "shelfRenderer")] RichItemRenderer { content: Box, }, /// Contains search results /// /// Seems to be currently A/B tested on the video details page, /// as of 11.10.2022 /// /// GridRenderer: contains videos on channel page #[serde(alias = "expandedShelfContentsRenderer", alias = "gridRenderer")] ItemSectionRenderer { #[cfg(feature = "userdata")] header: Option, #[serde(alias = "items")] contents: MapResult>, }, /// Age-restricted channel #[serde(rename_all = "camelCase")] ChannelAgeGateRenderer { channel_title: String, #[serde_as(as = "Text")] main_text: String, }, /// No video list item (e.g. ad) or unimplemented item /// /// Unimplemented: /// - compactPlaylistRenderer (recommended playlists) /// - compactRadioRenderer (recommended mix) #[serde(other, deserialize_with = "deserialize_ignore_any")] None, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct VideoRenderer { pub video_id: String, pub thumbnail: Thumbnails, #[serde_as(as = "Text")] pub title: String, #[serde(rename = "shortBylineText")] pub channel: Option, pub channel_thumbnail: Option, pub channel_thumbnail_supported_renderers: Option, #[serde_as(as = "Option")] pub published_time_text: Option, #[serde_as(as = "Option")] pub length_text: Option, /// Contains `No views` if the view count is zero #[serde_as(as = "Option")] pub view_count_text: Option, /// Channel verification badge #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub owner_badges: Vec, /// Contains live tag for recommended videos #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub badges: Vec, /// Contains Short/Live tag #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub thumbnail_overlays: Vec, /// Abbreviated video description (on startpage) #[serde_as(as = "Option")] pub description_snippet: Option, /// Contains abbreviated video description (on search page) #[serde_as(as = "Option>")] pub detailed_metadata_snippets: Option>, /// Release date for upcoming videos pub upcoming_event_data: Option, } /// Short video item #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelItemRenderer { pub video_id: String, pub thumbnail: Thumbnails, #[serde_as(as = "Text")] pub headline: String, /// Contains `No views` if the view count is zero #[serde_as(as = "Option")] pub view_count_text: Option, #[serde(default)] #[serde_as(as = "DefaultOnError")] pub navigation_endpoint: Option, } // New short video item #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ShortsLockupViewModel { /// `shorts-shelf-item-[video_id]` pub entity_id: String, pub thumbnail: Thumbnails, pub overlay_metadata: ShortsOverlayMetadata, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ShortsOverlayMetadata { /// Title #[serde_as(as = "AttributedText")] pub primary_text: String, /// View count #[serde_as(as = "Option")] pub secondary_text: Option, } /// Generalized list item, currently only used for channel playlists and YTM items #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct LockupViewModel { pub content_id: String, #[serde(default)] #[serde_as(deserialize_as = "DefaultOnError")] pub content_type: LockupContentType, pub content_image: ContentImage, pub metadata: LockupViewModelMetadata, } #[derive(Default, Debug, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[allow(clippy::enum_variant_names)] pub(crate) enum LockupContentType { LockupContentTypePlaylist, LockupContentTypeVideo, #[default] Unknown, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct LockupViewModelMetadata { pub lockup_metadata_view_model: LockupViewModelMetadataInner, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct LockupViewModelMetadataInner { #[serde_as(as = "AttributedText")] pub title: String, pub metadata: PhMetadataView, } /// Video displayed in a playlist #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PlaylistVideoRenderer { pub video_id: String, pub thumbnail: Thumbnails, #[serde_as(as = "Text")] pub title: String, #[serde(rename = "shortBylineText")] pub channel: TextComponent, #[serde_as(as = "Option")] pub length_seconds: Option, /// Regular video: `["29K views", " • ", "13 years ago"]` /// Livestream: `["66K", " watching"]` /// Upcoming: `["8", " waiting"]` #[serde(default)] #[serde_as(as = "DefaultOnError")] pub video_info: Vec, /// Contains Short/Live tag #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub thumbnail_overlays: Vec, /// Release date for upcoming videos pub upcoming_event_data: Option, } /// Playlist displayed in search results #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PlaylistRenderer { pub playlist_id: String, #[serde_as(as = "Text")] pub title: String, pub thumbnail: Option, /// Used by playlists from search page /// /// The first item of this list contains the playlist thumbnail, /// subsequent items contain very small thumbnails of the next playlist videos pub thumbnails: Option>, #[serde_as(as = "Option")] pub video_count: Option, #[serde_as(as = "Option")] pub video_count_short_text: Option, #[serde(rename = "shortBylineText")] pub channel: Option, /// Channel verification badge #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub owner_badges: Vec, } /// Channel displayed in search results #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChannelRenderer { pub channel_id: String, #[serde_as(as = "Text")] pub title: String, pub thumbnail: Thumbnails, /// Abbreviated channel description /// /// Not present if the channel has no description #[serde(default)] #[serde_as(as = "Text")] pub description_snippet: String, /// Not present if the channel has no videos #[serde_as(as = "Option")] pub video_count_text: Option, #[serde_as(as = "Option")] pub subscriber_count_text: Option, /// Channel verification badge #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub owner_badges: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct YouTubeListRendererWrap { #[serde(alias = "richGridRenderer")] pub section_list_renderer: YouTubeListRenderer, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct YouTubeListRenderer { pub contents: MapResult>, } #[cfg(feature = "userdata")] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ItemSectionHeader { pub item_section_header_renderer: SimpleHeaderRenderer, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct UpcomingEventData { /// Unixtime in seconds #[serde_as(as = "DisplayFromStr")] pub start_time: i64, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct TimeOverlay { pub thumbnail_overlay_time_status_renderer: TimeOverlayRenderer, } /// Badges are displayed on the video thumbnail and /// show certain video properties (e.g. active livestream) #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct VideoBadge { pub metadata_badge_renderer: VideoBadgeRenderer, } /// Badges are displayed on the video thumbnail and /// show certain video properties (e.g. active livestream) #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct VideoBadgeRenderer { pub style: VideoBadgeStyle, } #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub(crate) enum VideoBadgeStyle { /// Active livestream BadgeStyleTypeLiveNow, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct TimeOverlayRenderer { /// `29:54` /// /// Is `LIVE` in case of a livestream and `SHORTS` in case of a short video #[serde_as(as = "Text")] pub text: String, #[serde(default)] #[serde_as(deserialize_as = "DefaultOnError")] pub style: TimeOverlayStyle, } #[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub(crate) enum TimeOverlayStyle { #[default] Default, Live, Shorts, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct DetailedMetadataSnippet { #[serde_as(as = "Text")] pub snippet_text: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChannelThumbnailSupportedRenderers { pub channel_thumbnail_with_link_renderer: ChannelThumbnailWithLinkRenderer, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChannelThumbnailWithLinkRenderer { pub thumbnail: Thumbnails, } /// Short video item navigation endpoint (contains upload date) #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelNavigationEndpoint { pub reel_watch_endpoint: ReelWatchEndpoint, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelWatchEndpoint { pub overlay: ReelPlayerOverlay, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelPlayerOverlay { pub reel_player_overlay_renderer: ReelPlayerOverlayRenderer, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelPlayerOverlayRenderer { pub reel_player_header_supported_renderers: ReelPlayerHeaderRenderers, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelPlayerHeaderRenderers { pub reel_player_header_renderer: ReelPlayerHeaderRenderer, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ReelPlayerHeaderRenderer { #[serde_as(as = "Text")] pub timestamp_text: String, } trait IsLive { fn is_live(&self) -> bool; } trait IsShort { fn is_short(&self) -> bool; } impl IsLive for Vec { fn is_live(&self) -> bool { self.iter().any(|badge| { badge.metadata_badge_renderer.style == VideoBadgeStyle::BadgeStyleTypeLiveNow }) } } impl IsLive for Vec { fn is_live(&self) -> bool { self.iter().any(|overlay| { overlay.thumbnail_overlay_time_status_renderer.style == TimeOverlayStyle::Live }) } } impl IsShort for Vec { fn is_short(&self) -> bool { self.iter().any(|overlay| { overlay.thumbnail_overlay_time_status_renderer.style == TimeOverlayStyle::Shorts }) } } /// Result of mapping a list of different YouTube enities /// (videos, channels, playlists) #[derive(Debug)] pub(crate) struct YouTubeListMapper { lang: Language, channel: Option, pub items: Vec, pub warnings: Vec, pub ctoken: Option, pub corrected_query: Option, } impl YouTubeListMapper { pub fn new(lang: Language) -> Self { Self { lang, channel: None, items: Vec::new(), warnings: Vec::new(), ctoken: None, corrected_query: None, } } pub fn with_channel(lang: Language, channel: &Channel, warnings: Vec) -> Self { Self { lang, channel: Some(ChannelTag { id: channel.id.clone(), name: channel.name.clone(), avatar: Vec::new(), verification: channel.verification, subscriber_count: channel.subscriber_count, }), items: Vec::new(), warnings, ctoken: None, corrected_query: None, } } fn map_video(&mut self, video: VideoRenderer) -> VideoItem { let is_live = video.thumbnail_overlays.is_live() || video.badges.is_live(); let is_short = video.thumbnail_overlays.is_short(); let length_text = video.length_text.or_else(|| { video .thumbnail_overlays .into_iter() .find(|ol| { ol.thumbnail_overlay_time_status_renderer.style == TimeOverlayStyle::Default }) .map(|ol| ol.thumbnail_overlay_time_status_renderer.text) }); VideoItem { id: video.video_id, name: video.title, duration: length_text.and_then(|txt| util::parse_video_length(&txt)), thumbnail: video.thumbnail.into(), channel: video .channel .and_then(|c| ChannelTag::try_from(c).ok()) .map(|mut c| { c.avatar = video .channel_thumbnail_supported_renderers .map(|tn| tn.channel_thumbnail_with_link_renderer.thumbnail) .or(video.channel_thumbnail) .unwrap_or_default() .into(); if !c.verification.verified() { c.verification = video.owner_badges.into(); } c }) .or_else(|| self.channel.clone()), publish_date: video .upcoming_event_data .as_ref() .and_then(|upc| OffsetDateTime::from_unix_timestamp(upc.start_time).ok()) .or_else(|| { video.published_time_text.as_ref().and_then(|txt| { timeago::parse_timeago_dt_or_warn(self.lang, txt, &mut self.warnings) }) }), publish_date_txt: video.published_time_text, view_count: video .view_count_text .map(|txt| util::parse_numeric(&txt).unwrap_or_default()), is_live, is_short, is_upcoming: video.upcoming_event_data.is_some(), short_description: video .detailed_metadata_snippets .and_then(|snippets| snippets.into_iter().next().map(|s| s.snippet_text)) .or(video.description_snippet), } } fn map_short_video(&mut self, video: ReelItemRenderer) -> VideoItem { let pub_date_txt = video.navigation_endpoint.map(|n| { n.reel_watch_endpoint .overlay .reel_player_overlay_renderer .reel_player_header_supported_renderers .reel_player_header_renderer .timestamp_text }); VideoItem { id: video.video_id, name: video.headline, duration: None, thumbnail: video.thumbnail.into(), channel: self.channel.clone(), publish_date: pub_date_txt.as_ref().and_then(|txt| { timeago::parse_timeago_dt_or_warn(self.lang, txt, &mut self.warnings) }), publish_date_txt: pub_date_txt, view_count: video.view_count_text.and_then(|txt| { util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings) }), is_live: false, is_short: true, is_upcoming: false, short_description: None, } } fn map_short_video2(&mut self, video: ShortsLockupViewModel) -> Option { if let Some(video_id) = video.entity_id.strip_prefix("shorts-shelf-item-") { Some(VideoItem { id: video_id.to_owned(), name: video.overlay_metadata.primary_text, duration: None, thumbnail: video.thumbnail.into(), channel: self.channel.clone(), publish_date: None, publish_date_txt: None, view_count: video.overlay_metadata.secondary_text.and_then(|txt| { util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings) }), is_live: false, is_short: true, is_upcoming: false, short_description: None, }) } else { self.warnings .push(format!("invalid shorts entityId: {}", video.entity_id)); None } } fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem { let channel = ChannelTag::try_from(video.channel).ok(); let mut video_info = video.video_info.into_iter(); let video_info1 = video_info .next() .map(|s| match video_info.next().as_deref() { None | Some(util::DOT_SEPARATOR) => s, Some(s2) => s + s2, }); let video_info2 = video_info.next(); // RU: "7 лет назад" " • " "210 млн просмотров" (order flipped) let (view_count_txt, publish_date_txt) = if self.lang == Language::Ru && video_info2.is_some() { (video_info2, video_info1) } else { (video_info1, video_info2) }; let is_live = video.thumbnail_overlays.is_live(); let publish_date = video .upcoming_event_data .as_ref() .and_then(|upc| OffsetDateTime::from_unix_timestamp(upc.start_time).ok()) .or_else(|| { if is_live { None } else { publish_date_txt.as_ref().and_then(|txt| { timeago::parse_timeago_dt_or_warn(self.lang, txt, &mut self.warnings) }) } }); VideoItem { id: video.video_id, name: video.title, duration: video.length_seconds, thumbnail: video.thumbnail.into(), channel, publish_date, publish_date_txt, view_count: view_count_txt.and_then(|txt| { util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings) }), is_live, is_short: video.thumbnail_overlays.is_short(), is_upcoming: video.upcoming_event_data.is_some(), short_description: None, } } fn map_playlist(&self, playlist: PlaylistRenderer) -> PlaylistItem { PlaylistItem { id: playlist.playlist_id, name: playlist.title, thumbnail: playlist .thumbnail .or_else(|| playlist.thumbnails.and_then(|mut t| t.try_swap_remove(0))) .unwrap_or_default() .into(), channel: playlist .channel .and_then(|c| ChannelTag::try_from(c).ok()) .map(|mut c| { if !c.verification.verified() { c.verification = playlist.owner_badges.into(); } c }) .or_else(|| self.channel.clone()), video_count: playlist.video_count.or_else(|| { playlist .video_count_short_text .and_then(|txt| util::parse_numeric(&txt).ok()) }), } } fn map_channel(&mut self, channel: ChannelRenderer) -> ChannelItem { // channel handle instead of subscriber count (A/B test 3) let (handle, sc_txt) = if channel .subscriber_count_text .as_ref() .map(|txt| txt.starts_with('@')) .unwrap_or_default() { (channel.subscriber_count_text, channel.video_count_text) } else { (None, channel.subscriber_count_text) }; ChannelItem { id: channel.channel_id, name: channel.title, handle, avatar: channel.thumbnail.into(), verification: channel.owner_badges.into(), subscriber_count: sc_txt.and_then(|txt| { util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings) }), short_description: channel.description_snippet, } } fn map_lockup(&mut self, lockup: LockupViewModel) -> Option { let md = lockup.metadata.lockup_metadata_view_model; let tn = lockup.content_image.into_image(); match lockup.content_type { LockupContentType::LockupContentTypePlaylist => { Some(YouTubeItem::Playlist(PlaylistItem { id: lockup.content_id, name: md.title, thumbnail: tn.image.into(), channel: self.channel.clone(), video_count: tn .overlays .first() .and_then(|ol| { ol.thumbnail_overlay_badge_view_model .thumbnail_badges .first() }) .and_then(|badge| { util::parse_numeric(&badge.thumbnail_badge_view_model.text).ok() }), })) } LockupContentType::LockupContentTypeVideo => { let mut mdr = md .metadata .content_metadata_view_model .metadata_rows .into_iter(); let channel = mdr .next() .and_then(|r| r.metadata_parts.into_iter().next()) .and_then(|p| ChannelTag::try_from(p.into_text_component()).ok()); let (view_count, publish_date_txt) = mdr .next() .map(|metadata_row| { let mut parts = metadata_row.metadata_parts.into_iter(); let p1 = parts.next(); let p2 = parts.next(); ( p1.and_then(|p| { util::parse_large_numstr_or_warn( p.as_str(), self.lang, &mut self.warnings, ) }), p2.map(|p2| p2.into_text_component().into_string()), ) }) .unwrap_or_default(); Some(YouTubeItem::Video(VideoItem { id: lockup.content_id, name: md.title, duration: tn .overlays .first() .and_then(|ol| { ol.thumbnail_overlay_badge_view_model .thumbnail_badges .first() }) .and_then(|badge| { util::parse_video_length(&badge.thumbnail_badge_view_model.text) }), thumbnail: tn.image.into(), channel, publish_date: publish_date_txt.as_deref().and_then(|t| { timeago::parse_timeago_dt_or_warn(self.lang, t, &mut self.warnings) }), publish_date_txt, view_count, is_live: false, is_short: false, is_upcoming: false, short_description: None, })) } LockupContentType::Unknown => None, } } } impl YouTubeListMapper { fn map_item(&mut self, item: YouTubeListItem) { match item { YouTubeListItem::VideoRenderer(video) => { let mapped = YouTubeItem::Video(self.map_video(video)); self.items.push(mapped); } YouTubeListItem::ShortsLockupViewModel(video) => { if let Some(mapped) = self.map_short_video2(video) { self.items.push(YouTubeItem::Video(mapped)); } } YouTubeListItem::ReelItemRenderer(video) => { let mapped = self.map_short_video(video); self.items.push(YouTubeItem::Video(mapped)); } YouTubeListItem::PlaylistVideoRenderer(video) => { let mapped = self.map_playlist_video(video); self.items.push(YouTubeItem::Video(mapped)); } YouTubeListItem::PlaylistRenderer(playlist) => { let mapped = YouTubeItem::Playlist(self.map_playlist(playlist)); self.items.push(mapped); } YouTubeListItem::ChannelRenderer(channel) => { let mapped = YouTubeItem::Channel(self.map_channel(channel)); self.items.push(mapped); } YouTubeListItem::LockupViewModel(lockup) => { if let Some(mapped) = self.map_lockup(lockup) { self.items.push(mapped); } } YouTubeListItem::ContinuationItemRenderer { continuation_endpoint, } => self.ctoken = Some(continuation_endpoint.continuation_command.token), YouTubeListItem::ShowingResultsForRenderer { corrected_query } => { self.corrected_query = Some(corrected_query); } YouTubeListItem::RichItemRenderer { content } => { self.map_item(*content); } YouTubeListItem::ItemSectionRenderer { mut contents, .. } => { self.warnings.append(&mut contents.warnings); contents.c.into_iter().for_each(|it| self.map_item(it)); } YouTubeListItem::None | YouTubeListItem::ChannelAgeGateRenderer { .. } => {} } } pub(crate) fn map_response(&mut self, mut res: MapResult>) { self.warnings.append(&mut res.warnings); res.c.into_iter().for_each(|item| self.map_item(item)); } } impl YouTubeListMapper { fn map_item(&mut self, item: YouTubeListItem) { match item { YouTubeListItem::VideoRenderer(video) => { let mapped = self.map_video(video); self.items.push(mapped); } YouTubeListItem::ReelItemRenderer(video) => { let mapped = self.map_short_video(video); self.items.push(mapped); } YouTubeListItem::ShortsLockupViewModel(video) => { if let Some(mapped) = self.map_short_video2(video) { self.items.push(mapped); } } YouTubeListItem::PlaylistVideoRenderer(video) => { let mapped = self.map_playlist_video(video); self.items.push(mapped); } YouTubeListItem::LockupViewModel(lockup) => { if let Some(YouTubeItem::Video(mapped)) = self.map_lockup(lockup) { self.items.push(mapped); } } YouTubeListItem::ContinuationItemRenderer { continuation_endpoint, } => self.ctoken = Some(continuation_endpoint.continuation_command.token), YouTubeListItem::ShowingResultsForRenderer { corrected_query } => { self.corrected_query = Some(corrected_query); } YouTubeListItem::RichItemRenderer { content } => { self.map_item(*content); } YouTubeListItem::ItemSectionRenderer { mut contents, .. } => { self.warnings.append(&mut contents.warnings); contents.c.into_iter().for_each(|it| self.map_item(it)); } _ => {} } } pub(crate) fn map_response(&mut self, mut res: MapResult>) { self.warnings.append(&mut res.warnings); res.c.into_iter().for_each(|item| self.map_item(item)); } #[cfg(feature = "userdata")] pub(crate) fn conv_history_items( self, date_txt: Option, utc_offset: UtcOffset, res: &mut MapResult>>, ) { res.warnings.extend(self.warnings); res.c.extend(self.items.into_iter().map(|item| HistoryItem { item, playback_date: date_txt.as_deref().and_then(|s| { timeago::parse_textual_date_to_d(self.lang, utc_offset, s, &mut res.warnings) }), playback_date_txt: date_txt.clone(), })); } } impl YouTubeListMapper { fn map_item(&mut self, item: YouTubeListItem) { match item { YouTubeListItem::PlaylistRenderer(playlist) => { let mapped = self.map_playlist(playlist); self.items.push(mapped); } YouTubeListItem::LockupViewModel(lockup) => { if let Some(YouTubeItem::Playlist(mapped)) = self.map_lockup(lockup) { self.items.push(mapped); } } YouTubeListItem::ContinuationItemRenderer { continuation_endpoint, } => self.ctoken = Some(continuation_endpoint.continuation_command.token), YouTubeListItem::ShowingResultsForRenderer { corrected_query } => { self.corrected_query = Some(corrected_query); } YouTubeListItem::RichItemRenderer { content } => { self.map_item(*content); } YouTubeListItem::ItemSectionRenderer { mut contents, .. } => { self.warnings.append(&mut contents.warnings); contents.c.into_iter().for_each(|it| self.map_item(it)); } _ => {} } } pub(crate) fn map_response(&mut self, mut res: MapResult>) { self.warnings.append(&mut res.warnings); res.c.into_iter().for_each(|item| self.map_item(item)); } }