fix: A/B test 18: music playlist facepile avatar model

This commit is contained in:
ThetaDev 2024-11-25 16:42:00 +01:00
parent a846b729e3
commit 6c8108c94a
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
10 changed files with 9765 additions and 31 deletions

View file

@ -37,6 +37,7 @@ pub enum ABTest {
ChannelShortsLockup = 15,
PlaylistPageHeader = 16,
ChannelPlaylistsLockup = 17,
MusicPlaylistFacepile = 18,
}
/// List of active A/B tests that are run when none is manually specified
@ -114,6 +115,7 @@ pub async fn run_test(
ABTest::ChannelShortsLockup => channel_shorts_lockup(&query).await,
ABTest::PlaylistPageHeader => playlist_page_header_renderer(&query).await,
ABTest::ChannelPlaylistsLockup => channel_playlists_lockup(&query).await,
ABTest::MusicPlaylistFacepile => music_playlist_facepile(&query).await,
}
.unwrap();
pb.inc(1);
@ -391,3 +393,18 @@ pub async fn channel_playlists_lockup(rp: &RustyPipeQuery) -> Result<bool> {
.await?;
Ok(res.contains("\"lockupViewModel\""))
}
pub async fn music_playlist_facepile(rp: &RustyPipeQuery) -> Result<bool> {
let id = "VLPL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe";
let res = rp
.raw(
ClientType::DesktopMusic,
"browse",
&QBrowse {
browse_id: id,
params: None,
},
)
.await?;
Ok(res.contains("\"facepile\""))
}

View file

@ -793,7 +793,7 @@ YouTube changed the data model for the channel shorts tab
- **Encountered on:** 11.10.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Common (99%)
- **Status:** Stabilized
```json
{
@ -905,7 +905,7 @@ YouTube changed the data model for the channel shorts tab
- **Encountered on:** 09.11.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse
- **Status:** Common (50%)
- **Status:** Stabilized
YouTube changed the data model for the channel playlists / podcasts / albums tab
@ -969,3 +969,58 @@ YouTube changed the data model for the channel playlists / podcasts / albums tab
}
}
```
## [18] Music playlists facepile avatar
- **Encountered on:** 25.11.2024
- **Impact:** 🟢 Low
- **Endpoint:** browse (YTM)
- **Status:** Stabilized
YouTube changed the data model for the channel playlist owner avatar into a `facepile`
object. It now also contains the channel avatar.
The model is also used for playlists owned by YouTube Music (with the avatar and
commandContext missing).
```json
{
"facepile": {
"avatarStackViewModel": {
"avatars": [
{
"avatarViewModel": {
"image": {
"sources": [
{
"url": "https://yt3.ggpht.com/ytc/AIdro_n9ALaLETwQH6_2WlXitIaIKV-IqBDWWquvyI2jucNAZaQ=s48-c-k-c0x00000000-no-cc-rj-rp"
}
]
},
"avatarImageSize": "AVATAR_SIZE_XS"
}
}
],
"text": {
"content": "Chaosflo44"
},
"rendererContext": {
"commandContext": {
"onTap": {
"innertubeCommand": {
"browseEndpoint": {
"browseId": "UCQM0bS4_04-Y4JuYrgmnpZQ",
"browseEndpointContextSupportedConfigs": {
"browseEndpointContextMusicConfig": {
"pageType": "MUSIC_PAGE_TYPE_USER_CHANNEL"
}
}
}
}
}
}
}
}
}
}
```

View file

@ -12,6 +12,8 @@ use crate::{
util::{self, TryRemove, DOT_SEPARATOR},
};
use self::response::url_endpoint::MusicPageType;
use super::{
response::{
self,
@ -213,13 +215,38 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
Some(header) => {
let h = header.music_detail_header_renderer;
let st = match h.strapline_text_one {
Some(s) => s,
None => h.subtitle,
};
let (from_ytm, channel) = match h.facepile {
Some(facepile) => {
let from_ytm = facepile.avatar_stack_view_model.text.starts_with("YouTube");
let channel = facepile
.avatar_stack_view_model
.renderer_context
.command_context
.and_then(|c| {
c.on_tap
.innertube_command
.music_page()
.filter(|p| p.typ == MusicPageType::User)
.map(|p| p.id)
})
.map(|id| ChannelId {
id,
name: facepile.avatar_stack_view_model.text,
});
let from_ytm = st.0.iter().any(util::is_ytm);
let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
(from_ytm && channel.is_none(), channel)
}
None => {
let st = match h.strapline_text_one {
Some(s) => s,
None => h.subtitle,
};
let from_ytm = st.0.iter().any(util::is_ytm);
let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
(from_ytm, channel)
}
};
(
from_ytm,
@ -484,6 +511,7 @@ mod tests {
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
#[case::two_columns("20240228_twoColumns", "RDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM")]
#[case::n_album("20240228_album", "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0")]
#[case::facepile("20241125_facepile", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
fn map_music_playlist(#[case] name: &str, #[case] id: &str) {
let json_path = path!(*TESTFILES / "music_playlist" / format!("playlist_{name}.json"));
let json_file = File::open(json_path).unwrap();

View file

@ -2,9 +2,9 @@ use serde::Deserialize;
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
use super::{
video_item::YouTubeListRenderer, Alert, AttachmentRun, ChannelBadge, ContentRenderer,
ContentsRenderer, ContinuationActionWrap, ImageView, PageHeaderRendererContent, PhMetadataView,
ResponseContext, Thumbnails, TwoColumnBrowseResults,
video_item::YouTubeListRenderer, Alert, AttachmentRun, AvatarViewModel, ChannelBadge,
ContentRenderer, ContentsRenderer, ContinuationActionWrap, ImageView,
PageHeaderRendererContent, PhMetadataView, ResponseContext, Thumbnails, TwoColumnBrowseResults,
};
use crate::{
model::Verification,
@ -162,13 +162,7 @@ pub(crate) struct PhAvatarView {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhAvatarView2 {
pub avatar: PhAvatarView3,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PhAvatarView3 {
pub avatar_view_model: ImageView,
pub avatar: AvatarViewModel,
}
#[derive(Default, Debug, Deserialize)]

View file

@ -113,6 +113,12 @@ pub(crate) struct ImageView {
pub image: Thumbnails,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AvatarViewModel {
pub avatar_view_model: ImageView,
}
/// List of images in different resolutions.
/// Not only used for thumbnails, but also for avatars and banners.
#[derive(Default, Debug, Deserialize)]
@ -203,7 +209,7 @@ pub(crate) struct TextBox {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TextComponentBox {
#[serde_as(deserialize_as = "AttributedText")]
#[serde_as(as = "AttributedText")]
pub text: TextComponent,
}
@ -591,7 +597,7 @@ pub(crate) struct PhMetadataRow {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum MetadataPart {
Text(#[serde_as(deserialize_as = "AttributedText")] TextComponent),
Text(#[serde_as(as = "AttributedText")] TextComponent),
#[serde(rename_all = "camelCase")]
AvatarStack {
avatar_stack_view_model: TextComponentBox,

View file

@ -1,12 +1,13 @@
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::{Text, TextComponents};
use crate::serializer::text::{AttributedText, Text, TextComponents};
use super::{
music_item::{
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
},
url_endpoint::OnTapWrap,
ContentsRenderer, SectionList, Tab,
};
@ -83,6 +84,10 @@ pub(crate) struct HeaderRenderer {
#[serde(default)]
#[serde_as(as = "Text")]
pub second_subtitle: Vec<String>,
/// Channel (newer data model)
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub facepile: Option<AvatarStackViewModelWrap>,
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub menu: Option<HeaderMenu>,
@ -135,6 +140,29 @@ impl From<Description> for TextComponents {
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AvatarStackViewModelWrap {
pub avatar_stack_view_model: AvatarStackViewModel,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AvatarStackViewModel {
// #[serde(default)]
// pub avatars: Vec<AvatarViewModel>,
#[serde_as(as = "AttributedText")]
pub text: String,
pub renderer_context: AvatarStackRendererContext,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AvatarStackRendererContext {
pub command_context: Option<OnTapWrap>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Microformat {

View file

@ -4,9 +4,9 @@ use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::{AttributedText, Text, TextComponent, TextComponents};
use super::{
url_endpoint::OnTap, video_item::YouTubeListRenderer, Alert, ContentRenderer, ContentsRenderer,
ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext, SectionList, Tab,
TextBox, ThumbnailsWrap, TwoColumnBrowseResults,
url_endpoint::OnTapWrap, video_item::YouTubeListRenderer, Alert, ContentRenderer,
ContentsRenderer, ImageView, PageHeaderRendererContent, PhMetadataView, ResponseContext,
SectionList, Tab, TextBox, ThumbnailsWrap, TwoColumnBrowseResults,
};
#[serde_as]
@ -173,11 +173,5 @@ pub(crate) struct ActionsRow {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ButtonAction {
pub button_view_model: ButtonViewModel,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ButtonViewModel {
pub on_tap: OnTap,
pub button_view_model: OnTapWrap,
}

View file

@ -163,6 +163,12 @@ pub(crate) struct OnTap {
pub innertube_command: NavigationEndpoint,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct OnTapWrap {
pub on_tap: OnTap,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
pub(crate) enum MusicVideoType {
#[default]

File diff suppressed because it is too large Load diff