This repository has been archived on 2026-05-27. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
rustypipe/src/client/video_details.rs
2022-09-24 18:50:52 +02:00

1103 lines
38 KiB
Rust

use std::convert::TryFrom;
use anyhow::{anyhow, bail, Result};
use reqwest::Method;
use serde::Serialize;
use crate::{
model::{
Channel, ChannelId, Chapter, Comment, Language, Paginator, RecommendedVideo, VideoDetails,
},
serializer::MapResult,
timeago,
util::{self, TryRemove},
};
use super::{
response::{self, IconType, IsLive},
ClientType, MapResponse, RustyPipeQuery, YTContext,
};
#[derive(Debug, Serialize)]
struct QVideo {
context: YTContext,
/// 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(Debug, Serialize)]
struct QVideoCont {
context: YTContext,
continuation: String,
}
impl RustyPipeQuery {
pub async fn video_details(self, video_id: &str) -> Result<VideoDetails> {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QVideo {
context,
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
};
self.execute_request::<response::VideoDetails, _, _>(
ClientType::Desktop,
"video_details",
video_id,
Method::POST,
"next",
&request_body,
)
.await
}
pub async fn video_recommendations(self, ctoken: &str) -> Result<Paginator<RecommendedVideo>> {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QVideoCont {
context,
continuation: ctoken.to_owned(),
};
self.execute_request::<response::VideoRecommendations, _, _>(
ClientType::Desktop,
"video_recommendations",
ctoken,
Method::POST,
"next",
&request_body,
)
.await
}
pub async fn video_comments(self, ctoken: &str) -> Result<Paginator<Comment>> {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QVideoCont {
context,
continuation: ctoken.to_owned(),
};
self.execute_request::<response::VideoComments, _, _>(
ClientType::Desktop,
"video_comments",
ctoken,
Method::POST,
"next",
&request_body,
)
.await
}
}
impl MapResponse<VideoDetails> for response::VideoDetails {
fn map_response(
self,
id: &str,
lang: crate::model::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<VideoDetails>> {
let mut warnings = Vec::new();
let video_id = self.current_video_endpoint.watch_endpoint.video_id;
if id != video_id {
bail!("got wrong playlist id {}, expected {}", video_id, id);
}
let mut primary_results = self
.contents
.two_column_watch_next_results
.results
.results
.contents;
warnings.append(&mut primary_results.warnings);
let mut primary_info = None;
let mut secondary_info = None;
let mut comment_count_section = None;
let mut comment_ctoken_section = None;
primary_results.c.into_iter().for_each(|r| match r {
response::video_details::VideoResultsItem::VideoPrimaryInfoRenderer { .. } => {
primary_info = Some(r);
}
response::video_details::VideoResultsItem::VideoSecondaryInfoRenderer { .. } => {
secondary_info = Some(r);
}
response::video_details::VideoResultsItem::ItemSectionRenderer(section) => {
match section {
response::video_details::ItemSection::CommentsEntryPoint { mut contents } => {
comment_count_section = contents.try_swap_remove(0);
}
response::video_details::ItemSection::CommentItemSection { mut contents } => {
comment_ctoken_section = contents.try_swap_remove(0);
}
response::video_details::ItemSection::None => {}
}
}
response::video_details::VideoResultsItem::None => {}
});
let (title, view_count, like_count, publish_date, publish_date_txt, is_live) =
match primary_info {
Some(response::video_details::VideoResultsItem::VideoPrimaryInfoRenderer {
title,
view_count,
video_actions,
date_text,
}) => {
let like_btn = video_actions
.menu_renderer
.top_level_buttons
.into_iter()
.find_map(|button| {
let btn = match button {
response::video_details::TopLevelButton::ToggleButtonRenderer(btn) => btn,
response::video_details::TopLevelButton::SegmentedLikeDislikeButtonRenderer { like_button } => like_button.toggle_button_renderer,
};
match btn.default_icon.icon_type {
IconType::Like => Some(btn),
_ => None
}
});
(
title,
// view count field contains `No views` if the view count is zero
util::parse_numeric(&view_count.video_view_count_renderer.view_count)
.unwrap_or_default(),
// accessibility_data contains no digits if the like count is hidden,
// so we ignore parse errors here for now
like_btn.and_then(|btn| util::parse_numeric(&btn.accessibility_data).ok()),
timeago::parse_textual_date_or_warn(lang, &date_text, &mut warnings),
date_text,
view_count.video_view_count_renderer.is_live,
)
}
_ => bail!("could not find primary_info"),
};
let comment_count = comment_count_section.and_then(|s| {
util::parse_large_numstr::<u32>(
&s.comments_entry_point_header_renderer.comment_count,
lang,
)
});
let comment_ctoken = comment_ctoken_section.map(|s| {
s.continuation_item_renderer
.continuation_endpoint
.continuation_command
.token
});
let (owner, description, is_ccommons) = match secondary_info {
Some(response::video_details::VideoResultsItem::VideoSecondaryInfoRenderer {
owner,
description,
attributed_description,
metadata_row_container,
}) => {
let is_ccommons = metadata_row_container
.map(|c| {
c.metadata_row_container_renderer.rows.iter().any(|cr| {
cr.metadata_row_renderer.contents.iter().any(|links| {
links.0.iter().any(|link| match link {
crate::serializer::text::TextComponent::Web {
text: _,
url,
} => url == "https://www.youtube.com/t/creative_commons",
_ => false,
})
})
})
})
.unwrap_or_default();
let desc = description
.or(attributed_description)
.unwrap_or_default()
.into();
(owner.video_owner_renderer, desc, is_ccommons)
}
_ => bail!("could not find secondary_info"),
};
let (channel_id, channel_name) = match owner.title {
crate::serializer::text::TextComponent::Browse {
text,
page_type,
browse_id,
} => match page_type {
crate::serializer::text::PageType::Channel => (browse_id, text),
_ => bail!("invalid channel link type"),
},
_ => bail!("invalid channel link"),
};
let recommended = self
.contents
.two_column_watch_next_results
.secondary_results
.and_then(|sr| {
sr.secondary_results.results.map(|r| {
let mut res = map_recommendations(r, lang);
warnings.append(&mut res.warnings);
res.c
})
})
.unwrap_or_default();
let mut engagement_panels = self.engagement_panels;
warnings.append(&mut engagement_panels.warnings);
let mut chapter_panel = None;
let mut comment_panel = None;
engagement_panels.c.into_iter().for_each(|panel| match panel.engagement_panel_section_list_renderer {
response::video_details::EngagementPanelRenderer::EngagementPanelMacroMarkersDescriptionChapters { content } => {
chapter_panel = Some(content);
},
response::video_details::EngagementPanelRenderer::EngagementPanelCommentsSection { header } => {
comment_panel = Some(header);
},
response::video_details::EngagementPanelRenderer::None => {},
});
let chapters = chapter_panel
.map(|chapters| {
let mut content = chapters.macro_markers_list_renderer.contents;
warnings.append(&mut content.warnings);
content
.c
.into_iter()
.map(|item| Chapter {
title: item.macro_markers_list_item_renderer.title,
position: item
.macro_markers_list_item_renderer
.on_tap
.watch_endpoint
.start_time_seconds,
thumbnail: item.macro_markers_list_item_renderer.thumbnail.into(),
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let latest_comments_ctoken = comment_panel.and_then(|comments| {
let mut items = comments
.engagement_panel_title_header_renderer
.menu
.sort_filter_sub_menu_renderer
.sub_menu_items;
items
.try_swap_remove(1)
.map(|c| c.service_endpoint.continuation_command.token)
});
Ok(MapResult {
c: VideoDetails {
id: video_id,
title,
description,
channel: Channel {
id: channel_id,
name: channel_name,
avatar: owner.thumbnail.into(),
verification: owner.badges.into(),
subscriber_count: owner
.subscriber_count_text
.and_then(|txt| util::parse_large_numstr(&txt, lang)),
},
view_count,
like_count,
publish_date,
publish_date_txt,
is_live,
is_ccommons,
chapters,
recommended,
top_comments: Paginator::new(comment_count, Vec::new(), comment_ctoken),
latest_comments: Paginator::new(comment_count, Vec::new(), latest_comments_ctoken),
},
warnings,
})
}
}
impl MapResponse<Paginator<RecommendedVideo>> for response::VideoRecommendations {
fn map_response(
self,
_id: &str,
lang: crate::model::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Paginator<RecommendedVideo>>> {
let mut endpoints = self.on_response_received_endpoints;
let cont = some_or_bail!(
endpoints.try_swap_remove(0),
Err(anyhow!("no continuation endpoint"))
);
Ok(map_recommendations(
cont.append_continuation_items_action.continuation_items,
lang,
))
}
}
impl MapResponse<Paginator<Comment>> for response::VideoComments {
fn map_response(
self,
_id: &str,
lang: crate::model::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Paginator<Comment>>> {
let mut warnings = self.on_response_received_endpoints.warnings;
let mut comments = Vec::new();
let mut comment_count = None;
let mut ctoken = None;
self.on_response_received_endpoints
.c
.into_iter()
.for_each(|citem| {
let mut items = citem.append_continuation_items_action.continuation_items;
warnings.append(&mut items.warnings);
items.c.into_iter().for_each(|item| match item {
response::video_details::CommentListItem::CommentThreadRenderer {
comment,
replies,
rendering_priority,
} => {
let mut res = map_comment(
comment.comment_renderer,
Some(replies),
rendering_priority,
lang,
);
comments.push(res.c);
warnings.append(&mut res.warnings)
}
response::video_details::CommentListItem::CommentRenderer(comment) => {
let mut res = map_comment(
comment,
None,
response::video_details::CommentPriority::RenderingPriorityUnknown,
lang,
);
comments.push(res.c);
warnings.append(&mut res.warnings)
}
response::video_details::CommentListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token);
}
response::video_details::CommentListItem::CommentsHeaderRenderer {
count_text,
} => {
comment_count = count_text.and_then(|txt| {
util::parse_numeric_or_warn::<u32>(&txt, &mut warnings)
});
}
});
});
Ok(MapResult {
c: Paginator::new(comment_count, comments, ctoken),
warnings,
})
}
}
fn map_recommendations(
r: MapResult<Vec<response::VideoListItem>>,
lang: Language,
) -> MapResult<Paginator<RecommendedVideo>> {
let mut warnings = r.warnings;
let mut ctoken = None;
let items =
r.c.into_iter()
.filter_map(|item| match item {
response::VideoListItem::CompactVideoRenderer(video) => {
match ChannelId::try_from(video.channel) {
Ok(channel) => Some(RecommendedVideo {
id: video.video_id,
title: video.title,
length: video.length_text.and_then(|txt| {
util::parse_video_length_or_warn(&txt, &mut warnings)
}),
thumbnail: video.thumbnail.into(),
channel: Channel {
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_or_warn(lang, txt, &mut 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: video.badges.is_live(),
}),
Err(e) => {
warnings.push(e.to_string());
None
}
}
}
response::VideoListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
_ => None,
})
.collect::<Vec<_>>();
MapResult {
c: Paginator::new(None, items, ctoken),
warnings,
}
}
fn map_comment(
c: response::video_details::CommentRenderer,
replies: Option<response::video_details::Replies>,
priority: response::video_details::CommentPriority,
lang: Language,
) -> MapResult<Comment> {
let mut warnings = Vec::new();
let mut reply_ctoken = None;
let replies = replies.map(|replies| {
replies
.comment_replies_renderer
.contents
.into_iter()
.filter_map(|item| match item {
response::video_details::CommentListItem::CommentRenderer(comment) => {
let mut res = map_comment(
comment,
None,
response::video_details::CommentPriority::default(),
lang,
);
warnings.append(&mut res.warnings);
Some(res.c)
}
response::video_details::CommentListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
reply_ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
_ => None,
})
.collect::<Vec<_>>()
});
MapResult {
c: Comment {
id: c.comment_id,
text: c.content_text.into(),
author: match (c.author_endpoint, c.author_text) {
(Some(aep), Some(name)) => Some(Channel {
id: aep.browse_endpoint.browse_id,
name,
avatar: c.author_thumbnail.into(),
verification: c
.author_comment_badge
.map(|b| b.author_comment_badge_renderer.icon.into())
.unwrap_or_default(),
subscriber_count: None,
}),
_ => None,
},
publish_date: timeago::parse_timeago_or_warn(
lang,
&c.published_time_text,
&mut warnings,
),
publish_date_txt: c.published_time_text,
like_count: util::parse_numeric_or_warn(
&c.action_buttons
.comment_action_buttons_renderer
.like_button
.toggle_button_renderer
.accessibility_data,
&mut warnings,
),
reply_count: c.reply_count,
replies: replies
.map(|items| Paginator::new(Some(c.reply_count), items, reply_ctoken))
.unwrap_or_default(),
by_owner: c.author_is_channel_owner,
pinned: priority
== response::video_details::CommentPriority::RenderingPriorityPinnedComment,
hearted: c
.action_buttons
.comment_action_buttons_renderer
.creator_heart
.map(|h| h.creator_heart_renderer.is_hearted)
.unwrap_or_default(),
},
warnings,
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use chrono::Datelike;
use rstest::rstest;
use crate::{
client::{response, MapResponse, RustyPipe},
model::{richtext::ToPlaintext, Language, Verification},
};
#[tokio::test]
async fn get_video_details() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "ZeerrnuLi5E");
assert_eq!(details.title, "aespa 에스파 'Black Mamba' MV");
let desc = details.description.to_plaintext();
assert!(
desc.contains("Listen and download aespa's debut single \"Black Mamba\""),
"bad description: {}",
desc
);
assert_eq!(details.channel.id, "UCEf_Bc-KVd7onSeifS3py9g");
assert_eq!(details.channel.name, "SMTOWN");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::Verified);
assert!(
details.channel.subscriber_count.unwrap() > 30000000,
"expected >30M subs, got {}",
details.channel.subscriber_count.unwrap()
);
assert!(
details.view_count > 232000000,
"expected > 232M views, got {}",
details.view_count
);
assert!(
details.like_count.unwrap() > 4000000,
"expected > 4M likes, got {}",
details.like_count.unwrap()
);
let date = details.publish_date.unwrap();
assert_eq!(date.year(), 2020);
assert_eq!(date.month(), 11);
assert_eq!(date.day(), 17);
assert!(!details.is_live);
assert!(!details.is_ccommons);
assert!(!details.recommended.items.is_empty());
assert!(!details.recommended.is_exhausted());
assert!(
details.top_comments.count.unwrap() > 700000,
"expected > 700K comments, got {}",
details.top_comments.count.unwrap()
);
assert!(!details.top_comments.is_exhausted());
assert!(!details.latest_comments.is_exhausted());
}
#[tokio::test]
async fn get_video_details_music() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("XuM2onMGvTI").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "XuM2onMGvTI");
assert_eq!(details.title, "Gäa");
let desc = details.description.to_plaintext();
assert!(desc.contains("Gäa · Oonagh"), "bad description: {}", desc);
assert_eq!(details.channel.id, "UCVGvnqB-5znqPSbMGlhF4Pw");
assert_eq!(details.channel.name, "Sentamusic");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::Artist);
assert!(
details.channel.subscriber_count.unwrap() > 33000,
"expected >33K subs, got {}",
details.channel.subscriber_count.unwrap()
);
assert!(
details.view_count > 20309,
"expected > 20309 views, got {}",
details.view_count
);
assert!(
details.like_count.unwrap() > 145,
"expected > 145 likes, got {}",
details.like_count.unwrap()
);
let date = details.publish_date.unwrap();
assert_eq!(date.year(), 2020);
assert_eq!(date.month(), 8);
assert_eq!(date.day(), 6);
assert!(!details.is_live);
assert!(!details.is_ccommons);
assert!(!details.recommended.items.is_empty());
assert!(!details.recommended.is_exhausted());
// Comments are disabled for this video
assert_eq!(details.top_comments.count, Some(0));
assert_eq!(details.latest_comments.count, Some(0));
assert!(details.top_comments.is_empty());
assert!(details.latest_comments.is_empty());
}
#[tokio::test]
async fn get_video_details_ccommons() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("0rb9CfOvojk").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "0rb9CfOvojk");
assert_eq!(
details.title,
"BahnMining - Pünktlichkeit ist eine Zier (David Kriesel)"
);
let desc = details.description.to_plaintext();
assert!(
desc.contains("Seit Anfang 2019 hat David jeden einzelnen Halt jeder einzelnen Zugfahrt auf jedem einzelnen Fernbahnhof in ganz Deutschland"),
"bad description: {}",
desc
);
assert_eq!(details.channel.id, "UC2TXq_t06Hjdr2g_KdKpHQg");
assert_eq!(details.channel.name, "media.ccc.de");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::None);
assert!(
details.channel.subscriber_count.unwrap() > 170000,
"expected >170K subs, got {}",
details.channel.subscriber_count.unwrap()
);
assert!(
details.view_count > 2517358,
"expected > 2517358 views, got {}",
details.view_count
);
assert!(
details.like_count.unwrap() > 52330,
"expected > 52330 likes, got {}",
details.like_count.unwrap()
);
let date = details.publish_date.unwrap();
assert_eq!(date.year(), 2019);
assert_eq!(date.month(), 12);
assert_eq!(date.day(), 29);
assert!(!details.is_live);
assert!(details.is_ccommons);
assert!(!details.recommended.items.is_empty());
assert!(!details.recommended.is_exhausted());
assert!(
details.top_comments.count.unwrap() > 2199,
"expected > 2199 comments, got {}",
details.top_comments.count.unwrap()
);
assert!(!details.top_comments.is_exhausted());
assert!(!details.latest_comments.is_exhausted());
}
#[tokio::test]
async fn get_video_details_chapters() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("nFDBxBUfE74").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "nFDBxBUfE74");
assert_eq!(details.title, "The Prepper PC");
let desc = details.description.to_plaintext();
assert!(
desc.contains("These days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse"),
"bad description: {}",
desc
);
assert_eq!(details.channel.id, "UCXuqSBlHAE6Xw-yeJA0Tunw");
assert_eq!(details.channel.name, "Linus Tech Tips");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::Verified);
assert!(
details.channel.subscriber_count.unwrap() > 14700000,
"expected >14.7M subs, got {}",
details.channel.subscriber_count.unwrap()
);
assert!(
details.view_count > 1157262,
"expected > 1157262 views, got {}",
details.view_count
);
assert!(
details.like_count.unwrap() > 54670,
"expected > 54670 likes, got {}",
details.like_count.unwrap()
);
let date = details.publish_date.unwrap();
assert_eq!(date.year(), 2022);
assert_eq!(date.month(), 9);
assert_eq!(date.day(), 15);
assert!(!details.is_live);
assert!(!details.is_ccommons);
insta::assert_ron_snapshot!(details.chapters, {
"[].thumbnail" => insta::dynamic_redaction(move |value, _path| {
assert!(!value.as_slice().unwrap().is_empty());
"[ok]"
}),
}, @r###"
[
Chapter(
title: "Intro",
position: 0,
thumbnail: "[ok]",
),
Chapter(
title: "The PC Built for Super Efficiency",
position: 42,
thumbnail: "[ok]",
),
Chapter(
title: "Our BURIAL ENCLOSURE?!",
position: 161,
thumbnail: "[ok]",
),
Chapter(
title: "Our Power Solution (Thanks Jackery!)",
position: 211,
thumbnail: "[ok]",
),
Chapter(
title: "Diggin\' Holes",
position: 287,
thumbnail: "[ok]",
),
Chapter(
title: "Colonoscopy?",
position: 330,
thumbnail: "[ok]",
),
Chapter(
title: "Diggin\' like a man",
position: 424,
thumbnail: "[ok]",
),
Chapter(
title: "The world\'s worst woodsman",
position: 509,
thumbnail: "[ok]",
),
Chapter(
title: "Backyard cable management",
position: 543,
thumbnail: "[ok]",
),
Chapter(
title: "Time to bury this boy",
position: 602,
thumbnail: "[ok]",
),
Chapter(
title: "Solar Power Generation",
position: 646,
thumbnail: "[ok]",
),
Chapter(
title: "Issues",
position: 697,
thumbnail: "[ok]",
),
Chapter(
title: "First Play Test",
position: 728,
thumbnail: "[ok]",
),
Chapter(
title: "Conclusion",
position: 800,
thumbnail: "[ok]",
),
]
"###);
assert!(!details.recommended.items.is_empty());
assert!(!details.recommended.is_exhausted());
assert!(
details.top_comments.count.unwrap() > 3199,
"expected > 3199 comments, got {}",
details.top_comments.count.unwrap()
);
assert!(!details.top_comments.is_exhausted());
assert!(!details.latest_comments.is_exhausted());
}
#[tokio::test]
async fn get_video_details_live() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("86YLFOog4GM").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "86YLFOog4GM");
assert_eq!(
details.title,
"🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS"
);
let desc = details.description.to_plaintext();
assert!(
desc.contains("Live NASA - Views Of Earth from Space"),
"bad description: {}",
desc
);
assert_eq!(details.channel.id, "UCakgsb0w7QB0VHdnCc-OVEA");
assert_eq!(details.channel.name, "Space Videos");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::Verified);
assert!(
details.channel.subscriber_count.unwrap() > 5500000,
"expected >5.5M subs, got {}",
details.channel.subscriber_count.unwrap()
);
assert!(
details.view_count > 10,
"expected > 10 views, got {}",
details.view_count
);
assert!(
details.like_count.unwrap() > 872290,
"expected > 872290 likes, got {}",
details.like_count.unwrap()
);
let date = details.publish_date.unwrap();
assert_eq!(date.year(), 2021);
assert_eq!(date.month(), 9);
assert_eq!(date.day(), 23);
assert!(details.is_live);
assert!(!details.is_ccommons);
assert!(!details.recommended.items.is_empty());
assert!(!details.recommended.is_exhausted());
// No comments because livestream
assert_eq!(details.top_comments.count, Some(0));
assert_eq!(details.latest_comments.count, Some(0));
assert!(details.top_comments.is_empty());
assert!(details.latest_comments.is_empty());
}
#[tokio::test]
async fn get_video_details_agegate() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("HRKu0cvrr_o").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "HRKu0cvrr_o");
assert_eq!(
details.title,
"AlphaOmegaSin Fanboy Logic: Likes/Dislikes Disabled = Point Invalid Lol wtf?"
);
insta::assert_ron_snapshot!(details.description, @"RichText([])");
assert_eq!(details.channel.id, "UCQT2yul0lr6Ie9qNQNmw-sg");
assert_eq!(details.channel.name, "PrinceOfFALLEN");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::None);
assert!(
details.channel.subscriber_count.unwrap() > 1400,
"expected >1400 subs, got {}",
details.channel.subscriber_count.unwrap()
);
assert!(
details.view_count > 200,
"expected > 200 views, got {}",
details.view_count
);
assert!(details.like_count.is_none(), "like count not hidden");
let date = details.publish_date.unwrap();
assert_eq!(date.year(), 2019);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 2);
assert!(!details.is_live);
assert!(!details.is_ccommons);
// No recommendations because agegate
assert_eq!(details.recommended.count, Some(0));
assert!(details.recommended.items.is_empty());
}
#[tokio::test]
async fn get_video_recommendations() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
let next_recommendations = details.recommended.next(rp.query()).await.unwrap().unwrap();
dbg!(&next_recommendations);
assert!(
next_recommendations.items.len() > 10,
"expected > 10 next recommendations, got {}",
next_recommendations.items.len()
);
assert!(!next_recommendations.is_exhausted());
}
#[tokio::test]
async fn get_video_comments() {
let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
let top_comments = details
.top_comments
.next(rp.query())
.await
.unwrap()
.unwrap();
assert!(
top_comments.items.len() > 10,
"expected > 10 next comments, got {}",
top_comments.items.len()
);
assert!(!top_comments.is_exhausted());
let n_comments = top_comments.count.unwrap();
assert!(
n_comments > 700000,
"expected > 700k comments, got {}",
n_comments
);
// Comment count should be exact after fetching first page
assert!(n_comments % 1000 != 0);
let latest_comments = details
.latest_comments
.next(rp.query())
.await
.unwrap()
.unwrap();
assert!(
latest_comments.items.len() > 10,
"expected > 10 next comments, got {}",
latest_comments.items.len()
);
assert!(!latest_comments.is_exhausted());
}
#[rstest]
#[case::mv("mv", "ZeerrnuLi5E")]
#[case::music("music", "XuM2onMGvTI")]
#[case::ccommons("ccommons", "0rb9CfOvojk")]
#[case::chapters("chapters", "nFDBxBUfE74")]
#[case::live("live", "86YLFOog4GM")]
#[case::agegate("agegate", "HRKu0cvrr_o")]
#[case::newdesc("newdesc", "ZeerrnuLi5E")]
fn t_map_video_details(#[case] name: &str, #[case] id: &str) {
let filename = format!("testfiles/video_details/video_details_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let details: response::VideoDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = details.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_video_details_{}", name), map_res.c, {
".publish_date" => "[date]",
".recommended.items[].publish_date" => "[date]",
});
}
#[test]
fn t_map_recommendations() {
let json_path = Path::new("testfiles/video_details/recommendations.json");
let json_file = File::open(json_path).unwrap();
let recommendations: response::VideoRecommendations =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = recommendations
.map_response("", Language::En, None)
.unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!("map_recommendations", map_res.c, {
".items[].publish_date" => "[date]",
});
}
#[rstest]
#[case::top("top")]
#[case::latest("latest")]
fn t_map_comments(#[case] name: &str) {
let filename = format!("testfiles/video_details/comments_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let comments: response::VideoComments =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = comments.map_response("", Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_comments_{}", name), map_res.c, {
".items[].publish_date" => "[date]",
});
}
}