fix: A/B test 18: music playlist facepile avatar model
This commit is contained in:
parent
a846b729e3
commit
6c8108c94a
10 changed files with 9765 additions and 31 deletions
|
|
@ -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\""))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use crate::{
|
|||
util::{self, TryRemove, DOT_SEPARATOR},
|
||||
};
|
||||
|
||||
use self::response::url_endpoint::MusicPageType;
|
||||
|
||||
use super::{
|
||||
response::{
|
||||
self,
|
||||
|
|
@ -213,6 +215,28 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
Some(header) => {
|
||||
let h = header.music_detail_header_renderer;
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
(from_ytm && channel.is_none(), channel)
|
||||
}
|
||||
None => {
|
||||
let st = match h.strapline_text_one {
|
||||
Some(s) => s,
|
||||
None => h.subtitle,
|
||||
|
|
@ -220,6 +244,9 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
7973
testfiles/music_playlist/playlist_20241125_facepile.json
Normal file
7973
testfiles/music_playlist/playlist_20241125_facepile.json
Normal file
File diff suppressed because it is too large
Load diff
Reference in a new issue