fix: A/B test 15 (parsing channel shortsLockupViewModel)

This commit is contained in:
ThetaDev 2024-09-10 03:12:16 +02:00
parent ed08f9ff9a
commit 7972df0df4
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
8 changed files with 14241 additions and 23 deletions

View file

@ -34,6 +34,7 @@ pub enum ABTest {
ChannelPageHeader = 12,
MusicPlaylistTwoColumn = 13,
CommentsFrameworkUpdate = 14,
ChannelShortsLockup = 15,
}
/// List of active A/B tests that are run when none is manually specified
@ -110,6 +111,7 @@ pub async fn run_test(
ABTest::ChannelPageHeader => channel_page_header(&query).await,
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
}
.unwrap();
pb.inc(1);
@ -363,3 +365,20 @@ pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
.unwrap();
Ok(res.contains("\"frameworkUpdates\""))
}
pub async fn channel_shorts_lockup(rp: &RustyPipeQuery) -> Result<bool> {
let id = "UCh8gHdtzO2tXd593_bjErWg";
let res = rp
.raw(
ClientType::Desktop,
"browse",
&QBrowse {
context: rp.get_context(ClientType::Desktop, true, None).await,
browse_id: id,
params: Some("EgZzaG9ydHPyBgUKA5oBAA%3D%3D"),
},
)
.await
.unwrap();
Ok(res.contains("\"shortsLockupViewModel\""))
}

View file

@ -748,3 +748,42 @@ seperate framework update object
}
}
```
## [15] Channel shorts: shortsLockupViewModel
- **Encountered on:** 10.09.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Common
YouTube changed the data model for the channel shorts tab
```json
{
"richItemRenderer": {
"content": {
"shortsLockupViewModel": {
"entityId": "shorts-shelf-item-ovaHmfy3O6U",
"accessibilityText": "hangover food, 17 million views - play Short",
"thumbnail": {
"sources": [
{
"url": "https://i.ytimg.com/vi/ovaHmfy3O6U/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&rs=AOn4CLBg-kG4rAi-BQ8Xkp2hOtOu-oXDLQ",
"width": 405,
"height": 720
}
]
},
"overlayMetadata": {
"primaryText": {
"content": "hangover food"
},
"secondaryText": {
"content": "17M views"
}
}
}
}
}
}
```

View file

@ -710,6 +710,7 @@ mod tests {
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
#[case::pageheader("shorts_20240129_pageheader", "UCh8gHdtzO2tXd593_bjErWg")]
#[case::pageheader2("videos_20240324_pageheader2", "UC2DjFE7Xf11URZqWBigcVOQ")]
#[case::shorts2("shorts_20240910_lockup", "UCh8gHdtzO2tXd593_bjErWg")]
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
let json_path = path!(*TESTFILES / "channel" / format!("channel_{name}.json"));
let json_file = File::open(json_path).unwrap();

View file

@ -12,7 +12,7 @@ use crate::{
},
param::Language,
serializer::{
text::{Text, TextComponent},
text::{AttributedText, Text, TextComponent},
MapResult,
},
util::{self, timeago, TryRemove},
@ -25,6 +25,7 @@ pub(crate) enum YouTubeListItem {
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
VideoRenderer(VideoRenderer),
ReelItemRenderer(ReelItemRenderer),
ShortsLockupViewModel(ShortsLockupViewModel),
PlaylistVideoRenderer(PlaylistVideoRenderer),
#[serde(alias = "gridPlaylistRenderer")]
@ -142,6 +143,28 @@ pub(crate) struct ReelItemRenderer {
pub navigation_endpoint: Option<ReelNavigationEndpoint>,
}
// New short video item
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ShortsLockupViewModel {
/// `shorts-shelf-item-[video_id]`
pub entity_id: String,
pub thumbnail: Thumbnails,
pub overlay_metadata: ShortsOverlayMetadata,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ShortsOverlayMetadata {
/// Title
#[serde_as(as = "AttributedText")]
pub primary_text: String,
/// View count
#[serde_as(as = "Option<AttributedText>")]
pub secondary_text: Option<String>,
}
/// Video displayed in a playlist
#[serde_as]
#[derive(Debug, Deserialize)]
@ -517,6 +540,31 @@ impl<T> YouTubeListMapper<T> {
}
}
fn map_short_video2(&mut self, video: ShortsLockupViewModel) -> Option<VideoItem> {
if let Some(video_id) = video.entity_id.strip_prefix("shorts-shelf-item-") {
Some(VideoItem {
id: video_id.to_owned(),
name: video.overlay_metadata.primary_text,
duration: None,
thumbnail: video.thumbnail.into(),
channel: self.channel.clone(),
publish_date: None,
publish_date_txt: None,
view_count: video.overlay_metadata.secondary_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
is_live: false,
is_short: true,
is_upcoming: false,
short_description: None,
})
} else {
self.warnings
.push(format!("invalid shorts entityId: {}", video.entity_id));
None
}
}
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
let channel = ChannelId::try_from(video.channel)
.ok()
@ -642,6 +690,11 @@ impl YouTubeListMapper<YouTubeItem> {
let mapped = YouTubeItem::Video(self.map_video(video));
self.items.push(mapped);
}
YouTubeListItem::ShortsLockupViewModel(video) => {
if let Some(mapped) = self.map_short_video2(video) {
self.items.push(YouTubeItem::Video(mapped));
}
}
YouTubeListItem::ReelItemRenderer(video) => {
let mapped = self.map_short_video(video);
self.items.push(YouTubeItem::Video(mapped));
@ -692,6 +745,11 @@ impl YouTubeListMapper<VideoItem> {
let mapped = self.map_short_video(video);
self.items.push(mapped);
}
YouTubeListItem::ShortsLockupViewModel(video) => {
if let Some(mapped) = self.map_short_video2(video) {
self.items.push(mapped);
}
}
YouTubeListItem::PlaylistVideoRenderer(video) => {
let mapped = self.map_playlist_video(video);
self.items.push(mapped);

View file

@ -469,6 +469,19 @@ impl<'de> DeserializeAs<'de, TextComponent> for AttributedText {
}
}
impl<'de> DeserializeAs<'de, String> for AttributedText {
fn deserialize_as<D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let components: TextComponents = AttributedText::deserialize_as(deserializer)?;
Ok(components
.0
.into_iter()
.fold(String::new(), |acc, c| acc + c.as_str()))
}
}
impl TryFrom<TextComponent> for crate::model::ChannelId {
type Error = ();

File diff suppressed because it is too large Load diff

View file

@ -221,13 +221,13 @@ async fn check_video_stream(s: impl YtStream) {
true
)]
#[case::agelimit(
"laru0QoJUmI",
"DJ Robin x Schürze - Layla (Official Video)",
"Endlich ist es soweit! Zwei Männer aus dem Schwabenland",
188,
"UCkJfSrMnLonOZWh-q5os5bg",
"Summerfield Records",
10_000_000,
"ZDKQmBWTRnw",
"The Rinky Pink Pounder. Hitachi Magic Wand clone teardown.",
"violent adult toys for disassembly",
1333,
"UCtM5z2gkrGRuWd0JQMx76qA",
"bigclivedotcom",
250_000,
false,
false
)]
@ -717,7 +717,7 @@ async fn get_video_details_live(rp: RustyPipe) {
assert_eq!(details.id, "jfKfPfyJRdk");
assert_eq!(
details.name,
"lofi hip hop radio 📚 - beats to relax/study to"
"lofi hip hop radio 📚 beats to relax/study to"
);
let desc = details.description.to_plaintext();
assert!(
@ -752,24 +752,27 @@ async fn get_video_details_live(rp: RustyPipe) {
#[rstest]
#[tokio::test]
async fn get_video_details_agegate(rp: RustyPipe) {
let details = rp.query().video_details("laru0QoJUmI").await.unwrap();
let details = rp.query().video_details("ZDKQmBWTRnw").await.unwrap();
// dbg!(&details);
assert_eq!(details.id, "laru0QoJUmI");
assert_eq!(details.name, "DJ Robin x Schürze - Layla (Official Video)");
assert_eq!(details.id, "ZDKQmBWTRnw");
assert_eq!(
details.name,
"The Rinky Pink Pounder. Hitachi Magic Wand clone teardown."
);
insta::assert_ron_snapshot!(details.description, @"RichText([])");
assert_eq!(details.channel.id, "UCkJfSrMnLonOZWh-q5os5bg");
assert_eq!(details.channel.name, "Summerfield Records");
assert_eq!(details.channel.id, "UCtM5z2gkrGRuWd0JQMx76qA");
assert_eq!(details.channel.name, "bigclivedotcom");
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
assert_eq!(details.channel.verification, Verification::Verified);
assert_gteo(details.channel.subscriber_count, 250_000, "subscribers");
assert_gte(details.view_count, 10_000_000, "views");
assert_gteo(details.like_count, 150_000, "likes");
assert_gteo(details.channel.subscriber_count, 1_000_000, "subscribers");
assert_gte(details.view_count, 250_000, "views");
assert_gteo(details.like_count, 5_000, "likes");
let date = details.publish_date.expect("publish_date");
assert_eq!(date.date(), date!(2022 - 5 - 13));
assert_eq!(date.date(), date!(2017 - 3 - 09));
assert!(!details.is_live);
assert!(!details.is_ccommons);
@ -876,8 +879,11 @@ async fn channel_videos(rp: RustyPipe) {
#[rstest]
#[tokio::test]
async fn channel_shorts(rp: RustyPipe) {
let vd = rp.query().get_visitor_data().await.unwrap();
let channel = rp
.query()
.visitor_data(vd)
.channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
.await
.unwrap();
@ -2153,7 +2159,7 @@ async fn music_search_playlists(rp: RustyPipe, unlocalized: bool) {
async fn music_search_playlists_community(rp: RustyPipe) {
let res = rp
.query()
.music_search_playlists("Best Pop Music Videos - Top Pop Hits Playlist", true)
.music_search_playlists("Miku my beloved (Jaiden Animation Miku Playlist)", true)
.await
.unwrap();
@ -2162,20 +2168,20 @@ async fn music_search_playlists_community(rp: RustyPipe) {
.items
.items
.iter()
.find(|p| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u")
.find(|p| p.id == "PLgAAMoX4rK3KhSGmIsN0LEoC3qowEr2Lz")
.unwrap_or_else(|| {
panic!("could not find playlist, got {:#?}", &res.items.items);
});
assert_eq!(
playlist.name,
"Best Pop Music Videos - Top Pop Hits Playlist"
"Miku my beloved (Jaiden Animation Miku Playlist)"
);
assert!(!playlist.thumbnail.is_empty(), "got no thumbnail");
let channel = playlist.channel.as_ref().unwrap();
assert_eq!(channel.id, "UCs72iRpTEuwV3y6pdWYLgiw");
assert_eq!(channel.name, "Redlist - Just Hits");
assert_eq!(channel.id, "UCsXOMpqp3_ZPOmk-HGKEPRg");
assert_eq!(channel.name, "Beanie Bean");
assert!(!playlist.from_ytm);
}