fix: A/B test 15 (parsing channel shortsLockupViewModel)
This commit is contained in:
parent
ed08f9ff9a
commit
7972df0df4
8 changed files with 14241 additions and 23 deletions
|
|
@ -34,6 +34,7 @@ pub enum ABTest {
|
||||||
ChannelPageHeader = 12,
|
ChannelPageHeader = 12,
|
||||||
MusicPlaylistTwoColumn = 13,
|
MusicPlaylistTwoColumn = 13,
|
||||||
CommentsFrameworkUpdate = 14,
|
CommentsFrameworkUpdate = 14,
|
||||||
|
ChannelShortsLockup = 15,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List of active A/B tests that are run when none is manually specified
|
/// 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::ChannelPageHeader => channel_page_header(&query).await,
|
||||||
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
|
ABTest::MusicPlaylistTwoColumn => music_playlist_two_column(&query).await,
|
||||||
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
|
ABTest::CommentsFrameworkUpdate => comments_framework_update(&query).await,
|
||||||
|
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
|
||||||
}
|
}
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pb.inc(1);
|
pb.inc(1);
|
||||||
|
|
@ -363,3 +365,20 @@ pub async fn comments_framework_update(rp: &RustyPipeQuery) -> Result<bool> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(res.contains("\"frameworkUpdates\""))
|
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\""))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,7 @@ mod tests {
|
||||||
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
#[case::pageheader("shorts_20240129_pageheader", "UCh8gHdtzO2tXd593_bjErWg")]
|
#[case::pageheader("shorts_20240129_pageheader", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
#[case::pageheader2("videos_20240324_pageheader2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
#[case::pageheader2("videos_20240324_pageheader2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
|
#[case::shorts2("shorts_20240910_lockup", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
||||||
let json_path = path!(*TESTFILES / "channel" / format!("channel_{name}.json"));
|
let json_path = path!(*TESTFILES / "channel" / format!("channel_{name}.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::{
|
serializer::{
|
||||||
text::{Text, TextComponent},
|
text::{AttributedText, Text, TextComponent},
|
||||||
MapResult,
|
MapResult,
|
||||||
},
|
},
|
||||||
util::{self, timeago, TryRemove},
|
util::{self, timeago, TryRemove},
|
||||||
|
|
@ -25,6 +25,7 @@ pub(crate) enum YouTubeListItem {
|
||||||
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
|
#[serde(alias = "gridVideoRenderer", alias = "compactVideoRenderer")]
|
||||||
VideoRenderer(VideoRenderer),
|
VideoRenderer(VideoRenderer),
|
||||||
ReelItemRenderer(ReelItemRenderer),
|
ReelItemRenderer(ReelItemRenderer),
|
||||||
|
ShortsLockupViewModel(ShortsLockupViewModel),
|
||||||
PlaylistVideoRenderer(PlaylistVideoRenderer),
|
PlaylistVideoRenderer(PlaylistVideoRenderer),
|
||||||
|
|
||||||
#[serde(alias = "gridPlaylistRenderer")]
|
#[serde(alias = "gridPlaylistRenderer")]
|
||||||
|
|
@ -142,6 +143,28 @@ pub(crate) struct ReelItemRenderer {
|
||||||
pub navigation_endpoint: Option<ReelNavigationEndpoint>,
|
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
|
/// Video displayed in a playlist
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[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 {
|
fn map_playlist_video(&mut self, video: PlaylistVideoRenderer) -> VideoItem {
|
||||||
let channel = ChannelId::try_from(video.channel)
|
let channel = ChannelId::try_from(video.channel)
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -642,6 +690,11 @@ impl YouTubeListMapper<YouTubeItem> {
|
||||||
let mapped = YouTubeItem::Video(self.map_video(video));
|
let mapped = YouTubeItem::Video(self.map_video(video));
|
||||||
self.items.push(mapped);
|
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) => {
|
YouTubeListItem::ReelItemRenderer(video) => {
|
||||||
let mapped = self.map_short_video(video);
|
let mapped = self.map_short_video(video);
|
||||||
self.items.push(YouTubeItem::Video(mapped));
|
self.items.push(YouTubeItem::Video(mapped));
|
||||||
|
|
@ -692,6 +745,11 @@ impl YouTubeListMapper<VideoItem> {
|
||||||
let mapped = self.map_short_video(video);
|
let mapped = self.map_short_video(video);
|
||||||
self.items.push(mapped);
|
self.items.push(mapped);
|
||||||
}
|
}
|
||||||
|
YouTubeListItem::ShortsLockupViewModel(video) => {
|
||||||
|
if let Some(mapped) = self.map_short_video2(video) {
|
||||||
|
self.items.push(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
YouTubeListItem::PlaylistVideoRenderer(video) => {
|
YouTubeListItem::PlaylistVideoRenderer(video) => {
|
||||||
let mapped = self.map_playlist_video(video);
|
let mapped = self.map_playlist_video(video);
|
||||||
self.items.push(mapped);
|
self.items.push(mapped);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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 {
|
impl TryFrom<TextComponent> for crate::model::ChannelId {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
|
|
|
||||||
12760
testfiles/channel/channel_shorts_20240910_lockup.json
Normal file
12760
testfiles/channel/channel_shorts_20240910_lockup.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -221,13 +221,13 @@ async fn check_video_stream(s: impl YtStream) {
|
||||||
true
|
true
|
||||||
)]
|
)]
|
||||||
#[case::agelimit(
|
#[case::agelimit(
|
||||||
"laru0QoJUmI",
|
"ZDKQmBWTRnw",
|
||||||
"DJ Robin x Schürze - Layla (Official Video)",
|
"The Rinky Pink Pounder. Hitachi Magic Wand clone teardown.",
|
||||||
"Endlich ist es soweit! Zwei Männer aus dem Schwabenland",
|
"violent adult toys for disassembly",
|
||||||
188,
|
1333,
|
||||||
"UCkJfSrMnLonOZWh-q5os5bg",
|
"UCtM5z2gkrGRuWd0JQMx76qA",
|
||||||
"Summerfield Records",
|
"bigclivedotcom",
|
||||||
10_000_000,
|
250_000,
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
)]
|
)]
|
||||||
|
|
@ -717,7 +717,7 @@ async fn get_video_details_live(rp: RustyPipe) {
|
||||||
assert_eq!(details.id, "jfKfPfyJRdk");
|
assert_eq!(details.id, "jfKfPfyJRdk");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
details.name,
|
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();
|
let desc = details.description.to_plaintext();
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -752,24 +752,27 @@ async fn get_video_details_live(rp: RustyPipe) {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_video_details_agegate(rp: RustyPipe) {
|
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);
|
// dbg!(&details);
|
||||||
|
|
||||||
assert_eq!(details.id, "laru0QoJUmI");
|
assert_eq!(details.id, "ZDKQmBWTRnw");
|
||||||
assert_eq!(details.name, "DJ Robin x Schürze - Layla (Official Video)");
|
assert_eq!(
|
||||||
|
details.name,
|
||||||
|
"The Rinky Pink Pounder. Hitachi Magic Wand clone teardown."
|
||||||
|
);
|
||||||
insta::assert_ron_snapshot!(details.description, @"RichText([])");
|
insta::assert_ron_snapshot!(details.description, @"RichText([])");
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UCkJfSrMnLonOZWh-q5os5bg");
|
assert_eq!(details.channel.id, "UCtM5z2gkrGRuWd0JQMx76qA");
|
||||||
assert_eq!(details.channel.name, "Summerfield Records");
|
assert_eq!(details.channel.name, "bigclivedotcom");
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
assert_eq!(details.channel.verification, Verification::Verified);
|
assert_eq!(details.channel.verification, Verification::Verified);
|
||||||
assert_gteo(details.channel.subscriber_count, 250_000, "subscribers");
|
assert_gteo(details.channel.subscriber_count, 1_000_000, "subscribers");
|
||||||
assert_gte(details.view_count, 10_000_000, "views");
|
assert_gte(details.view_count, 250_000, "views");
|
||||||
assert_gteo(details.like_count, 150_000, "likes");
|
assert_gteo(details.like_count, 5_000, "likes");
|
||||||
|
|
||||||
let date = details.publish_date.expect("publish_date");
|
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_live);
|
||||||
assert!(!details.is_ccommons);
|
assert!(!details.is_ccommons);
|
||||||
|
|
@ -876,8 +879,11 @@ async fn channel_videos(rp: RustyPipe) {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn channel_shorts(rp: RustyPipe) {
|
async fn channel_shorts(rp: RustyPipe) {
|
||||||
|
let vd = rp.query().get_visitor_data().await.unwrap();
|
||||||
|
|
||||||
let channel = rp
|
let channel = rp
|
||||||
.query()
|
.query()
|
||||||
|
.visitor_data(vd)
|
||||||
.channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
|
.channel_videos_tab("UCh8gHdtzO2tXd593_bjErWg", ChannelVideoTab::Shorts)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -2153,7 +2159,7 @@ async fn music_search_playlists(rp: RustyPipe, unlocalized: bool) {
|
||||||
async fn music_search_playlists_community(rp: RustyPipe) {
|
async fn music_search_playlists_community(rp: RustyPipe) {
|
||||||
let res = rp
|
let res = rp
|
||||||
.query()
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -2162,20 +2168,20 @@ async fn music_search_playlists_community(rp: RustyPipe) {
|
||||||
.items
|
.items
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u")
|
.find(|p| p.id == "PLgAAMoX4rK3KhSGmIsN0LEoC3qowEr2Lz")
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
panic!("could not find playlist, got {:#?}", &res.items.items);
|
panic!("could not find playlist, got {:#?}", &res.items.items);
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
playlist.name,
|
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");
|
assert!(!playlist.thumbnail.is_empty(), "got no thumbnail");
|
||||||
|
|
||||||
let channel = playlist.channel.as_ref().unwrap();
|
let channel = playlist.channel.as_ref().unwrap();
|
||||||
assert_eq!(channel.id, "UCs72iRpTEuwV3y6pdWYLgiw");
|
assert_eq!(channel.id, "UCsXOMpqp3_ZPOmk-HGKEPRg");
|
||||||
assert_eq!(channel.name, "Redlist - Just Hits");
|
assert_eq!(channel.name, "Beanie Bean");
|
||||||
assert!(!playlist.from_ytm);
|
assert!(!playlist.from_ytm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue