diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index a87873f..23dd8ea 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -27,6 +27,7 @@ pub enum ABTest { TrackViewcount = 8, PlaylistsForShorts = 9, ChannelAboutModal = 10, + LikeButtonViewmodel = 11, } const TESTS_TO_RUN: [ABTest; 3] = [ @@ -100,6 +101,7 @@ pub async fn run_test( ABTest::PlaylistsForShorts => playlists_for_shorts(&query).await, ABTest::TrackViewcount => track_viewcount(&query).await, ABTest::ChannelAboutModal => channel_about_modal(&query).await, + ABTest::LikeButtonViewmodel => like_button_viewmodel(&query).await, } .unwrap(); pb.inc(1); @@ -301,3 +303,19 @@ pub async fn channel_about_modal(rp: &RustyPipeQuery) -> Result { .unwrap(); Ok(!res.contains("\"EgVhYm91dPIGBAoCEgA%3D\"")) } + +pub async fn like_button_viewmodel(rp: &RustyPipeQuery) -> Result { + let res = rp + .raw( + ClientType::Desktop, + "next", + &QVideo { + context: rp.get_context(ClientType::Desktop, true, None).await, + video_id: "ZeerrnuLi5E", + content_check_ok: true, + racy_check_ok: true, + }, + ) + .await?; + Ok(res.contains("\"segmentedLikeDislikeButtonViewModel\"")) +} diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 20efb3b..a6108e8 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -432,3 +432,35 @@ channel metadata has to be fetched. The new modal uses a continuation request with a token which can be easily generated. Attempts to fetch the old about tab with the A/B test enabled will lead to a redirect to the main tab. + +## [11] Like-Button viewmodel + +- **Encountered on:** 03.11.2023 +- **Impact:** 🟢 Low +- **Endpoint:** next + +YouTube introduced an updated date model for the like/dislike buttons. The new model +looks needlessly complex but contains the same parsing-relevant data as the old model +(accessibility text to get like count). + +```json +{ + "segmentedLikeDislikeButtonViewModel": { + "likeButtonViewModel": { + "likeButtonViewModel": { + "toggleButtonViewModel": { + "toggleButtonViewModel": { + "defaultButtonViewModel": { + "buttonViewModel": { + "iconName": "LIKE", + "title": "4.2M", + "accessibilityText": "like this video along with 4,209,059 other people" + } + } + } + } + } + } + } +} +``` diff --git a/src/client/channel.rs b/src/client/channel.rs index 4af14ee..fa4d0ac 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -291,10 +291,14 @@ impl MapResponse for response::ChannelAbout { fn map_response( self, _id: &str, - lang: Language, + _lang: Language, _deobf: Option<&crate::deobfuscate::DeobfData>, _visitor_data: Option<&str>, ) -> Result, ExtractionError> { + // Channel info is always fetched in English. There is no localized data there + // and it allows parsing the country name. + let lang = Language::En; + let ep = self .on_response_received_endpoints .into_iter() diff --git a/src/client/response/video_details.rs b/src/client/response/video_details.rs index c9b4d2e..50373d7 100644 --- a/src/client/response/video_details.rs +++ b/src/client/response/video_details.rs @@ -147,6 +147,46 @@ pub(crate) enum TopLevelButton { SegmentedLikeDislikeButtonRenderer { like_button: ToggleButtonWrap, }, + #[serde(rename_all = "camelCase")] + SegmentedLikeDislikeButtonViewModel { + like_button_view_model: LikeButtonViewModelWrap, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct LikeButtonViewModelWrap { + pub like_button_view_model: LikeButtonViewModel, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct LikeButtonViewModel { + pub toggle_button_view_model: ToggleButtonViewModelWrap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ToggleButtonViewModelWrap { + pub toggle_button_view_model: ToggleButtonViewModel, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ToggleButtonViewModel { + pub default_button_view_model: ButtonViewModelWrap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ButtonViewModelWrap { + pub button_view_model: ButtonViewModel, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ButtonViewModel { + pub accessibility_text: String, } /// Like/Dislike button diff --git a/src/client/video_details.rs b/src/client/video_details.rs index 32b63f4..14949be 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -159,17 +159,20 @@ impl MapResponse for response::VideoDetails { video_actions, date_text, }) => { - let like_btn = video_actions + let like_text = 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, + let (icon, text) = match button { + response::video_details::TopLevelButton::ToggleButtonRenderer(btn) => (btn.default_icon.icon_type, btn.accessibility_data), + response::video_details::TopLevelButton::SegmentedLikeDislikeButtonRenderer { like_button } => (like_button.toggle_button_renderer.default_icon.icon_type, like_button.toggle_button_renderer.accessibility_data), + response::video_details::TopLevelButton::SegmentedLikeDislikeButtonViewModel { like_button_view_model } => { + (IconType::Like, like_button_view_model.like_button_view_model.toggle_button_view_model.toggle_button_view_model.default_button_view_model.button_view_model.accessibility_text) + }, }; - match btn.default_icon.icon_type { - IconType::Like => Some(btn), + match icon { + IconType::Like => Some(text), _ => None } }); @@ -184,7 +187,7 @@ impl MapResponse for response::VideoDetails { .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()), + like_text.and_then(|txt| util::parse_numeric(&txt).ok()), date_text.as_deref().and_then(|txt| { timeago::parse_textual_date_or_warn(lang, txt, &mut warnings) }), diff --git a/src/util/mod.rs b/src/util/mod.rs index 9de438d..c1c4f17 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -362,6 +362,7 @@ where let mut filtered = String::new(); let mut exp = 0; let mut after_point = false; + let mut last_number = false; for c in string.chars() { if c.is_ascii_digit() { @@ -370,6 +371,10 @@ where if after_point { exp -= 1; } + if !last_number { + filtered.push(' '); + last_number = true; + } } else if c == decimal_point && !digits.is_empty() { after_point = true; } else if !matches!( @@ -377,6 +382,7 @@ where '\u{200b}' | '\u{202b}' | '\u{202c}' | '\u{202e}' | '\u{200e}' | '\u{200f}' | '.' | ',' ) { c.to_lowercase().for_each(|c| filtered.push(c)); + last_number = false; } } @@ -636,6 +642,7 @@ pub(crate) mod tests { )] #[case(Language::As, "১ জন গ্ৰাহক", 1)] #[case(Language::Ru, "Зрителей, ожидающих начала трансляции: 6", 6)] + #[case(Language::Si, "වාදන මි4.6ක්", 4_600_000)] fn t_parse_large_numstr(#[case] lang: Language, #[case] string: &str, #[case] expect: u64) { let res = parse_large_numstr::(string, lang).unwrap(); assert_eq!(res, expect);