fix: A/B test 19: Music artist album groups reordered

This commit is contained in:
ThetaDev 2025-01-13 03:21:51 +01:00
parent 23cd03a19d
commit 5daad1b700
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
13 changed files with 17830 additions and 460 deletions

View file

@ -15,6 +15,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
futures-util.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_plain.workspace = true
serde_with.workspace = true
once_cell.workspace = true
regex.workspace = true

View file

@ -38,14 +38,11 @@ pub enum ABTest {
PlaylistPageHeader = 16,
ChannelPlaylistsLockup = 17,
MusicPlaylistFacepile = 18,
MusicAlbumGroupsReordered = 19,
}
/// List of active A/B tests that are run when none is manually specified
const TESTS_TO_RUN: [ABTest; 3] = [
ABTest::ChannelPageHeader,
ABTest::MusicPlaylistTwoColumn,
ABTest::CommentsFrameworkUpdate,
];
const TESTS_TO_RUN: &[ABTest] = &[ABTest::MusicAlbumGroupsReordered];
#[derive(Debug, Serialize, Deserialize)]
pub struct ABTestRes {
@ -116,6 +113,7 @@ pub async fn run_test(
ABTest::PlaylistPageHeader => playlist_page_header_renderer(&query).await,
ABTest::ChannelPlaylistsLockup => channel_playlists_lockup(&query).await,
ABTest::MusicPlaylistFacepile => music_playlist_facepile(&query).await,
ABTest::MusicAlbumGroupsReordered => music_album_groups_reordered(&query).await,
}
.unwrap();
pb.inc(1);
@ -141,10 +139,10 @@ pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
let mut results = Vec::new();
for ab in TESTS_TO_RUN {
let (occurrences, vd_present, vd_absent) = run_test(ab, n, concurrency).await;
let (occurrences, vd_present, vd_absent) = run_test(*ab, n, concurrency).await;
results.push(ABTestRes {
id: ab as u16,
name: ab,
id: *ab as u16,
name: *ab,
tests: n,
occurrences,
vd_present,
@ -408,3 +406,18 @@ pub async fn music_playlist_facepile(rp: &RustyPipeQuery) -> Result<bool> {
.await?;
Ok(res.contains("\"facepile\""))
}
pub async fn music_album_groups_reordered(rp: &RustyPipeQuery) -> Result<bool> {
let id = "UCOR4_bSVIXPsGa4BbCSt60Q";
let res = rp
.raw(
ClientType::DesktopMusic,
"browse",
&QBrowse {
browse_id: id,
params: None,
},
)
.await?;
Ok(res.contains("\"Singles & EPs\""))
}

View file

@ -7,22 +7,35 @@ use rustypipe::{
model::AlbumType,
param::{Language, LANGUAGES},
};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_with::rust::deserialize_ignore_any;
use crate::{
model::{QBrowse, TextRuns},
model::{ContentsRenderer, QBrowse, SectionList, Tab, TextRuns},
util::{self, DICT_DIR},
};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
enum AlbumTypeX {
Album,
Ep,
Single,
Audiobook,
Show,
AlbumRow,
SingleRow,
}
pub async fn collect_album_types(concurrency: usize) {
let json_path = path!(*DICT_DIR / "album_type_samples.json");
let album_types = [
(AlbumType::Album, "MPREb_nlBWQROfvjo"),
(AlbumType::Single, "MPREb_bHfHGoy7vuv"),
(AlbumType::Ep, "MPREb_u1I69lSAe5v"),
(AlbumType::Audiobook, "MPREb_gaoNzsQHedo"),
(AlbumType::Show, "MPREb_cwzk8EUwypZ"),
(AlbumTypeX::Album, "MPREb_nlBWQROfvjo"),
(AlbumTypeX::Single, "MPREb_bHfHGoy7vuv"),
(AlbumTypeX::Ep, "MPREb_u1I69lSAe5v"),
(AlbumTypeX::Audiobook, "MPREb_gaoNzsQHedo"),
(AlbumTypeX::Show, "MPREb_cwzk8EUwypZ"),
];
let rp = RustyPipe::new();
@ -32,7 +45,7 @@ pub async fn collect_album_types(concurrency: usize) {
let rp = rp.clone();
async move {
let query = rp.query().lang(lang);
let mut data: BTreeMap<AlbumType, String> = BTreeMap::new();
let mut data: BTreeMap<AlbumTypeX, String> = BTreeMap::new();
for (album_type, id) in album_types {
let atype_txt = get_album_type(&query, id).await;
@ -40,6 +53,22 @@ pub async fn collect_album_types(concurrency: usize) {
data.insert(album_type, atype_txt);
}
let (albums_txt, singles_txt) = get_album_groups(&query).await;
println!(
"collected {}-{:?} ({})",
lang,
AlbumTypeX::AlbumRow,
&albums_txt
);
println!(
"collected {}-{:?} ({})",
lang,
AlbumTypeX::SingleRow,
&singles_txt
);
data.insert(AlbumTypeX::AlbumRow, albums_txt);
data.insert(AlbumTypeX::SingleRow, singles_txt);
(lang, data)
}
})
@ -55,7 +84,7 @@ pub fn write_samples_to_dict() {
let json_path = path!(*DICT_DIR / "album_type_samples.json");
let json_file = File::open(json_path).unwrap();
let collected: BTreeMap<Language, BTreeMap<AlbumType, String>> =
let collected: BTreeMap<Language, BTreeMap<String, String>> =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let mut dict = util::read_dict();
let langs = dict.keys().copied().collect::<Vec<_>>();
@ -67,10 +96,12 @@ pub fn write_samples_to_dict() {
e_langs.push(lang);
for lang in &e_langs {
collected.get(lang).unwrap().iter().for_each(|(t, v)| {
collected.get(lang).unwrap().iter().for_each(|(t_str, v)| {
let t =
serde_plain::from_str::<AlbumType>(t_str.split('_').next().unwrap()).unwrap();
dict_entry
.album_types
.insert(v.to_lowercase().trim().to_owned(), *t);
.insert(v.to_lowercase().trim().to_owned(), t);
});
}
}
@ -80,13 +111,19 @@ pub fn write_samples_to_dict() {
#[derive(Debug, Deserialize)]
struct AlbumData {
header: Header,
contents: AlbumContents,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Header {
music_detail_header_renderer: HeaderRenderer,
struct AlbumContents {
two_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<AlbumHeader>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AlbumHeader {
music_responsive_header_renderer: HeaderRenderer,
}
#[derive(Debug, Deserialize)]
@ -106,8 +143,20 @@ async fn get_album_type(query: &RustyPipeQuery, id: &str) -> String {
let album = serde_json::from_str::<AlbumData>(&response_txt).unwrap();
album
.header
.music_detail_header_renderer
.contents
.two_column_browse_results_renderer
.contents
.into_iter()
.next()
.unwrap()
.tab_renderer
.content
.section_list_renderer
.contents
.into_iter()
.next()
.unwrap()
.music_responsive_header_renderer
.subtitle
.runs
.into_iter()
@ -115,3 +164,84 @@ async fn get_album_type(query: &RustyPipeQuery, id: &str) -> String {
.unwrap()
.text
}
async fn get_album_groups(query: &RustyPipeQuery) -> (String, String) {
let body = QBrowse {
browse_id: "UCOR4_bSVIXPsGa4BbCSt60Q",
params: None,
};
let response_txt = query
.clone()
.visitor_data("CgtwbzJZcS1XZWc1QSjM2JG8BjIKCgJERRIEEgAgCw%3D%3D")
.raw(ClientType::DesktopMusic, "browse", &body)
.await
.unwrap();
let artist = serde_json::from_str::<ArtistData>(&response_txt).unwrap();
let sections = artist
.contents
.single_column_browse_results_renderer
.contents
.into_iter()
.next()
.map(|c| c.tab_renderer.content.section_list_renderer.contents)
.unwrap();
let titles = sections
.into_iter()
.filter_map(|s| {
if let ItemSection::MusicCarouselShelfRenderer(r) = s {
r.header
} else {
None
}
})
.map(|h| {
h.music_carousel_shelf_basic_header_renderer
.title
.runs
.into_iter()
.next()
.unwrap()
.text
})
.collect::<Vec<_>>();
assert!(titles.len() >= 2, "too few sections");
let mut titles_it = titles.into_iter();
(titles_it.next().unwrap(), titles_it.next().unwrap())
}
#[derive(Debug, Deserialize)]
struct ArtistData {
contents: ArtistDataContents,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ArtistDataContents {
single_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<ItemSection>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
enum ItemSection {
MusicCarouselShelfRenderer(MusicCarouselShelf),
#[serde(other, deserialize_with = "deserialize_ignore_any")]
None,
}
#[derive(Debug, Deserialize)]
struct MusicCarouselShelf {
header: Option<MusicCarouselShelfHeader>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MusicCarouselShelfHeader {
music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer,
}
#[derive(Debug, Deserialize)]
struct MusicCarouselShelfHeaderRenderer {
title: TextRuns,
}

View file

@ -145,7 +145,7 @@ pub struct Text {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Channel {
pub contents: Contents,
pub contents: TwoColumnBrowseResults,
pub header: ChannelHeader,
}
@ -163,7 +163,7 @@ pub struct HeaderRenderer {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contents {
pub struct TwoColumnBrowseResults {
pub two_column_browse_results_renderer: TabsRenderer,
}
@ -172,24 +172,37 @@ pub struct Contents {
#[serde(rename_all = "camelCase")]
pub struct TabsRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub tabs: Vec<TabRendererWrap>,
pub tabs: Vec<Tab<RichGrid>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TabRendererWrap {
pub tab_renderer: TabRenderer,
pub struct ContentsRenderer<T> {
#[serde(alias = "tabs")]
pub contents: Vec<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TabRenderer {
pub content: RichGridRendererWrap,
pub struct Tab<T> {
pub tab_renderer: TabRenderer<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichGridRendererWrap {
pub struct TabRenderer<T> {
pub content: T,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SectionList<T> {
pub section_list_renderer: ContentsRenderer<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichGrid {
pub rich_grid_renderer: RichGridRenderer,
}