feat: add is_live to video details

This commit is contained in:
ThetaDev 2022-09-20 21:22:18 +02:00
parent 8c1e7bf6ac
commit 584d6aa3f5
9 changed files with 28819 additions and 7735 deletions

View file

@ -1,3 +1,3 @@
report2yaml: report2yaml:
yq e -Pi rustypipe_reports/*.json mkdir -p rustypipe_reports/conv
for f in rustypipe_reports/*.json; do mv $f rustypipe_reports/`basename $f .json`.yaml; done; for f in rustypipe_reports/*.json; do yq '.http_request.resp_body' $f | yq -o json -P > rustypipe_reports/conv/`basename $f .json`_body.json; yq e -Pi $f; mv $f rustypipe_reports/conv/`basename $f .json`.yaml; done;

View file

@ -123,10 +123,12 @@ async fn playlist(testfiles: &Path) {
async fn video_details(testfiles: &Path) { async fn video_details(testfiles: &Path) {
for (name, id) in [ for (name, id) in [
("music", "MZOgTu2dMTg"), ("music", "XuM2onMGvTI"),
("mv", "ZeerrnuLi5E"), ("mv", "ZeerrnuLi5E"),
("ccommons", "0rb9CfOvojk"), ("ccommons", "0rb9CfOvojk"),
("chapters", "nFDBxBUfE74"), ("chapters", "nFDBxBUfE74"),
("agegate", "XuM2onMGvTI"),
("live", "86YLFOog4GM"),
] { ] {
let mut json_path = testfiles.to_path_buf(); let mut json_path = testfiles.to_path_buf();
json_path.push("video_details"); json_path.push("video_details");
@ -156,33 +158,4 @@ async fn comments_top(testfiles: &Path) {
.video_comments(&details.top_comments.ctoken.unwrap()) .video_comments(&details.top_comments.ctoken.unwrap())
.await .await
.unwrap(); .unwrap();
// rp.query().video_comments("Eg0SC0lITnpPSGk4c0pzGAYyJSIRIgtJSE56T0hpOHNKczAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D").await.unwrap();
// Desktop 1
// top: Eg0SC0lITnpPSGk4c0pzGAYyJSIRIgtJSE56T0hpOHNKczAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D
// latest: Eg0SC0lITnpPSGk4c0pzGAYyJSIRIgtJSE56T0hpOHNKczABeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D
// shows count
// Desktop 2
// top: Eg0SC0lITnpPSGk4c0pzGAYyVSIuIgtJSE56T0hpOHNKczAAeAKqAhpVZ3lVZG5WQnBNR09tMnVMR3o1NEFhQUJBZzABQiFlbmdhZ2VtZW50LXBhbmVsLWNvbW1lbnRzLXNlY3Rpb24%3D
// shows no count
// latest: Eg0SC0lITnpPSGk4c0pzGAYyOCIRIgtJSE56T0hpOHNKczABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u
// shows no count
// cont: Eg0SC0lITnpPSGk4c0pzGAYyJSIRIgtJSE56T0hpOHNKczAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D
// shows count
}
#[tokio::test]
async fn test() {
let id = "IHNzOHi8sJs";
let rp = RustyPipe::new();
let details = rp.query().video_details(id).await.unwrap();
let ctoken_top = details.top_comments.ctoken;
let ctoken_latest = details.latest_comments.ctoken;
dbg!(ctoken_top);
dbg!(ctoken_latest);
} }

View file

@ -20,6 +20,9 @@ Album with unknown artists: https://music.youtube.com/playlist?list=OLAK5uy_mEX9
Comment by artist: 3pv_rHKnwAs Comment by artist: 3pv_rHKnwAs
Comments disabled: XuM2onMGvTI
Likes hidden:
# Playlists # Playlists
962 Songs: PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI 962 Songs: PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI
97 Songs, YTM: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk 97 Songs, YTM: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk

View file

@ -47,7 +47,9 @@ pub struct TwoColumnWatchNextResults {
/// Metadata about the video /// Metadata about the video
pub results: VideoResultsWrap, pub results: VideoResultsWrap,
/// Video recommendations /// Video recommendations
pub secondary_results: RecommendationResultsWrap, ///
/// Can be `None` for age-restricted videos
pub secondary_results: Option<RecommendationResultsWrap>,
} }
/// Metadata about the video /// Metadata about the video
@ -85,6 +87,7 @@ pub enum VideoResultsItem {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
VideoSecondaryInfoRenderer { VideoSecondaryInfoRenderer {
owner: VideoOwner, owner: VideoOwner,
#[serde(default)]
#[serde_as(as = "Text")] #[serde_as(as = "Text")]
description: String, description: String,
/// Additional metadata (e.g. Creative Commons License) /// Additional metadata (e.g. Creative Commons License)
@ -114,6 +117,8 @@ pub struct ViewCountRenderer {
/// View count (`232,975,196 views`) /// View count (`232,975,196 views`)
#[serde_as(as = "Text")] #[serde_as(as = "Text")]
pub view_count: String, pub view_count: String,
#[serde(default)]
pub is_live: bool,
} }
/// Like/Dislike buttons /// Like/Dislike buttons
@ -129,7 +134,23 @@ pub struct VideoActions {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct VideoActionsMenu { pub struct VideoActionsMenu {
#[serde_as(as = "VecSkipError<_>")] #[serde_as(as = "VecSkipError<_>")]
pub top_level_buttons: Vec<ToggleButtonWrap>, pub top_level_buttons: Vec<TopLevelButton>,
}
/// The different TopLevelButtons
///
/// YouTube seems to be A/B testing the SegmentedLikeDislikeButtonRenderer
///
/// See: https://github.com/TeamNewPipe/NewPipeExtractor/pull/926
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TopLevelButton {
ToggleButtonRenderer(ToggleButton),
#[serde(rename_all = "camelCase")]
SegmentedLikeDislikeButtonRenderer {
like_button: ToggleButtonWrap,
},
} }
/// Like/Dislike button /// Like/Dislike button
@ -147,6 +168,8 @@ pub struct ToggleButton {
/// Icon type: `LIKE` / `DISLIKE` /// Icon type: `LIKE` / `DISLIKE`
pub default_icon: Icon, pub default_icon: Icon,
/// Number of likes (`like this video along with 4,010,156 other people`) /// Number of likes (`like this video along with 4,010,156 other people`)
///
/// Contains no digits (e.g. `I like this`) if likes are hidden by the creator.
#[serde_as(as = "AccessibilityText")] #[serde_as(as = "AccessibilityText")]
pub accessibility_data: String, pub accessibility_data: String,
} }
@ -257,8 +280,9 @@ pub struct RecommendationResultsWrap {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RecommendationResults { pub struct RecommendationResults {
#[serde_as(as = "VecLogError<_>")] /// Can be `None` for age-restricted videos
pub results: MapResult<Vec<VideoListItem<RecommendedVideo>>>, #[serde_as(as = "Option<VecLogError<_>>")]
pub results: Option<MapResult<Vec<VideoListItem<RecommendedVideo>>>>,
} }
/// Video recommendation item /// Video recommendation item
@ -313,7 +337,7 @@ pub enum EngagementPanelRenderer {
/// Ignored items: /// Ignored items:
/// - `engagement-panel-ads` /// - `engagement-panel-ads`
/// - `engagement-panel-structured-description` /// - `engagement-panel-structured-description`
/// (Desctiption already included in `VideoSecondaryInfoRenderer`) /// (Description already included in `VideoSecondaryInfoRenderer`)
/// - `engagement-panel-searchable-transcript` /// - `engagement-panel-searchable-transcript`
/// (basically video subtitles in a different format) /// (basically video subtitles in a different format)
#[serde(other, deserialize_with = "ignore_any")] #[serde(other, deserialize_with = "ignore_any")]
@ -378,11 +402,13 @@ pub struct CommentItemSectionHeader {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CommentItemSectionHeaderRenderer { pub struct CommentItemSectionHeaderRenderer {
/// Average comment count (e.g. `81`, `2.2K`, `705K`) /// Approximate comment count (e.g. `81`, `2.2K`, `705K`)
/// ///
/// The accurate count is included in the first comment response. /// The accurate count is included in the first comment response.
#[serde_as(as = "Text")] ///
pub contextual_info: String, /// Is `None` if there are no comments.
#[serde_as(as = "Option<Text>")]
pub contextual_info: Option<String>,
pub menu: CommentItemSectionHeaderMenu, pub menu: CommentItemSectionHeaderMenu,
} }

View file

@ -138,33 +138,41 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
response::video_details::VideoResultsItem::None => {} response::video_details::VideoResultsItem::None => {}
}); });
let (title, view_count, like_count, publish_date, publish_date_txt) = match primary_info { let (title, view_count, like_count, publish_date, publish_date_txt, is_live) =
Some(response::video_details::VideoResultsItem::VideoPrimaryInfoRenderer { match primary_info {
title, Some(response::video_details::VideoResultsItem::VideoPrimaryInfoRenderer {
view_count,
video_actions,
date_text,
}) => {
let like_btn = some_or_bail!(
video_actions
.menu_renderer
.top_level_buttons
.into_iter()
.find(|button| {
button.toggle_button_renderer.default_icon.icon_type == IconType::Like
}),
Err(anyhow!("could not find like button"))
);
(
title, title,
util::parse_numeric(&view_count.video_view_count_renderer.view_count)?, view_count,
util::parse_numeric(&like_btn.toggle_button_renderer.accessibility_data)?, video_actions,
timeago::parse_textual_date_or_warn(lang, &date_text, &mut warnings),
date_text, date_text,
) }) => {
} let like_btn = video_actions
_ => bail!("could not find primary_info"), .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,
util::parse_numeric(&view_count.video_view_count_renderer.view_count)?,
// 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"),
};
/* /*
TODO: use large number parser for this TODO: use large number parser for this
@ -220,15 +228,19 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
_ => bail!("invalid channel link"), _ => bail!("invalid channel link"),
}; };
let mut recommended = map_recommendations( let recommended = self
self.contents .contents
.two_column_watch_next_results .two_column_watch_next_results
.secondary_results .secondary_results
.secondary_results .map(|sr| {
.results, sr.secondary_results.results.map(|r| {
lang, let mut res = map_recommendations(r, lang);
); warnings.append(&mut res.warnings);
warnings.append(&mut recommended.warnings); res.c
})
})
.flatten()
.unwrap_or_default();
let mut engagement_panels = self.engagement_panels; let mut engagement_panels = self.engagement_panels;
warnings.append(&mut engagement_panels.warnings); warnings.append(&mut engagement_panels.warnings);
@ -273,8 +285,9 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
like_count, like_count,
publish_date, publish_date,
publish_date_txt, publish_date_txt,
is_live,
is_ccommons, is_ccommons,
recommended: recommended.c, recommended,
top_comments: Paginator { top_comments: Paginator {
count: None, count: None,
items: Vec::new(), items: Vec::new(),
@ -521,9 +534,9 @@ fn map_comment(
}) })
.unwrap_or_default(), .unwrap_or_default(),
by_owner: c.author_is_channel_owner, by_owner: c.author_is_channel_owner,
is_pinned: priority pinned: priority
== response::video_details::CommentPriority::RenderingPriorityPinnedComment, == response::video_details::CommentPriority::RenderingPriorityPinnedComment,
is_hearted: c hearted: c
.action_buttons .action_buttons
.comment_action_buttons_renderer .comment_action_buttons_renderer
.creator_heart .creator_heart
@ -541,7 +554,7 @@ mod tests {
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn get_video_details() { async fn get_video_details() {
let rp = RustyPipe::builder().strict().build(); let rp = RustyPipe::builder().strict().build();
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap(); let details = rp.query().video_details("HRKu0cvrr_o").await.unwrap();
dbg!(&details); dbg!(&details);
} }

View file

@ -218,16 +218,22 @@ pub struct VideoDetails {
pub title: String, pub title: String,
/// Video description /// Video description
pub description: String, pub description: String,
/// Channel owning the video /// Channel of the video
pub channel: Channel, pub channel: Channel,
/// Number of views /// Number of views
pub view_count: u64, pub view_count: u64,
/// Number of likes /// Number of likes
pub like_count: u32, ///
/// Video publish date. `None` if the date could not be parsed. /// `None` if the like count was hidden by the creator.
pub like_count: Option<u32>,
/// Video publishing date. Start date in case of a livestream.
///
/// `None` if the date could not be parsed.
pub publish_date: Option<DateTime<Local>>, pub publish_date: Option<DateTime<Local>>,
/// Textual video publish date (e.g. `Aug 2, 2013`, depends on language) /// Textual video publishing date (e.g. `Aug 2, 2013`, depends on language)
pub publish_date_txt: String, pub publish_date_txt: String,
/// Is the video a livestream?
pub is_live: bool,
/// Is the video published under the Creative Commons BY 3.0 license? /// Is the video published under the Creative Commons BY 3.0 license?
/// ///
/// Information about the license: /// Information about the license:
@ -237,6 +243,8 @@ pub struct VideoDetails {
/// https://creativecommons.org/licenses/by/3.0/ /// https://creativecommons.org/licenses/by/3.0/
pub is_ccommons: bool, pub is_ccommons: bool,
/// Recommended videos /// Recommended videos
///
/// Note: Recommendations are not available for age-restricted videos
pub recommended: Paginator<RecommendedVideo>, pub recommended: Paginator<RecommendedVideo>,
/// Paginator to fetch comments (most liked first) /// Paginator to fetch comments (most liked first)
pub top_comments: Paginator<Comment>, pub top_comments: Paginator<Comment>,
@ -260,9 +268,11 @@ pub struct RecommendedVideo {
pub length: Option<u32>, pub length: Option<u32>,
/// Video thumbnail /// Video thumbnail
pub thumbnail: Vec<Thumbnail>, pub thumbnail: Vec<Thumbnail>,
/// Channel owning the video /// Channel of the video
pub channel: Channel, pub channel: Channel,
/// Video publish date. `None` if the date could not be parsed. /// Video publishing date.
///
/// `None` if the date could not be parsed.
pub publish_date: Option<DateTime<Local>>, pub publish_date: Option<DateTime<Local>>,
/// Textual video publish date (e.g. `11 months ago`, depends on language) /// Textual video publish date (e.g. `11 months ago`, depends on language)
/// ///
@ -327,7 +337,9 @@ pub struct Comment {
/// ///
/// There may be comments with missing authors (possibly deleted users?). /// There may be comments with missing authors (possibly deleted users?).
pub author: Option<Channel>, pub author: Option<Channel>,
/// Comment publish date. `None` if the date could not be parsed. /// Comment publishing date.
///
/// `None` if the date could not be parsed.
pub publish_date: Option<DateTime<Local>>, pub publish_date: Option<DateTime<Local>>,
/// Textual comment publish date (e.g. `14 hours ago`), depends on language setting /// Textual comment publish date (e.g. `14 hours ago`), depends on language setting
pub publish_date_txt: String, pub publish_date_txt: String,
@ -340,7 +352,7 @@ pub struct Comment {
/// Is the comment from the channel owner? /// Is the comment from the channel owner?
pub by_owner: bool, pub by_owner: bool,
/// Has the channel owner pinned the comment to the top? /// Has the channel owner pinned the comment to the top?
pub is_pinned: bool, pub pinned: bool,
/// Has the channel owner marked the comment with a ❤️ heart ? /// Has the channel owner marked the comment with a ❤️ heart ?
pub is_hearted: bool, pub hearted: bool,
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff