feat: add music genres
This commit is contained in:
parent
c6cd364b9e
commit
de7bd2a965
21 changed files with 154217 additions and 49 deletions
252
src/client/music_genres.rs
Normal file
252
src/client/music_genres.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{MusicGenre, MusicGenreItem, MusicGenreSection},
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
response::{self, music_item::MusicListMapper},
|
||||
ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QGenre<'a> {
|
||||
context: YTContext<'a>,
|
||||
browse_id: &'a str,
|
||||
params: &'a str,
|
||||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: "FEmusic_moods_and_genres",
|
||||
};
|
||||
|
||||
self.execute_request::<response::MusicGenres, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_genres",
|
||||
"",
|
||||
"browse",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn music_genre<S: AsRef<str>>(&self, genre_id: S) -> Result<MusicGenre, Error> {
|
||||
let genre_id = genre_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QGenre {
|
||||
context,
|
||||
browse_id: "FEmusic_moods_and_genres_category",
|
||||
params: genre_id,
|
||||
};
|
||||
|
||||
self.execute_request::<response::MusicGenre, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_genre",
|
||||
genre_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
_lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||
let content = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
// Skip over the first section (which contains a personalized genre/mood list)
|
||||
let i_start = content.len().saturating_sub(2);
|
||||
let mut content_iter = content.into_iter();
|
||||
for _ in 0..i_start {
|
||||
content_iter.next();
|
||||
}
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let genres = content_iter
|
||||
.enumerate()
|
||||
.flat_map(|(i, grid)| {
|
||||
let mut grid = grid.grid_renderer.items;
|
||||
warnings.append(&mut grid.warnings);
|
||||
grid.c.into_iter().filter_map(move |section| match section {
|
||||
response::music_genres::NavigationButton::MusicNavigationButtonRenderer(
|
||||
btn,
|
||||
) => Some(MusicGenreItem {
|
||||
id: btn.click_command.browse_endpoint.params,
|
||||
name: btn.button_text,
|
||||
is_mood: i == 0,
|
||||
color: btn.solid.left_stripe_color,
|
||||
}),
|
||||
response::music_genres::NavigationButton::None => None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(MapResult {
|
||||
c: genres,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let content = self
|
||||
.contents
|
||||
.single_column_browse_results_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let sections = content
|
||||
.into_iter()
|
||||
.filter_map(|section| {
|
||||
let (name, subgenre_id, items) = match section {
|
||||
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => (
|
||||
shelf
|
||||
.header
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
h.music_carousel_shelf_basic_header_renderer
|
||||
.title
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
shelf.header.and_then(|h| {
|
||||
h.music_carousel_shelf_basic_header_renderer
|
||||
.more_content_button
|
||||
.and_then(|btn| {
|
||||
btn.button_renderer
|
||||
.navigation_endpoint
|
||||
.browse_endpoint
|
||||
.and_then(|browse| {
|
||||
if browse.browse_id
|
||||
== "FEmusic_moods_and_genres_category"
|
||||
{
|
||||
Some(browse.params)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}),
|
||||
shelf.contents,
|
||||
),
|
||||
response::music_item::ItemSection::GridRenderer(grid) => (
|
||||
grid.header
|
||||
.map(|h| h.grid_header_renderer.title)
|
||||
.unwrap_or_default(),
|
||||
None,
|
||||
grid.items,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
mapper.map_response(items);
|
||||
let mut mapped = mapper.conv_items();
|
||||
warnings.append(&mut mapped.warnings);
|
||||
|
||||
Some(MusicGenreSection {
|
||||
name,
|
||||
subgenre_id,
|
||||
playlists: mapped.c,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(MapResult {
|
||||
c: MusicGenre {
|
||||
id: id.to_owned(),
|
||||
name: self.header.music_header_renderer.title,
|
||||
sections,
|
||||
},
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
use path_macro::path;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{model, param::Language};
|
||||
|
||||
#[test]
|
||||
fn map_music_genres() {
|
||||
let json_path = path!("testfiles" / "music_genres" / "genres.json");
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let playlist: response::MusicGenres =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Vec<model::MusicGenreItem>> =
|
||||
playlist.map_response("", Language::En, None).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!("map_music_genres", map_res.c);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default("default", "ggMPOg1uX1lMbVZmbzl6NlJ3")]
|
||||
#[case::mood("mood", "ggMPOg1uX1JOQWZFeDByc2Jm")]
|
||||
fn map_music_genre(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!("testfiles" / "music_genres" / format!("genre_{}.json", name));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let playlist: response::MusicGenre =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<model::MusicGenre> =
|
||||
playlist.map_response(id, Language::En, None).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_music_genre_{}", name), map_res.c);
|
||||
}
|
||||
}
|
||||
Reference in a new issue