rustypipe/src/client/music_genres.rs

244 lines
8.3 KiB
Rust

use std::borrow::Cow;
use crate::{
error::{Error, ExtractionError},
model::{MusicGenre, MusicGenreItem, MusicGenreSection},
serializer::MapResult,
};
use super::{
response::{self, music_item::MusicListMapper},
ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
};
impl RustyPipeQuery {
/// Get a list of moods and genres from YouTube Music
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
}
/// Get the playlists from a YouTube Music genre
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 = QBrowseParams {
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::DeobfData>,
) -> 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.contents;
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::DeobfData>,
) -> 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, util::tests::TESTFILES};
#[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_{name}.json"));
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);
}
}