feat: add video response

- started timeago_table
This commit is contained in:
ThetaDev 2022-09-03 11:20:07 +02:00
parent 346406c1c8
commit 9da166304a
21 changed files with 41070 additions and 9244 deletions

41
src/client/channel.rs Normal file
View file

@ -0,0 +1,41 @@
use anyhow::Result;
use reqwest::Method;
use serde::Serialize;
use super::{response, ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QChannel {
context: ContextYT,
browse_id: String,
params: String,
}
impl RustyTube {
async fn get_channel_response(&self, channel_id: &str) -> Result<response::Channel> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QChannel {
context,
browse_id: channel_id.to_owned(),
params: "EgZ2aWRlb3PyBgQKAjoA".to_owned(),
};
let resp = client
.request_builder(Method::POST, "browse")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::Channel>().await?)
}
}
#[cfg(test)]
mod tests {
}

View file

@ -1,5 +1,7 @@
pub mod player;
pub mod playlist;
pub mod video;
mod response;
#[cfg(test)]

View file

@ -2,6 +2,7 @@ use serde::Deserialize;
use serde_with::serde_as;
use serde_with::VecSkipError;
use super::TimeOverlay;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
#[derive(Clone, Debug, Deserialize)]
@ -64,24 +65,10 @@ pub struct ChannelVideo {
pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde_as(as = "crate::serializer::text::Text")]
pub published_time_text: String,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count_text: String,
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlayWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlayWrap {
pub thumbnail_overlay_time_status_renderer: TimeOverlay,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlay {
#[serde_as(as = "crate::serializer::text::Text")]
pub text: String,
pub thumbnail_overlays: Vec<TimeOverlay>,
}

View file

@ -2,11 +2,15 @@ pub mod channel;
pub mod player;
pub mod playlist;
pub mod playlist_music;
pub mod video;
pub use channel::Channel;
pub use player::Player;
pub use playlist::Playlist;
pub use playlist_music::PlaylistMusic;
pub use video::Video;
pub use video::VideoComments;
pub use video::VideoRecommendations;
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
@ -50,7 +54,7 @@ pub struct Thumbnail {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VideoListItem<T> {
#[serde(alias = "playlistVideoRenderer")]
#[serde(alias = "playlistVideoRenderer", alias = "compactVideoRenderer")]
GridVideoRenderer {
#[serde(flatten)]
video: T,
@ -73,6 +77,77 @@ pub struct ContinuationCommand {
pub token: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Icon {
pub icon_type: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwner {
pub video_owner_renderer: VideoOwnerRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwnerRenderer {
#[serde_as(as = "crate::serializer::text::TextLink")]
pub title: TextLink,
pub thumbnail: Thumbnails,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub subscriber_count_text: Option<String>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub badges: Vec<UserBadge>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserBadge {
pub metadata_badge_renderer: UserBadgeRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserBadgeRenderer {
pub style: UserBadgeStyle,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum UserBadgeStyle {
BadgeStyleTypeVerified,
BadgeStyleTypeVerifiedArtist,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlay {
pub thumbnail_overlay_time_status_renderer: TimeOverlayRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlayRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
pub text: String,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub style: TimeOverlayStyle,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TimeOverlayStyle {
#[default]
Default,
Live,
Shorts,
}
// YouTube Music
#[serde_as]
@ -80,13 +155,10 @@ pub struct ContinuationCommand {
#[serde(rename_all = "camelCase")]
pub struct MusicItem {
pub thumbnail: MusicThumbnailRenderer,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub playlist_item_data: Option<PlaylistItemData>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub flex_columns: Vec<MusicColumn>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub fixed_columns: Vec<MusicColumn>,
}

View file

@ -81,7 +81,6 @@ pub struct Format {
#[serde_as(as = "JsonString")]
pub content_length: u64,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub quality: Option<Quality>,
pub fps: Option<u8>,
@ -90,7 +89,6 @@ pub struct Format {
pub color_info: Option<ColorInfo>,
// Audio only
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub audio_quality: Option<AudioQuality>,
#[serde_as(as = "Option<JsonString>")]

View file

@ -4,7 +4,9 @@ use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use crate::serializer::text::{Text, TextLink};
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
use super::{
ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem, VideoOwner,
};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -73,7 +75,6 @@ pub struct PlaylistVideo {
pub channel: TextLink,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
pub is_playable: bool,
}
#[derive(Clone, Debug, Deserialize)]
@ -89,7 +90,6 @@ pub struct HeaderRenderer {
pub playlist_id: String,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(default)]
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
pub description_text: Option<String>,
/// `"495", " videos"`
@ -122,7 +122,7 @@ pub enum SidebarRendererItem {
// stats: Vec<Text>,
},
#[serde(rename_all = "camelCase")]
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwnerWrap },
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwner },
}
#[derive(Clone, Debug, Deserialize)]
@ -133,20 +133,6 @@ pub struct PlaylistThumbnailRenderer {
pub playlist_video_thumbnail_renderer: ThumbnailsWrap,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwnerWrap {
pub video_owner_renderer: VideoOwner,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwner {
#[serde_as(as = "crate::serializer::text::TextLink")]
pub title: TextLink,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnResponseReceivedAction {

View file

@ -0,0 +1,435 @@
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{DefaultOnError, VecSkipError};
use crate::serializer::text::TextLink;
use super::{ContinuationEndpoint, Icon, Thumbnails, VideoListItem, VideoOwner};
/// Video info response
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Video {
pub contents: Contents,
#[serde_as(as = "VecSkipError<_>")]
pub engagement_panels: Vec<EngagementPanel>,
}
/// Video recommendations response
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoRecommendations {
pub on_response_received_endpoints: Vec<RecommendationsContItem>,
}
/// Video comments response
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoComments {
#[serde_as(as = "VecSkipError<_>")]
pub on_response_received_endpoints: Vec<CommentsContItem>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contents {
pub two_column_watch_next_results: TwoColumnWatchNextResults,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TwoColumnWatchNextResults {
pub results: VideoResultsWrap,
pub secondary_results: RecommendationResultsWrap,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoResultsWrap {
pub results: VideoResults,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoResults {
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<VideoResultsItem>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VideoResultsItem {
#[serde(rename_all = "camelCase")]
VideoPrimaryInfoRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
title: String,
view_count: ViewCountWrap,
video_actions: VideoActions,
#[serde_as(as = "crate::serializer::text::Text")]
date_text: String,
},
#[serde(rename_all = "camelCase")]
VideoSecondaryInfoRenderer {
owner: VideoOwner,
#[serde_as(as = "crate::serializer::text::Text")]
description: String,
#[serde_as(deserialize_as = "DefaultOnError")]
metadata_row_container: Option<MetadataRowContainer>,
},
#[serde(rename_all = "camelCase")]
ItemSectionRenderer {
#[serde_as(as = "VecSkipError<_>")]
contents: Vec<ItemSection>,
section_identifier: String,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ViewCountWrap {
pub video_view_count_renderer: ViewCount,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ViewCount {
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoActions {
pub menu_renderer: VideoActionsMenu,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoActionsMenu {
#[serde_as(as = "VecSkipError<_>")]
pub top_level_buttons: Vec<ToggleButtonWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToggleButtonWrap {
pub toggle_button_renderer: ToggleButton,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToggleButton {
pub default_icon: Icon,
#[serde_as(as = "crate::serializer::text::Text")]
pub default_text: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRowContainer {
pub metadata_row_container_renderer: MetadataRowContainerRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRowContainerRenderer {
pub rows: Vec<MetadataRow>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRow {
pub metadata_row_renderer: MetadataRowRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRowRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde_as(as = "Vec<crate::serializer::text::TextLinks>")]
pub contents: Vec<Vec<TextLink>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ItemSection {
#[serde(rename_all = "camelCase")]
CommentsEntryPointHeaderRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
header_text: String,
#[serde_as(as = "crate::serializer::text::Text")]
comment_count: String,
},
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendationResultsWrap {
pub secondary_results: RecommendationResults,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendationResults {
#[serde_as(as = "VecSkipError<_>")]
pub results: Vec<VideoListItem<RecommendedVideo>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendedVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
#[serde_as(as = "crate::serializer::text::TextLink")]
pub channel: TextLink,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub length_text: Option<String>,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count_text: String,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub badges: Vec<VideoBadge>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoBadge {
pub metadata_badge_renderer: VideoBadgeRenderer,
}
#[derive(Clone, 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 {
BadgeStyleTypeLiveNow,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanel {
pub engagement_panel_section_list_renderer: EngagementPanelRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelRenderer {
pub header: EngagementPanelHeader,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelHeader {
pub engagement_panel_title_header_renderer: EngagementPanelHeaderRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelHeaderRenderer {
pub menu: EngagementPanelMenu,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelMenu {
pub sort_filter_sub_menu_renderer: EngagementPanelMenuRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelMenuRenderer {
pub sub_menu_items: Vec<EngagementPanelMenuItem>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelMenuItem {
pub service_endpoint: ContinuationEndpoint,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendationsContItem {
pub append_continuation_items_action: AppendRecommendations,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendRecommendations {
pub continuation_items: Vec<VideoListItem<RecommendedVideo>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentsContItem {
#[serde(alias = "reloadContinuationItemsCommand")]
pub append_continuation_items_action: AppendComments,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendComments {
#[serde_as(as = "VecSkipError<_>")]
pub continuation_items: Vec<CommentListItem>,
pub target_id: String,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CommentListItem {
#[serde(rename_all = "camelCase")]
CommentThreadRenderer {
comment: Comment,
#[serde(default)]
replies: Replies,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
rendering_priority: CommentPriority,
},
CommentRenderer {
#[serde(flatten)]
comment: CommentRenderer,
},
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
// TODO: TMP
#[serde(rename_all = "camelCase")]
CommentsHeaderRenderer { count_text: Option<String> },
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Comment {
pub comment_renderer: CommentRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentRenderer {
/*
#[serde_as(as = "crate::serializer::text::Text")]
pub author_text: String,
pub author_thumbnail: Thumbnails,
pub author_endpoint: AuthorEndpoint,
*/
// There may be comments with missing authors (possibly deleted users?)
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
pub author_text: Option<String>,
pub author_thumbnail: Thumbnails,
#[serde_as(as = "DefaultOnError")]
pub author_endpoint: Option<AuthorEndpoint>,
#[serde_as(as = "crate::serializer::text::Text")]
pub content_text: String,
#[serde_as(as = "crate::serializer::text::Text")]
pub published_time_text: String,
pub comment_id: String,
pub author_is_channel_owner: bool,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub vote_count: Option<String>,
pub author_comment_badge: Option<AuthorCommentBadge>,
#[serde(default)]
pub reply_count: u32,
pub action_buttons: CommentActionButtons,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorEndpoint {
pub browse_endpoint: BrowseEndpoint,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowseEndpoint {
pub browse_id: String,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CommentPriority {
#[default]
RenderingPriorityUnknown,
RenderingPriorityPinnedComment,
}
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Replies {
pub comment_replies_renderer: RepliesRenderer,
}
#[serde_as]
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RepliesRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<CommentListItem>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentActionButtons {
pub comment_action_buttons_renderer: CommentActionButtonsRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentActionButtonsRenderer {
pub creator_heart: Option<CreatorHeart>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatorHeart {
pub creator_heart_renderer: CreatorHeartRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatorHeartRenderer {
pub is_hearted: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorCommentBadge {
pub author_comment_badge_renderer: AuthorCommentBadgeRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorCommentBadgeRenderer {
pub icon: Icon,
}

View file

@ -8,9 +8,8 @@ use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use serde_with::VecSkipError;
use crate::client::ClientType;
use crate::client::ContextYT;
use crate::client::RustyTube;
use crate::client::response::Icon;
use crate::client::{ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@ -77,12 +76,6 @@ struct CompactLinkRenderer {
service_endpoint: ServiceEndpoint<MenuAction>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Icon {
icon_type: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceEndpoint<T> {
@ -161,7 +154,7 @@ async fn generate_locales() {
code.push_str(&format!(" /// {}\n ", n));
if c.contains('-') {
code.push_str(&format!("#[serde(rename=\"{}\")]\n ", c));
code.push_str(&format!("#[serde(rename = \"{}\")]\n ", c));
}
c.split('-').for_each(|c| {

View file

@ -3,17 +3,26 @@
use std::{
collections::{BTreeMap, HashSet},
fs::File,
io::BufReader,
path::Path,
};
use anyhow::anyhow;
use fancy_regex::Regex;
use futures::{stream, StreamExt};
use intl_pluralrules::{PluralCategory, PluralRuleType, PluralRules};
use once_cell::sync::Lazy;
use reqwest::Method;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use unic_langid::LanguageIdentifier;
use crate::{
client::{response, ClientType, ContextYT, RustyTube},
client::{
response::{self, video::CommentListItem},
ClientType, ContextYT, RustyTube,
},
model::{Country, Language},
timeago,
timeago::{self, TimeAgo, TimeUnit, LANGUAGES},
};
#[derive(Clone, Debug, Serialize)]
@ -61,13 +70,101 @@ async fn get_channel_datestrings(rp: &RustyTube, channel_id: &str) -> Vec<String
.iter()
.filter_map(|itm| match itm {
response::VideoListItem::GridVideoRenderer { video } => {
Some(video.published_time_text.to_owned())
video.published_time_text.to_owned()
}
response::VideoListItem::ContinuationItemRenderer { .. } => None,
})
.collect::<Vec<_>>()
}
async fn get_comment_initial_ctoken(rp: &RustyTube, video_id: &str) -> (String, String) {
let video_response = rp.get_video_response(video_id).await.unwrap();
let top = video_response
.contents
.two_column_watch_next_results
.results
.results
.contents
.iter()
.find_map(|c| match c {
response::video::VideoResultsItem::ItemSectionRenderer {
contents,
section_identifier,
} => match section_identifier == "comment-item-section" {
true => match &contents[0] {
response::video::ItemSection::ContinuationItemRenderer {
continuation_endpoint,
} => Some(continuation_endpoint.continuation_command.token.to_owned()),
_ => None,
},
false => None,
},
_ => None,
})
.unwrap();
let latest = video_response
.engagement_panels
.iter()
.find_map(|p| {
p.engagement_panel_section_list_renderer
.header
.engagement_panel_title_header_renderer
.menu
.sort_filter_sub_menu_renderer
.sub_menu_items
.get(1)
.map(|i| i.service_endpoint.continuation_command.token.to_owned())
})
.unwrap();
(top, latest)
}
async fn get_comment_datestrings(rp: &RustyTube, ctoken: &str) -> (Vec<String>, Option<String>) {
let comments_response = rp.get_comments_response(ctoken).await.unwrap();
let mut next_ctoken: Option<String> = None;
let datestrings = comments_response
.on_response_received_endpoints
/*
.iter()
.find(|e| {
!e.append_continuation_items_action
.continuation_items
.is_empty()
&& matches!(
&e.append_continuation_items_action.continuation_items[0],
CommentListItem::CommentsHeaderRenderer { count_text }
)
})
.unwrap()
*/
.iter()
.rev()
.next()
.unwrap()
.append_continuation_items_action
.continuation_items
.iter()
.filter_map(|itm| match itm {
response::video::CommentListItem::CommentThreadRenderer { comment, .. } => {
Some(comment.comment_renderer.published_time_text.to_owned())
}
response::video::CommentListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
next_ctoken = Some(continuation_endpoint.continuation_command.token.to_owned());
None
}
_ => None,
})
.collect::<Vec<_>>();
(datestrings, next_ctoken)
}
#[test_log::test(tokio::test)]
async fn download_timeago_testfiles() {
let json_path = Path::new("testfiles/date/timeago.json").to_path_buf();
@ -128,3 +225,206 @@ async fn download_timeago_testfiles() {
let file = File::create(json_path).unwrap();
serde_json::to_writer_pretty(file, &strings_map).unwrap();
}
#[derive(Debug, Clone, Deserialize)]
struct PluralRulesData {
supplemental: PluralRulesInner,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct PluralRulesInner {
plurals_type_cardinal: BTreeMap<String, Ruleset>,
}
#[derive(Debug, Clone, Deserialize)]
struct Ruleset {
#[serde(rename = "pluralRule-count-one")]
one: Option<String>,
#[serde(rename = "pluralRule-count-two")]
two: Option<String>,
#[serde(rename = "pluralRule-count-few")]
few: Option<String>,
#[serde(rename = "pluralRule-count-many")]
many: Option<String>,
#[serde(rename = "pluralRule-count-other")]
other: Option<String>,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
enum PluralCat {
One,
Two,
Few,
Many,
Other,
}
impl TryFrom<PluralCategory> for PluralCat {
type Error = anyhow::Error;
fn try_from(value: PluralCategory) -> Result<Self, Self::Error> {
match value {
PluralCategory::ZERO => Err(anyhow!("zero is not supported")),
PluralCategory::ONE => Ok(Self::One),
PluralCategory::TWO => Ok(Self::Two),
PluralCategory::FEW => Ok(Self::Few),
PluralCategory::MANY => Ok(Self::Many),
PluralCategory::OTHER => Ok(Self::Other),
}
}
}
static PLURAL_RULES: Lazy<BTreeMap<String, HashSet<PluralCat>>> = Lazy::new(|| {
let json_path = Path::new("testfiles/date/cldr_pluralrules_cardinals.json");
let json_file = File::open(json_path).unwrap();
serde_json::from_reader::<_, PluralRulesData>(BufReader::new(json_file))
.unwrap()
.supplemental
.plurals_type_cardinal
.iter()
.map(|(lang, rules)| {
let mut hs: HashSet<PluralCat> = HashSet::new();
if rules.one.is_some() {
hs.insert(PluralCat::One);
}
if rules.two.is_some() {
hs.insert(PluralCat::Two);
}
if rules.few.is_some() {
hs.insert(PluralCat::Few);
}
if rules.many.is_some() {
hs.insert(PluralCat::Many);
}
if rules.other.is_some() {
hs.insert(PluralCat::Other);
}
(lang.to_owned(), hs)
})
.collect::<BTreeMap<_, _>>()
});
type TimeagoTable = BTreeMap<Language, BTreeMap<TimeUnit, TimeagoTableEntry>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TimeagoTableEntry {
cases: BTreeMap<String, TimeAgo>,
missing_plurals: HashSet<PluralCat>,
}
const TIME_UNITS: [TimeUnit; 7] = [
TimeUnit::Second,
TimeUnit::Minute,
TimeUnit::Hour,
TimeUnit::Day,
TimeUnit::Week,
TimeUnit::Month,
TimeUnit::Year,
];
fn new_timeago_table() -> TimeagoTable {
LANGUAGES
.iter()
.filter_map(|lang| {
// Check if language is redundant
match lang {
Language::EnGb
| Language::EnIn
| Language::FrCa
| Language::EsUs
| Language::Es419 => None,
_ => {
let cldr_lang_str = match lang {
Language::SrLatn => "sr".to_owned(),
Language::ZhCn | Language::ZhHk | Language::ZhTw => "zh".to_owned(),
_ => lang.to_string(),
};
let m = TIME_UNITS
.iter()
.map(|t| {
let missing_plurals = if t == &TimeUnit::Week {
// Week only has 3 valid values (1-3)
let mut mp = HashSet::new();
let l_id = cldr_lang_str.parse::<LanguageIdentifier>().unwrap();
let pr =
PluralRules::create(l_id, PluralRuleType::CARDINAL).unwrap();
mp.insert(PluralCat::try_from(pr.select(1).unwrap()).unwrap());
mp.insert(PluralCat::try_from(pr.select(2).unwrap()).unwrap());
mp.insert(PluralCat::try_from(pr.select(3).unwrap()).unwrap());
mp
} else {
PLURAL_RULES.get(&cldr_lang_str).unwrap().clone()
};
(
t.to_owned(),
TimeagoTableEntry {
cases: BTreeMap::new(),
missing_plurals,
},
)
})
.collect();
Some((lang.to_owned(), m))
}
}
})
.collect()
}
#[test]
fn t_new_timeago_table() {
let json_path = Path::new("testfiles/date/timeago_table.json").to_path_buf();
if json_path.exists() {
return;
}
let file = File::create(json_path).unwrap();
serde_json::to_writer_pretty(file, &new_timeago_table()).unwrap();
}
#[tokio::test]
async fn t_tmp() {
let rp = RustyTube::new();
let (top, latest) = get_comment_initial_ctoken(&rp, "gQlMMD8auMs").await;
// let (top, latest) = get_comment_initial_ctoken(&rp, "9bZkp7q19f0").await;
let mut ctoken = latest;
let brace_pattern = Regex::new(r"\(.+\)").unwrap();
for _ in 0..100 {
let (strings, new_ctoken) = get_comment_datestrings(&rp, &ctoken).await;
/*
strings
.iter()
.map(|s| {
// Remove zero-width space characters
let s = s.replace('\u{200b}', "");
// Remove braces
let s = brace_pattern.replace(&s, "");
let s = s.trim();
s.to_owned()
})
.for_each(|s| println!("{}", s));
*/
println!("n: {}", strings.len());
if let Some(new_ctoken) = new_ctoken {
ctoken = new_ctoken.to_owned();
} else {
break;
}
}
}

111
src/client/video.rs Normal file
View file

@ -0,0 +1,111 @@
use anyhow::Result;
use reqwest::Method;
use serde::Serialize;
use super::{response, ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
struct QVideo {
context: ContextYT,
/// YouTube video ID
video_id: String,
/// Set to true to allow extraction of streams with sensitive content
content_check_ok: bool,
/// Probably refers to allowing sensitive content, too
racy_check_ok: bool,
}
#[derive(Clone, Debug, Serialize)]
struct QVideoCont {
context: ContextYT,
continuation: String,
}
impl RustyTube {
pub async fn get_video_response(&self, video_id: &str) -> Result<response::Video> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QVideo {
context,
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
};
let resp = client
.request_builder(Method::POST, "next")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::Video>().await?)
}
pub async fn get_comments_response(&self, ctoken: &str) -> Result<response::VideoComments> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QVideoCont {
context,
continuation: ctoken.to_owned(),
};
let resp = client
.request_builder(Method::POST, "next")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::VideoComments>().await?)
}
pub async fn get_recommendations_response(
&self,
ctoken: &str,
) -> Result<response::VideoRecommendations> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QVideoCont {
context,
continuation: ctoken.to_owned(),
};
let resp = client
.request_builder(Method::POST, "next")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::VideoRecommendations>().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn t_get_video_response() {
let rt = RustyTube::new();
// rt.get_video("ZeerrnuLi5E").await.unwrap();
dbg!(rt.get_video_response("iQfSvIgIs_M").await.unwrap());
}
#[tokio::test]
async fn t_get_comments_response() {
let rt = RustyTube::new();
// rt.get_comments("Eg0SC2lRZlN2SWdJc19NGAYyJSIRIgtpUWZTdklnSXNfTTAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D").await.unwrap();
dbg!(rt.get_comments_response("Eg0SC2lRZlN2SWdJc19NGAYychpFEhRVZ2lnVGJVTEZ6Qk5FWGdDb0FFQyICCAAqGFVDWFgwUldPSUJqdDRvM3ppSHUtNmE1QTILaVFmU3ZJZ0lzX01AAUgKQiljb21tZW50LXJlcGxpZXMtaXRlbS1VZ2lnVGJVTEZ6Qk5FWGdDb0FFQw%3D%3D").await.unwrap());
}
#[tokio::test]
async fn t_get_recommendations_response() {
let rt = RustyTube::new();
dbg!(rt.get_recommendations_response("CBQSExILaVFmU3ZJZ0lzX03AAQHIAQEYACqkBjJzNkw2d3pVQkFyUkJBb0Q4ajRBQ2c3Q1Bnc0lvWXlRejhLZnRZUGNBUW9EOGo0QUNnN0NQZ3NJeElEX2w0YjFtNnUtQVFvRDhqNEFDZzNDUGdvSXg5Ykx3WUNKenFwX0NnUHlQZ0FLRGNJLUNnaW83T2pqZzVPTHZEOEtBX0ktQUFvTndqNEtDTE9venZmQThybVhXd29EOGo0QUNnM0NQZ29JdzZETV9vSFk0cHRCQ2dQeVBnQUtEc0ktQ3dqbW9QbURpcHVPel80QkNnUHlQZ0FLRGNJLUNnalY4THpEazlfOTRCWUtBX0ktQUFvT3dqNExDTXVZNU9YZzE3ejV2d0VLQV9JLUFBb053ajRLQ1A3eHZiSGswTnVuYWdvRDhqNEFDZzdDUGdzSXFQYVU5ZGp2Ml96S0FRb0Q4ajRBQ2c3Q1Bnc0lfSW1acUtQOTlfQ09BUW9EOGo0QUNnM0NQZ29JeGRtNzlZS3prcUFqQ2dQeVBnQUtEY0ktQ2dpZ3FJMkg0UENRX2s0S0FfSS1BQW9Pd2o0TENQV0V5NV9ZeDhERl9nRUtBX0ktQUFvT3dqNExDTzJid3VuV3BPX3ppd0VLQV9JLUFBb2gwajRlQ2h4U1JFTk5WVU5ZV0RCU1YwOUpRbXAwTkc4emVtbElkUzAyWVRWQkNnUHlQZ0FLRGNJLUNnaXpqcXZwcDh5MWwwMEtBX0ktQUFvTndqNEtDTFhWbl83dHhfWDJOUW9EOGo0QUNnN0NQZ3NJNWR5ZWc1NjZyUGUwQVJJVUFBSUVCZ2dLREE0UUVoUVdHQm9jSGlBaUpDWWFCQWdBRUFFYUJBZ0NFQU1hQkFnRUVBVWFCQWdHRUFjYUJBZ0lFQWthQkFnS0VBc2FCQWdNRUEwYUJBZ09FQThhQkFnUUVCRWFCQWdTRUJNYUJBZ1VFQlVhQkFnV0VCY2FCQWdZRUJrYUJBZ2FFQnNhQkFnY0VCMGFCQWdlRUI4YUJBZ2dFQ0VhQkFnaUVDTWFCQWdrRUNVYUJBZ21FQ2NxRkFBQ0JBWUlDZ3dPRUJJVUZoZ2FIQjRnSWlRbWoPd2F0Y2gtbmV4dC1mZWVk").await.unwrap());
}
}