From 9c67f8f85bef8214848dc9d17bff6cff252e015e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 25 Jan 2025 03:18:19 +0100 Subject: [PATCH] fix: a/b test 20: music continuation item renderer --- codegen/src/abtest.rs | 24 +++++++++++++++++++++++- notes/AB_Tests.md | 20 ++++++++++++++++++-- src/client/music_playlist.rs | 14 ++++++++------ src/client/music_search.rs | 1 + src/client/pagination.rs | 14 ++++++++++---- src/client/response/music_item.rs | 22 +++++++++++++++++++++- src/client/search.rs | 2 +- src/client/video_details.rs | 12 ++++++------ 8 files changed, 88 insertions(+), 21 deletions(-) diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 00c85cd..22c0b0a 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -39,10 +39,14 @@ pub enum ABTest { ChannelPlaylistsLockup = 17, MusicPlaylistFacepile = 18, MusicAlbumGroupsReordered = 19, + MusicContinuationItemRenderer = 20, } /// List of active A/B tests that are run when none is manually specified -const TESTS_TO_RUN: &[ABTest] = &[ABTest::MusicAlbumGroupsReordered]; +const TESTS_TO_RUN: &[ABTest] = &[ + ABTest::MusicAlbumGroupsReordered, + ABTest::MusicContinuationItemRenderer, +]; #[derive(Debug, Serialize, Deserialize)] pub struct ABTestRes { @@ -114,6 +118,9 @@ pub async fn run_test( ABTest::ChannelPlaylistsLockup => channel_playlists_lockup(&query).await, ABTest::MusicPlaylistFacepile => music_playlist_facepile(&query).await, ABTest::MusicAlbumGroupsReordered => music_album_groups_reordered(&query).await, + ABTest::MusicContinuationItemRenderer => { + music_continuation_item_renderer(&query).await + } } .unwrap(); pb.inc(1); @@ -421,3 +428,18 @@ pub async fn music_album_groups_reordered(rp: &RustyPipeQuery) -> Result { .await?; Ok(res.contains("\"Singles & EPs\"")) } + +pub async fn music_continuation_item_renderer(rp: &RustyPipeQuery) -> Result { + let id = "VLPLbZIPy20-1pN7mqjckepWF78ndb6ci_qi"; + let res = rp + .raw( + ClientType::DesktopMusic, + "browse", + &QBrowse { + browse_id: id, + params: None, + }, + ) + .await?; + Ok(res.contains("\"continuationItemRenderer\"")) +} diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index b006b4b..0a73315 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1034,5 +1034,21 @@ commandContext missing). YouTube Music used to group artist albums into 2 rows: "Albums" and "Singles". -These groups were changed into "Albums" and "Singles & EPs". Now the "Album" label is omitted -for albums in their group, while singles and EPs have a label with their type. +These groups were changed into "Albums" and "Singles & EPs". Now the "Album" label is +omitted for albums in their group, while singles and EPs have a label with their type. + +## [20] Music continuation item renderer + +- **Encountered on:** 25.01.2025 +- **Impact:** 🟢 Low +- **Endpoint:** browse (YTM) +- **Status:** Common (4%) + +YouTube Music now uses a `continuationItemRenderer` for music playlists instead of +putting the continuations in a separate attribute of the MusicShelf. + +The continuation response now uses a `onResponseReceivedActions` field for its music +items. + +YouTube Music now also sends a random 16-character string as a `clientScreenNonce` in +the request context. This is not mandatory though. diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index efe4859..b09656a 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -196,13 +196,15 @@ impl MapResponse for response::MusicPlaylist { let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(shelf.contents); - let map_res = mapper.conv_items(); - let ctoken = shelf - .continuations - .into_iter() - .next() - .map(|cont| cont.next_continuation_data.continuation); + let ctoken = mapper.ctoken.clone().or_else(|| { + shelf + .continuations + .into_iter() + .next() + .map(|cont| cont.next_continuation_data.continuation) + }); + let map_res = mapper.conv_items(); let track_count = if ctoken.is_some() { header.as_ref().and_then(|h| { diff --git a/src/client/music_search.rs b/src/client/music_search.rs index e213517..dd40ada 100644 --- a/src/client/music_search.rs +++ b/src/client/music_search.rs @@ -189,6 +189,7 @@ impl MapResponse> for response::MusicSearch response::music_search::ItemSection::None => {} }); + let ctoken = ctoken.or(mapper.ctoken.clone()); let map_res = mapper.conv_items(); Ok(MapResult { diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 07c4964..3fad657 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -199,11 +199,17 @@ impl MapResponse> for response::MusicContinuation { None => {} } + for a in self.on_response_received_actions { + mapper.map_response(a.append_continuation_items_action.continuation_items); + } + + let ctoken = mapper.ctoken.clone().or_else(|| { + continuations + .into_iter() + .next() + .map(|cont| cont.next_continuation_data.continuation) + }); let map_res = mapper.items(); - let ctoken = continuations - .into_iter() - .next() - .map(|cont| cont.next_continuation_data.continuation); Ok(MapResult { c: Paginator::new_ext( diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index cb51528..f5e0fac 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -18,7 +18,8 @@ use super::{ url_endpoint::{ BrowseEndpointWrap, MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType, }, - ContentsRenderer, MusicContinuationData, SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap, + ContentsRenderer, ContinuationActionWrap, ContinuationEndpoint, MusicContinuationData, + SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap, }; #[derive(Debug, Deserialize)] @@ -86,6 +87,10 @@ pub(crate) enum MusicResponseItem { MusicResponsiveListItemRenderer(ListMusicItem), MusicTwoRowItemRenderer(CoverMusicItem), MessageRenderer(serde::de::IgnoredAny), + #[serde(rename_all = "camelCase")] + ContinuationItemRenderer { + continuation_endpoint: ContinuationEndpoint, + }, } #[serde_as] @@ -322,10 +327,14 @@ impl From for Vec { } /// Music list continuation response model +#[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicContinuation { pub continuation_contents: Option, + #[serde(default)] + #[serde_as(as = "VecSkipError<_>")] + pub on_response_received_actions: Vec>, } #[derive(Debug, Deserialize)] @@ -434,6 +443,7 @@ pub(crate) struct MusicListMapper { search_suggestion: bool, items: Vec, warnings: Vec, + pub ctoken: Option, } #[derive(Debug)] @@ -455,6 +465,7 @@ impl MusicListMapper { search_suggestion: false, items: Vec::new(), warnings: Vec::new(), + ctoken: None, } } @@ -468,6 +479,7 @@ impl MusicListMapper { search_suggestion: true, items: Vec::new(), warnings: Vec::new(), + ctoken: None, } } @@ -482,6 +494,7 @@ impl MusicListMapper { search_suggestion: false, items: Vec::new(), warnings: Vec::new(), + ctoken: None, } } @@ -496,6 +509,7 @@ impl MusicListMapper { search_suggestion: false, items: Vec::new(), warnings: Vec::new(), + ctoken: None, } } @@ -507,6 +521,12 @@ impl MusicListMapper { // Tile MusicResponseItem::MusicTwoRowItemRenderer(item) => self.map_tile(item), MusicResponseItem::MessageRenderer(_) => Ok(None), + MusicResponseItem::ContinuationItemRenderer { + continuation_endpoint, + } => { + self.ctoken = Some(continuation_endpoint.continuation_command.token); + Ok(None) + } } } diff --git a/src/client/search.rs b/src/client/search.rs index a4389e4..b4ba544 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -120,7 +120,7 @@ impl MapResponse> for response::Search { .filter_map(T::from_yt_item) .collect(), mapper.ctoken, - None, + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::Search, false, ), diff --git a/src/client/video_details.rs b/src/client/video_details.rs index e91a890..9cc1ffc 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -277,7 +277,7 @@ impl MapResponse for response::VideoDetails { r, sr.secondary_results.continuations, visitor_data.clone(), - ctx.lang, + ctx, ); warnings.append(&mut res.warnings); res.c @@ -359,7 +359,7 @@ impl MapResponse for response::VideoDetails { comment_ctoken, visitor_data.clone(), ContinuationEndpoint::Next, - false, + ctx.authenticated, ), latest_comments: Paginator::new_ext( comment_count, @@ -367,7 +367,7 @@ impl MapResponse for response::VideoDetails { latest_comments_ctoken, visitor_data.clone(), ContinuationEndpoint::Next, - false, + ctx.authenticated, ), visitor_data, }, @@ -468,9 +468,9 @@ fn map_recommendations( r: MapResult>, continuations: Option>, visitor_data: Option, - lang: Language, + ctx: &MapRespCtx<'_>, ) -> MapResult> { - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(r); mapper.ctoken = mapper.ctoken.or_else(|| { @@ -486,7 +486,7 @@ fn map_recommendations( mapper.ctoken, visitor_data, ContinuationEndpoint::Next, - false, + ctx.authenticated, ), warnings: mapper.warnings, }