From 4f48ad59bdbdde043f02fe8c09923f0558304ac3 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Mon, 17 Oct 2022 22:00:33 +0200 Subject: [PATCH] refactor: update playlist model --- src/client/playlist.rs | 21 +- src/client/response/mod.rs | 588 ------------------------------ src/client/response/playlist.rs | 62 +++- src/client/response/video_item.rs | 121 +++++- src/model/mod.rs | 185 +--------- 5 files changed, 189 insertions(+), 788 deletions(-) diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 1084575..fb222ef 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -200,30 +200,23 @@ impl MapResponse> for response::PlaylistCont { } } -fn map_playlist_items(items: Vec) -> (Vec, Option) { +fn map_playlist_items( + items: Vec, +) -> (Vec, Option) { let mut ctoken: Option = None; let videos = items .into_iter() .filter_map(|it| match it { - response::VideoListItem::PlaylistVideoRenderer(video) => { - match ChannelId::try_from(video.channel) { - Ok(channel) => Some(PlaylistVideo { - id: video.video_id, - title: video.title, - length: video.length_seconds, - thumbnail: video.thumbnail.into(), - channel, - }), - Err(_) => None, - } + response::playlist::PlaylistItem::PlaylistVideoRenderer(video) => { + PlaylistVideo::try_from(video).ok() } - response::VideoListItem::ContinuationItemRenderer { + response::playlist::PlaylistItem::ContinuationItemRenderer { continuation_endpoint, } => { ctoken = Some(continuation_endpoint.continuation_command.token); None } - _ => None, + response::playlist::PlaylistItem::None => None, }) .collect::>(); (videos, ctoken) diff --git a/src/client/response/mod.rs b/src/client/response/mod.rs index f89b3aa..b94cff3 100644 --- a/src/client/response/mod.rs +++ b/src/client/response/mod.rs @@ -27,22 +27,15 @@ pub mod channel_rss; #[cfg(feature = "rss")] pub use channel_rss::ChannelRss; -use chrono::TimeZone; use serde::Deserialize; use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError}; use crate::error::ExtractionError; -use crate::model; -use crate::param::Language; use crate::serializer::MapResult; use crate::serializer::{ - ignore_any, text::{Text, TextComponent}, VecLogError, }; -use crate::timeago; -use crate::util::MappingError; -use crate::util::{self, TryRemove}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -81,235 +74,6 @@ pub struct Thumbnail { pub height: u32, } -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum VideoListItem { - /// Video on channel page - GridVideoRenderer(GridVideoRenderer), - /// Video in recommendations - CompactVideoRenderer(CompactVideoRenderer), - /// Video in playlist - PlaylistVideoRenderer(PlaylistVideoRenderer), - - /// Playlist on channel page - GridPlaylistRenderer(GridPlaylistRenderer), - - /// Video on startpage - /// - /// Seems to be currently A/B tested on the channel page, - /// as of 11.10.2022 - RichItemRenderer { content: RichItem }, - - /// Seems to be currently A/B tested on the video details page, - /// as of 11.10.2022 - ItemSectionRenderer { - #[serde_as(as = "VecLogError<_>")] - contents: MapResult>, - }, - - /// 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, - }, - /// No video list item (e.g. ad) or unimplemented item - /// - /// Unimplemented: - /// - compactPlaylistRenderer (recommended playlists) - /// - compactRadioRenderer (recommended mix) - #[serde(other, deserialize_with = "ignore_any")] - None, -} - -/// Video displayed on a channel page -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GridVideoRenderer { - pub video_id: String, - pub thumbnail: Thumbnails, - #[serde_as(as = "Text")] - pub title: String, - #[serde_as(as = "Option")] - pub published_time_text: Option, - /// Contains `No views` if the view count is zero - #[serde_as(as = "Option")] - pub view_count_text: Option, - /// Contains video length and Short/Live tag - #[serde_as(as = "VecSkipError<_>")] - pub thumbnail_overlays: Vec, - /// Release date for upcoming videos - pub upcoming_event_data: Option, -} - -/// Video displayed in recommendations -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CompactVideoRenderer { - pub video_id: String, - pub thumbnail: Thumbnails, - #[serde_as(as = "Text")] - pub title: String, - #[serde(rename = "shortBylineText")] - pub channel: TextComponent, - pub channel_thumbnail: Thumbnails, - /// Channel verification badge - #[serde(default)] - #[serde_as(as = "VecSkipError<_>")] - pub owner_badges: Vec, - #[serde_as(as = "Option")] - pub length_text: Option, - /// (e.g. `11 months ago`) - #[serde_as(as = "Option")] - pub published_time_text: Option, - /// Contains `No views` if the view count is zero - #[serde_as(as = "Option")] - pub view_count_text: Option, - /// Badges are displayed on the video thumbnail and - /// show certain video properties (e.g. active livestream) - #[serde(default)] - #[serde_as(as = "VecSkipError<_>")] - pub badges: Vec, - /// Contains Short/Live tag - #[serde_as(as = "VecSkipError<_>")] - pub thumbnail_overlays: Vec, -} - -/// Video displayed in search results -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub 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_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 Short/Live tag - #[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, -} - -/// Playlist displayed in search results -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PlaylistRenderer { - pub playlist_id: String, - #[serde_as(as = "Text")] - pub title: String, - /// The first item of this list contains the playlist thumbnail, - /// subsequent items contain very small thumbnails of the next playlist videos - pub thumbnails: Vec, - #[serde_as(as = "JsonString")] - pub video_count: u64, - #[serde(rename = "shortBylineText")] - pub channel: TextComponent, - /// Channel verification badge - #[serde(default)] - #[serde_as(as = "VecSkipError<_>")] - pub owner_badges: Vec, - /// First 2 videos - #[serde(default)] - #[serde_as(as = "VecSkipError<_>")] - pub videos: Vec, -} - -/// Video displayed in a playlist -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub 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 = "JsonString")] - pub length_seconds: u32, -} - -/// Channel displayed in search results -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub 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, -} - -/// Playlist displayed on a channel page -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GridPlaylistRenderer { - pub playlist_id: String, - #[serde_as(as = "Text")] - pub title: String, - pub thumbnail: Thumbnails, - #[serde_as(as = "Text")] - pub video_count_short_text: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -#[allow(clippy::large_enum_variant)] -pub enum RichItem { - VideoRenderer(VideoRenderer), - PlaylistRenderer(GridPlaylistRenderer), -} - -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpcomingEventData { - /// Unixtime in seconds - #[serde_as(as = "JsonString")] - pub start_time: i64, -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContinuationItemRenderer { @@ -384,58 +148,6 @@ pub enum ChannelBadgeStyle { BadgeStyleTypeVerifiedArtist, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TimeOverlay { - pub thumbnail_overlay_time_status_renderer: TimeOverlayRenderer, -} - -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub 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 enum TimeOverlayStyle { - #[default] - Default, - Live, - Shorts, -} - -/// Badges are displayed on the video thumbnail and -/// show certain video properties (e.g. active livestream) -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub 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 struct VideoBadgeRenderer { - pub style: VideoBadgeStyle, -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum VideoBadgeStyle { - /// Active livestream - BadgeStyleTypeLiveNow, -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Alert { @@ -450,43 +162,6 @@ pub struct AlertRenderer { pub text: String, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChannelThumbnailSupportedRenderers { - pub channel_thumbnail_with_link_renderer: ChannelThumbnailWithLinkRenderer, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChannelThumbnailWithLinkRenderer { - pub thumbnail: Thumbnails, -} - -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DetailedMetadataSnippet { - #[serde_as(as = "Text")] - pub snippet_text: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChildVideoRendererWrap { - pub child_video_renderer: ChildVideoRenderer, -} - -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ChildVideoRenderer { - pub video_id: String, - #[serde_as(as = "Text")] - pub title: String, - #[serde_as(as = "Option")] - pub length_text: Option, -} - // CONTINUATION #[serde_as] @@ -631,38 +306,6 @@ impl From for crate::model::Verification { } } -pub trait IsLive { - fn is_live(&self) -> bool; -} - -pub 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 - }) - } -} - pub fn alerts_to_err(alerts: Option>) -> ExtractionError { match alerts { Some(alerts) => ExtractionError::ContentUnavailable( @@ -676,234 +319,3 @@ pub fn alerts_to_err(alerts: Option>) -> ExtractionError { None => ExtractionError::ContentUnavailable("content not found".into()), } } - -pub trait FromWLang { - fn from_w_lang(from: T, lang: Language) -> Self; -} - -pub trait TryFromWLang: Sized { - fn from_w_lang(from: T, lang: Language) -> Result; -} - -impl FromWLang for model::ChannelVideo { - fn from_w_lang(video: GridVideoRenderer, lang: Language) -> Self { - let mut toverlays = video.thumbnail_overlays; - let is_live = toverlays.is_live(); - let is_short = toverlays.is_short(); - let to = toverlays.try_swap_remove(0); - - Self { - id: video.video_id, - title: video.title, - // Time text is `LIVE` for livestreams, so we ignore parse errors - length: to.and_then(|to| { - util::parse_video_length(&to.thumbnail_overlay_time_status_renderer.text) - }), - thumbnail: video.thumbnail.into(), - publish_date: video - .upcoming_event_data - .as_ref() - .map(|upc| { - chrono::Local.from_utc_datetime(&chrono::NaiveDateTime::from_timestamp( - upc.start_time, - 0, - )) - }) - .or_else(|| { - video - .published_time_text - .as_ref() - .and_then(|txt| timeago::parse_timeago_to_dt(lang, txt)) - }), - publish_date_txt: video.published_time_text, - view_count: video - .view_count_text - .and_then(|txt| util::parse_numeric(&txt).ok()) - .unwrap_or_default(), - is_live, - is_short, - is_upcoming: video.upcoming_event_data.is_some(), - } - } -} - -impl FromWLang for model::ChannelVideo { - fn from_w_lang(video: VideoRenderer, lang: Language) -> Self { - let mut toverlays = video.thumbnail_overlays; - let is_live = toverlays.is_live(); - let is_short = toverlays.is_short(); - let to = toverlays.try_swap_remove(0); - - Self { - id: video.video_id, - title: video.title, - // Time text is `LIVE` for livestreams, so we ignore parse errors - length: to.and_then(|to| { - util::parse_video_length(&to.thumbnail_overlay_time_status_renderer.text) - }), - thumbnail: video.thumbnail.into(), - publish_date: video - .upcoming_event_data - .as_ref() - .map(|upc| { - chrono::Local.from_utc_datetime(&chrono::NaiveDateTime::from_timestamp( - upc.start_time, - 0, - )) - }) - .or_else(|| { - video - .published_time_text - .as_ref() - .and_then(|txt| timeago::parse_timeago_to_dt(lang, txt)) - }), - publish_date_txt: video.published_time_text, - view_count: video - .view_count_text - .and_then(|txt| util::parse_numeric(&txt).ok()) - .unwrap_or_default(), - is_live, - is_short, - is_upcoming: video.upcoming_event_data.is_some(), - } - } -} - -impl From for model::ChannelPlaylist { - fn from(playlist: GridPlaylistRenderer) -> Self { - Self { - id: playlist.playlist_id, - name: playlist.title, - thumbnail: playlist.thumbnail.into(), - video_count: util::parse_numeric(&playlist.video_count_short_text).ok(), - } - } -} - -impl TryFromWLang for model::RecommendedVideo { - fn from_w_lang( - video: CompactVideoRenderer, - lang: Language, - ) -> Result { - let channel = model::ChannelId::try_from(video.channel)?; - - Ok(Self { - id: video.video_id, - title: video.title, - length: video - .length_text - .and_then(|txt| util::parse_video_length(&txt)), - thumbnail: video.thumbnail.into(), - channel: model::ChannelTag { - id: channel.id, - name: channel.name, - avatar: video.channel_thumbnail.into(), - verification: video.owner_badges.into(), - subscriber_count: None, - }, - publish_date: video - .published_time_text - .as_ref() - .and_then(|txt| timeago::parse_timeago_to_dt(lang, txt)), - publish_date_txt: video.published_time_text, - view_count: video - .view_count_text - .and_then(|txt| util::parse_numeric(&txt).ok()) - .unwrap_or_default(), - is_live: video.badges.is_live(), - is_short: video.thumbnail_overlays.is_short(), - }) - } -} - -impl TryFromWLang for model::SearchVideo { - fn from_w_lang(video: VideoRenderer, lang: Language) -> Result { - let channel = model::ChannelId::try_from( - video - .channel - .ok_or_else(|| MappingError("no video channel".into()))?, - )?; - let channel_thumbnail = video - .channel_thumbnail_supported_renderers - .ok_or_else(|| MappingError("no video channel thumbnail".into()))?; - - Ok(Self { - id: video.video_id, - title: video.title, - length: video - .length_text - .and_then(|txt| util::parse_video_length(&txt)), - thumbnail: video.thumbnail.into(), - channel: model::ChannelTag { - id: channel.id, - name: channel.name, - avatar: channel_thumbnail - .channel_thumbnail_with_link_renderer - .thumbnail - .into(), - verification: video.owner_badges.into(), - subscriber_count: None, - }, - publish_date: video - .published_time_text - .as_ref() - .and_then(|txt| timeago::parse_timeago_to_dt(lang, txt)), - publish_date_txt: video.published_time_text, - view_count: video - .view_count_text - .and_then(|txt| util::parse_numeric(&txt).ok()) - .unwrap_or_default(), - is_live: video.thumbnail_overlays.is_live(), - is_short: video.thumbnail_overlays.is_short(), - short_description: video - .detailed_metadata_snippets - .and_then(|mut snippets| snippets.try_swap_remove(0).map(|s| s.snippet_text)) - .or(video.description_snippet) - .unwrap_or_default(), - }) - } -} - -impl From for model::SearchPlaylist { - fn from(playlist: PlaylistRenderer) -> Self { - let mut thumbnails = playlist.thumbnails; - - Self { - id: playlist.playlist_id, - name: playlist.title, - thumbnail: thumbnails.try_swap_remove(0).unwrap_or_default().into(), - video_count: playlist.video_count, - first_videos: playlist - .videos - .into_iter() - .map(|v| model::SearchPlaylistVideo { - id: v.child_video_renderer.video_id, - title: v.child_video_renderer.title, - length: v - .child_video_renderer - .length_text - .and_then(|txt| util::parse_video_length(&txt)), - }) - .collect(), - } - } -} - -impl From for model::SearchChannel { - fn from(channel: ChannelRenderer) -> Self { - Self { - id: channel.channel_id, - name: channel.title, - avatar: channel.thumbnail.into(), - verification: channel.owner_badges.into(), - subscriber_count: channel - .subscriber_count_text - .and_then(|txt| util::parse_numeric(&txt).ok()), - video_count: channel - .video_count_text - .and_then(|txt| util::parse_numeric(&txt).ok()) - .unwrap_or_default(), - short_description: channel.description_snippet, - } - } -} diff --git a/src/client/response/playlist.rs b/src/client/response/playlist.rs index 8f2f521..0810379 100644 --- a/src/client/response/playlist.rs +++ b/src/client/response/playlist.rs @@ -1,11 +1,13 @@ use serde::Deserialize; -use serde_with::serde_as; -use serde_with::{DefaultOnError, VecSkipError}; +use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError}; use crate::serializer::text::{Text, TextComponent}; -use crate::serializer::{MapResult, VecLogError}; +use crate::serializer::{ignore_any, MapResult, VecLogError}; +use crate::util::MappingError; -use super::{Alert, ContentRenderer, ContentsRenderer, ThumbnailsWrap, VideoListItem}; +use super::{ + Alert, ContentRenderer, ContentsRenderer, ContinuationEndpoint, Thumbnails, ThumbnailsWrap, +}; #[serde_as] #[derive(Debug, Deserialize)] @@ -62,7 +64,7 @@ pub struct PlaylistVideoListRenderer { #[serde(rename_all = "camelCase")] pub struct PlaylistVideoList { #[serde_as(as = "VecLogError<_>")] - pub contents: MapResult>, + pub contents: MapResult>, } #[derive(Debug, Deserialize)] @@ -151,6 +153,54 @@ pub struct PlaylistThumbnailRenderer { pub playlist_video_thumbnail_renderer: ThumbnailsWrap, } +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PlaylistItem { + /// Video in playlist + PlaylistVideoRenderer(PlaylistVideoRenderer), + /// 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, + }, + /// No video list item (e.g. ad) or unimplemented item + #[serde(other, deserialize_with = "ignore_any")] + None, +} + +/// Video displayed in a playlist +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub 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 = "JsonString")] + pub length_seconds: u32, +} + +impl TryFrom for crate::model::PlaylistVideo { + type Error = MappingError; + + fn try_from(video: PlaylistVideoRenderer) -> Result { + Ok(Self { + id: video.video_id, + title: video.title, + length: video.length_seconds, + thumbnail: video.thumbnail.into(), + channel: crate::model::ChannelId::try_from(video.channel)?, + }) + } +} + +// Continuation + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OnResponseReceivedAction { @@ -162,5 +212,5 @@ pub struct OnResponseReceivedAction { #[serde(rename_all = "camelCase")] pub struct AppendAction { #[serde_as(as = "VecLogError<_>")] - pub continuation_items: MapResult>, + pub continuation_items: MapResult>, } diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index d956ec5..f9c06db 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -1,12 +1,8 @@ use chrono::TimeZone; use serde::Deserialize; -use serde_with::{json::JsonString, serde_as, VecSkipError}; +use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError}; -use super::{ - ChannelBadge, ChannelThumbnailSupportedRenderers, ContinuationEndpoint, - DetailedMetadataSnippet, IsLive, IsShort, Thumbnails, TimeOverlay, UpcomingEventData, - VideoBadge, -}; +use super::{ChannelBadge, ContinuationEndpoint, Thumbnails}; use crate::{ model::{ChannelId, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem}, param::Language, @@ -181,6 +177,119 @@ pub struct YouTubeListRenderer { pub contents: MapResult>, } +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpcomingEventData { + /// Unixtime in seconds + #[serde_as(as = "JsonString")] + pub start_time: i64, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub 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 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 struct VideoBadgeRenderer { + pub style: VideoBadgeStyle, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VideoBadgeStyle { + /// Active livestream + BadgeStyleTypeLiveNow, +} + +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub 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 enum TimeOverlayStyle { + #[default] + Default, + Live, + Shorts, +} + +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DetailedMetadataSnippet { + #[serde_as(as = "Text")] + pub snippet_text: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelThumbnailSupportedRenderers { + pub channel_thumbnail_with_link_renderer: ChannelThumbnailWithLinkRenderer, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelThumbnailWithLinkRenderer { + pub thumbnail: Thumbnails, +} + +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)] diff --git a/src/model/mod.rs b/src/model/mod.rs index 4ef5092..04b42d9 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -557,42 +557,6 @@ pub struct Chapter { pub thumbnail: Vec, } -/* -@RECOMMENDATIONS -*/ - -/// YouTube video fetched from the recommendations next to a video -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct RecommendedVideo { - /// Unique YouTube video ID - pub id: String, - /// Video title - pub title: String, - /// Video length in seconds. - /// - /// Is [`None`] for livestreams. - pub length: Option, - /// Video thumbnail - pub thumbnail: Vec, - /// Channel of the video - pub channel: ChannelTag, - /// Video publishing date. - /// - /// [`None`] if the date could not be parsed. - pub publish_date: Option>, - /// Textual video publish date (e.g. `11 months ago`, depends on language) - /// - /// Is [`None`] for livestreams. - pub publish_date_txt: Option, - /// View count - pub view_count: u64, - /// Is the video an active livestream? - pub is_live: bool, - /// Is the video a YouTube Short video (vertical and <60s)? - pub is_short: bool, -} - /// Channel information attached to a video or comment #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] @@ -710,55 +674,6 @@ pub struct Channel { pub content: T, } -/// Video fetched from a YouTube channel -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct ChannelVideo { - /// Unique YouTube video ID - pub id: String, - /// Video title - pub title: String, - /// Video length in seconds. - /// - /// Is [`None`] for livestreams. - pub length: Option, - /// Video thumbnail - pub thumbnail: Vec, - /// Video publishing date. - /// - /// [`None`] if the date could not be parsed. - /// May be in the future for upcoming videos - pub publish_date: Option>, - /// Textual video publish date (e.g. `11 months ago`, depends on language) - /// - /// Is [`None`] for livestreams and upcoming videos. - pub publish_date_txt: Option, - /// Number of views / current viewers in case of a livestream. - /// - /// [`None`] if it could not be extracted. - pub view_count: u64, - /// Is the video an active livestream? - pub is_live: bool, - /// Is the video a YouTube Short video (vertical and <60s)? - pub is_short: bool, - /// Is the video announced, but not released yet (YouTube Premiere)? - pub is_upcoming: bool, -} - -/// Playlist fetched from a YouTube channel -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct ChannelPlaylist { - /// Unique YouTube Playlist-ID (e.g. `PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ`) - pub id: String, - /// Playlist name - pub name: String, - /// Playlist thumbnail - pub thumbnail: Vec, - /// Number of playlist videos - pub video_count: Option, -} - /// Additional channel metadata fetched from the "About" tab. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] @@ -809,105 +724,27 @@ pub struct ChannelRssVideo { pub like_count: u64, } +/// YouTube search result #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SearchResult { + /// Search result items pub items: Paginator, + /// Corrected search query + /// + /// If the search term containes a typo, YouTube instead searches + /// for the corrected search term and displays it on top of the + /// search results page. pub corrected_query: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum SearchItem { - Video(SearchVideo), - Playlist(SearchPlaylist), - Channel(SearchChannel), -} - -/// YouTube video from the search results -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct SearchVideo { - /// Unique YouTube video ID - pub id: String, - /// Video title - pub title: String, - /// Video length in seconds. - /// - /// Is [`None`] for livestreams. - pub length: Option, - /// Video thumbnail - pub thumbnail: Vec, - /// Channel of the video - pub channel: ChannelTag, - /// Video publishing date. - /// - /// [`None`] if the date could not be parsed. - pub publish_date: Option>, - /// Textual video publish date (e.g. `11 months ago`, depends on language) - /// - /// Is [`None`] for livestreams. - pub publish_date_txt: Option, - /// View count - /// - /// [`None`] if it could not be extracted. - pub view_count: u64, - /// Is the video an active livestream? - pub is_live: bool, - /// Is the video a YouTube Short video (vertical and <60s)? - pub is_short: bool, - /// Abbreviated video description - pub short_description: String, -} - -/// Playlist from the search results -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct SearchPlaylist { - /// Unique YouTube Playlist-ID (e.g. `PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ`) - pub id: String, - /// Playlist name - pub name: String, - /// Playlist thumbnail - pub thumbnail: Vec, - /// Number of playlist videos - pub video_count: u64, - /// First 2 videos - pub first_videos: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct SearchPlaylistVideo { - pub id: String, - pub title: String, - pub length: Option, -} - -/// Channel from the search results -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[non_exhaustive] -pub struct SearchChannel { - /// Unique YouTube channel ID - pub id: String, - /// Channel name - pub name: String, - /// Channel avatar/profile picture - pub avatar: Vec, - /// Channel verification mark - pub verification: Verification, - /// Approximate number of subscribers - /// - /// [`None`] if hidden by the owner or not present. - pub subscriber_count: Option, - /// Number of videos from the channel - pub video_count: u64, - /// Abbreviated channel description - pub short_description: String, -} - +/// YouTube item (Video/Channel/Playlist) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum YouTubeItem { + /// YouTube video item Video(VideoItem), + /// YouTube playlist item Playlist(PlaylistItem), + /// YouTube channel item Channel(ChannelItem), }