fix: A/B test 19: Music artist album groups reordered
This commit is contained in:
parent
23cd03a19d
commit
5daad1b700
13 changed files with 17830 additions and 460 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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\""))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
Reference in a new issue