Compare commits

..

3 commits

22 changed files with 154303 additions and 60 deletions

View file

@ -26,6 +26,6 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
- [X] **Search suggestions** - [X] **Search suggestions**
- [X] **Radio** - [X] **Radio**
- [X] **Track details** (lyrics, recommendations) - [X] **Track details** (lyrics, recommendations)
- [ ] **Moods** - [X] **Moods/Genres**
- [X] **Charts** - [X] **Charts**
- [X] **New** - [X] **New**

View file

@ -61,6 +61,8 @@ pub async fn download_testfiles(project_root: &Path) {
music_new_albums(&testfiles).await; music_new_albums(&testfiles).await;
music_new_videos(&testfiles).await; music_new_videos(&testfiles).await;
music_charts(&testfiles).await; music_charts(&testfiles).await;
music_genres(&testfiles).await;
music_genre(&testfiles).await;
} }
const CLIENT_TYPES: [ClientType; 5] = [ const CLIENT_TYPES: [ClientType; 5] = [
@ -845,3 +847,32 @@ async fn music_charts(testfiles: &Path) {
rp.query().music_charts(country).await.unwrap(); rp.query().music_charts(country).await.unwrap();
} }
} }
async fn music_genres(testfiles: &Path) {
let mut json_path = testfiles.to_path_buf();
json_path.push("music_genres");
json_path.push("genres.json");
if json_path.exists() {
return;
}
let rp = rp_testfile(&json_path);
rp.query().music_genres().await.unwrap();
}
async fn music_genre(testfiles: &Path) {
for (name, id) in [
("default", "ggMPOg1uX1lMbVZmbzl6NlJ3"),
("mood", "ggMPOg1uX1JOQWZFeDByc2Jm"),
] {
let mut json_path = testfiles.to_path_buf();
json_path.push("music_genres");
json_path.push(&format!("genre_{}.json", name));
if json_path.exists() {
continue;
}
let rp = rp_testfile(&json_path);
rp.query().music_genre(id).await.unwrap();
}
}

View file

@ -6,6 +6,7 @@ mod channel;
mod music_artist; mod music_artist;
mod music_charts; mod music_charts;
mod music_details; mod music_details;
mod music_genres;
mod music_new; mod music_new;
mod music_playlist; mod music_playlist;
mod music_search; mod music_search;

View file

@ -224,7 +224,7 @@ fn map_artist_page(
mapper.map_response(shelf.contents); mapper.map_response(shelf.contents);
} }
} }
response::music_item::ItemSection::None => {} _ => {}
} }
} }

View file

@ -314,7 +314,6 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
.contents .contents
.iter() .iter()
.find_map(|section| match section { .find_map(|section| match section {
response::music_item::ItemSection::MusicShelfRenderer(_) => None,
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => { response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
shelf.header.as_ref().and_then(|h| { shelf.header.as_ref().and_then(|h| {
h.music_carousel_shelf_basic_header_renderer h.music_carousel_shelf_basic_header_renderer
@ -331,7 +330,7 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
}) })
}) })
} }
response::music_item::ItemSection::None => None, _ => None,
}); });
let mut mapper_tracks = MusicListMapper::new(lang); let mut mapper_tracks = MusicListMapper::new(lang);
@ -354,7 +353,7 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => { response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
mapper.map_response(shelf.contents); mapper.map_response(shelf.contents);
} }
response::music_item::ItemSection::None => {} _ => {}
}); });
let mapped_tracks = mapper_tracks.conv_items(); let mapped_tracks = mapper_tracks.conv_items();

252
src/client/music_genres.rs Normal file
View 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);
}
}

View file

@ -267,7 +267,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
response::music_item::ItemSection::MusicCarouselShelfRenderer(sh) => { response::music_item::ItemSection::MusicCarouselShelfRenderer(sh) => {
album_variants = Some(sh.contents) album_variants = Some(sh.contents)
} }
response::music_item::ItemSection::None => (), _ => (),
} }
} }
let shelf = shelf.ok_or(ExtractionError::InvalidData(Cow::Borrowed( let shelf = shelf.ok_or(ExtractionError::InvalidData(Cow::Borrowed(

View file

@ -146,7 +146,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => { response::music_item::ItemSection::MusicCarouselShelfRenderer(shelf) => {
mapper.map_response(shelf.contents); mapper.map_response(shelf.contents);
} }
response::music_item::ItemSection::None => {} _ => {}
} }
} }
} }

View file

@ -2,6 +2,7 @@ pub(crate) mod channel;
pub(crate) mod music_artist; pub(crate) mod music_artist;
pub(crate) mod music_charts; pub(crate) mod music_charts;
pub(crate) mod music_details; pub(crate) mod music_details;
pub(crate) mod music_genres;
pub(crate) mod music_item; pub(crate) mod music_item;
pub(crate) mod music_new; pub(crate) mod music_new;
pub(crate) mod music_playlist; pub(crate) mod music_playlist;
@ -21,6 +22,8 @@ pub(crate) use music_charts::MusicCharts;
pub(crate) use music_details::MusicDetails; pub(crate) use music_details::MusicDetails;
pub(crate) use music_details::MusicLyrics; pub(crate) use music_details::MusicLyrics;
pub(crate) use music_details::MusicRelated; pub(crate) use music_details::MusicRelated;
pub(crate) use music_genres::MusicGenre;
pub(crate) use music_genres::MusicGenres;
pub(crate) use music_item::MusicContinuation; pub(crate) use music_item::MusicContinuation;
pub(crate) use music_new::MusicNew; pub(crate) use music_new::MusicNew;
pub(crate) use music_playlist::MusicPlaylist; pub(crate) use music_playlist::MusicPlaylist;

View file

@ -4,24 +4,20 @@ use serde_with::{serde_as, DefaultOnError};
use crate::serializer::text::Text; use crate::serializer::text::Text;
use super::{ use super::{
music_item::{Grid, ItemSection, MusicThumbnailRenderer}, music_item::{
ContentsRenderer, SectionList, Tab, Grid, ItemSection, MusicThumbnailRenderer, SimpleHeader, SingleColumnBrowseResult,
},
SectionList, Tab,
}; };
/// Response model for YouTube Music artists /// Response model for YouTube Music artists
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtist { pub(crate) struct MusicArtist {
pub contents: Contents<ItemSection>, pub contents: SingleColumnBrowseResult<Tab<SectionList<ItemSection>>>,
pub header: Header, pub header: Header,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Contents<T> {
pub single_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<T>>>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct Header { pub(crate) struct Header {
@ -64,19 +60,5 @@ pub(crate) struct SubscriptionButtonRenderer {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtistAlbums { pub(crate) struct MusicArtistAlbums {
pub header: SimpleHeader, pub header: SimpleHeader,
pub contents: Contents<Grid>, pub contents: SingleColumnBrowseResult<Tab<SectionList<Grid>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SimpleHeader {
pub music_header_renderer: SimpleHeaderRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SimpleHeaderRenderer {
#[serde_as(as = "Text")]
pub title: String,
} }

View file

@ -0,0 +1,62 @@
use serde::Deserialize;
use serde_with::{rust::deserialize_ignore_any, serde_as};
use crate::serializer::{text::Text, MapResult, VecLogError};
use super::{
music_item::{ItemSection, SimpleHeader, SingleColumnBrowseResult},
url_endpoint::BrowseEndpointWrap,
SectionList, Tab,
};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicGenres {
pub contents: SingleColumnBrowseResult<Tab<SectionList<Grid>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Grid {
pub grid_renderer: GridRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GridRenderer {
#[serde_as(as = "VecLogError<_>")]
pub items: MapResult<Vec<NavigationButton>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum NavigationButton {
#[serde(rename_all = "camelCase")]
MusicNavigationButtonRenderer(NavigationButtonRenderer),
#[serde(other, deserialize_with = "deserialize_ignore_any")]
None,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NavigationButtonRenderer {
#[serde_as(as = "Text")]
pub button_text: String,
pub solid: NavigationButtonColor,
pub click_command: BrowseEndpointWrap,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NavigationButtonColor {
pub left_stripe_color: u32,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicGenre {
pub contents: SingleColumnBrowseResult<Tab<SectionList<ItemSection>>>,
pub header: SimpleHeader,
}

View file

@ -26,6 +26,7 @@ pub(crate) enum ItemSection {
#[serde(alias = "musicPlaylistShelfRenderer")] #[serde(alias = "musicPlaylistShelfRenderer")]
MusicShelfRenderer(MusicShelf), MusicShelfRenderer(MusicShelf),
MusicCarouselShelfRenderer(MusicCarouselShelf), MusicCarouselShelfRenderer(MusicCarouselShelf),
GridRenderer(GridRenderer),
#[serde(other, deserialize_with = "deserialize_ignore_any")] #[serde(other, deserialize_with = "deserialize_ignore_any")]
None, None,
} }
@ -357,6 +358,41 @@ pub(crate) struct Grid {
pub(crate) struct GridRenderer { pub(crate) struct GridRenderer {
#[serde_as(as = "VecLogError<_>")] #[serde_as(as = "VecLogError<_>")]
pub items: MapResult<Vec<MusicResponseItem>>, pub items: MapResult<Vec<MusicResponseItem>>,
pub header: Option<GridHeader>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GridHeader {
pub grid_header_renderer: GridHeaderRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GridHeaderRenderer {
#[serde_as(as = "Text")]
pub title: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SingleColumnBrowseResult<T> {
pub single_column_browse_results_renderer: ContentsRenderer<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SimpleHeader {
pub music_header_renderer: SimpleHeaderRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SimpleHeaderRenderer {
#[serde_as(as = "Text")]
pub title: String,
} }
/* /*
@ -764,10 +800,12 @@ impl MusicListMapper {
Ok(Some(MusicEntityType::Album)) Ok(Some(MusicEntityType::Album))
} }
MusicPageType::Playlist => { MusicPageType::Playlist => {
// When the playlist subtitle has only 1 part, it is a playlist from YT Music
// (featured on the startpage or in genres)
let from_ytm = subtitle_p2 let from_ytm = subtitle_p2
.as_ref() .as_ref()
.map(|p| p.first_str() == util::YT_MUSIC_NAME) .map(|p| p.first_str() == util::YT_MUSIC_NAME)
.unwrap_or_default(); .unwrap_or(true);
let channel = subtitle_p2.and_then(|p| { let channel = subtitle_p2.and_then(|p| {
p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()) p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok())
}); });

View file

@ -1,15 +1,12 @@
use serde::Deserialize; use serde::Deserialize;
use super::{music_item::Grid, ContentsRenderer, SectionList, Tab}; use super::{
music_item::{Grid, SingleColumnBrowseResult},
SectionList, Tab,
};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct MusicNew { pub(crate) struct MusicNew {
pub contents: Contents, pub contents: SingleColumnBrowseResult<Tab<SectionList<Grid>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Contents {
pub single_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<Grid>>>,
} }

View file

@ -3,23 +3,21 @@ use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::{Text, TextComponents}; use crate::serializer::text::{Text, TextComponents};
use super::music_item::{ItemSection, MusicContentsRenderer, MusicThumbnailRenderer}; use super::{
use super::{ContentsRenderer, Tab}; music_item::{
ItemSection, MusicContentsRenderer, MusicThumbnailRenderer, SingleColumnBrowseResult,
},
Tab,
};
/// Response model for YouTube Music playlists and albums /// Response model for YouTube Music playlists and albums
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct MusicPlaylist { pub(crate) struct MusicPlaylist {
pub contents: Contents, pub contents: SingleColumnBrowseResult<Tab<SectionList>>,
pub header: Option<Header>, pub header: Option<Header>,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Contents {
pub single_column_browse_results_renderer: ContentsRenderer<Tab<SectionList>>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct SectionList { pub(crate) struct SectionList {

View file

@ -0,0 +1,210 @@
---
source: src/client/music_genres.rs
expression: map_res.c
---
[
MusicGenreItem(
id: "ggMPOg1uXzJxenpkRkNOMk1y",
name: "Black Lives Matter",
is_mood: true,
color: 4280098077,
),
MusicGenreItem(
id: "ggMPOg1uX1JOQWZFeDByc2Jm",
name: "Chill",
is_mood: true,
color: 4288988671,
),
MusicGenreItem(
id: "ggMPOg1uX044Z2o5WERLckpU",
name: "Commute",
is_mood: true,
color: 4294951424,
),
MusicGenreItem(
id: "ggMPOg1uX2lRZUZiMnNrQnJW",
name: "Energy Boosters",
is_mood: true,
color: 4294961024,
),
MusicGenreItem(
id: "ggMPOg1uX2FVNHc2SVM4Y0dW",
name: "Feel Good",
is_mood: true,
color: 4289003428,
),
MusicGenreItem(
id: "ggMPOg1uX0NvNGNhWThMYWRh",
name: "Focus",
is_mood: true,
color: 4291611852,
),
MusicGenreItem(
id: "ggMPOg1uX1lIYXpVckRyZ0Qy",
name: "Party",
is_mood: true,
color: 4290018303,
),
MusicGenreItem(
id: "ggMPOg1uXzZBTTFqOGM1Ujda",
name: "Romance",
is_mood: true,
color: 4291559424,
),
MusicGenreItem(
id: "ggMPOg1uX1MxaFQ3Z0JMZkN4",
name: "Sleep",
is_mood: true,
color: 4286267099,
),
MusicGenreItem(
id: "ggMPOg1uXzIxYkNac21YZ2Z0",
name: "Workout",
is_mood: true,
color: 4281564671,
),
MusicGenreItem(
id: "ggMPOg1uX0UzWGxlRE5jMDVk",
name: "African",
is_mood: false,
color: 4278232339,
),
MusicGenreItem(
id: "ggMPOg1uX3VOQWxsblVZTFNE",
name: "Arabic",
is_mood: false,
color: 4293020416,
),
MusicGenreItem(
id: "ggMPOg1uX2NXUkgxdW0zUHJp",
name: "Blues",
is_mood: false,
color: 4281564671,
),
MusicGenreItem(
id: "ggMPOg1uX0JrcUJGUHhxaTFV",
name: "Bollywood & Indian",
is_mood: false,
color: 4286267099,
),
MusicGenreItem(
id: "ggMPOg1uX1N4VmduTmdUR3dm",
name: "Classical",
is_mood: false,
color: 4291611852,
),
MusicGenreItem(
id: "ggMPOg1uX2dlOG5sUHdpOWtC",
name: "Country & Americana",
is_mood: false,
color: 4281564671,
),
MusicGenreItem(
id: "ggMPOg1uXzBRcGpUakRJQXJ0",
name: "Dance & Electronic",
is_mood: false,
color: 4278227647,
),
MusicGenreItem(
id: "ggMPOg1uX0JpQ0g0RUplQ3By",
name: "Decades",
is_mood: false,
color: 4289003428,
),
MusicGenreItem(
id: "ggMPOg1uX3JBUDJTM2ZUUVJM",
name: "Fall",
is_mood: false,
color: 4293020416,
),
MusicGenreItem(
id: "ggMPOg1uX0dZanZjMGNoTEw1",
name: "Family",
is_mood: false,
color: 4278227647,
),
MusicGenreItem(
id: "ggMPOg1uX3luWTExakRvajZl",
name: "Folk & Acoustic",
is_mood: false,
color: 4278232339,
),
MusicGenreItem(
id: "ggMPOg1uX1NSeDlUaUU5Nlc2",
name: "Hip-Hop",
is_mood: false,
color: 4293020416,
),
MusicGenreItem(
id: "ggMPOg1uX2hhQTl4QVUzN2V5",
name: "Indie & Alternative",
is_mood: false,
color: 4291611852,
),
MusicGenreItem(
id: "ggMPOg1uXzAwSjVITDBZckJR",
name: "J-Pop",
is_mood: false,
color: 4294932735,
),
MusicGenreItem(
id: "ggMPOg1uX3lPcDFRaE9wM1BS",
name: "Jazz",
is_mood: false,
color: 4288988671,
),
MusicGenreItem(
id: "ggMPOg1uX0JrbjBDOFFPSzJW",
name: "K-Pop",
is_mood: false,
color: 4290018303,
),
MusicGenreItem(
id: "ggMPOg1uX1NXRmgzQ21ySXlU",
name: "Latin",
is_mood: false,
color: 4294951424,
),
MusicGenreItem(
id: "ggMPOg1uXzJxdDZFTHlDdUdW",
name: "Metal",
is_mood: false,
color: 4287401100,
),
MusicGenreItem(
id: "ggMPOg1uX1lMbVZmbzl6NlJ3",
name: "Pop",
is_mood: false,
color: 4294932735,
),
MusicGenreItem(
id: "ggMPOg1uX1kxNFJCWUR3WTBM",
name: "R&B & Soul",
is_mood: false,
color: 4286267099,
),
MusicGenreItem(
id: "ggMPOg1uX1JUc2lFcDFuUUth",
name: "Reggae & Caribbean",
is_mood: false,
color: 4294961024,
),
MusicGenreItem(
id: "ggMPOg1uX2VQZldWYXA5YUp1",
name: "Rock",
is_mood: false,
color: 4291559424,
),
MusicGenreItem(
id: "ggMPOg1uX1IxN1FMRWozWXZ5",
name: "Schlager",
is_mood: false,
color: 4290018303,
),
MusicGenreItem(
id: "ggMPOg1uX2xKb0I5eHVvNUQw",
name: "Soundtracks & Musicals",
is_mood: false,
color: 4283297535,
),
]

View file

@ -1285,3 +1285,52 @@ pub struct MusicCharts {
/// Set of available countries to fetch charts from /// Set of available countries to fetch charts from
pub available_countries: BTreeSet<Country>, pub available_countries: BTreeSet<Country>,
} }
/// YouTube Music genre/mood list item
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct MusicGenreItem {
/// Unique YouTube Music genre ID
pub id: String,
/// Genre name
pub name: String,
/// Is it a mood (e.g. Chill, Focus, Party)
pub is_mood: bool,
/// Color of the genre button
///
/// Encoded as a 32-bit integer with the following format (8 bits per number):
///
/// `[Alpha][R][G][B]`
///
/// **Note:** Alpa/Opacity is always set to 0xFF.
pub color: u32,
}
/// YouTube Music genre/mood content
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct MusicGenre {
/// Unique YouTube Music genre ID
pub id: String,
/// Genre name
pub name: String,
/// List of sections containing the content
pub sections: Vec<MusicGenreSection>,
}
/// YouTube Music genre/mood content section
///
/// Genre pages in YouTube Music are split into sections. These have a name
/// and contain several playlists. If the section is showing a subgenre
/// (e.g "2000s" from "Decades"), the section also includes a `subgenre_id`
/// for fetching more content of that subgenre.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct MusicGenreSection {
/// Name of the genre section
pub name: String,
/// Subgenre ID to fetch more content
pub subgenre_id: Option<String>,
/// List of playlists of the genre section
pub playlists: Vec<MusicPlaylistItem>,
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,884 @@
{
"contents": {
"singleColumnBrowseResultsRenderer": {
"tabs": [
{
"tabRenderer": {
"content": {
"sectionListRenderer": {
"contents": [
{
"gridRenderer": {
"header": {
"gridHeaderRenderer": {
"title": {
"runs": [
{
"text": "Moods & moments"
}
]
}
}
},
"itemSize": "COLLECTION_STYLE_ITEM_SIZE_MEDIUM",
"items": [
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Black Lives Matter"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uXzJxenpkRkNOMk1y"
},
"clickTrackingParams": "CCcQuKEFGAAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4280098077
},
"trackingParams": "CCcQuKEFGAAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Chill"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1JOQWZFeDByc2Jm"
},
"clickTrackingParams": "CCYQuKEFGAEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4288988671
},
"trackingParams": "CCYQuKEFGAEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Commute"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX044Z2o5WERLckpU"
},
"clickTrackingParams": "CCUQuKEFGAIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4294951424
},
"trackingParams": "CCUQuKEFGAIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Energy Boosters"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2lRZUZiMnNrQnJW"
},
"clickTrackingParams": "CCQQuKEFGAMiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4294961024
},
"trackingParams": "CCQQuKEFGAMiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Feel Good"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2FVNHc2SVM4Y0dW"
},
"clickTrackingParams": "CCMQuKEFGAQiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4289003428
},
"trackingParams": "CCMQuKEFGAQiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Focus"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX0NvNGNhWThMYWRh"
},
"clickTrackingParams": "CCIQuKEFGAUiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4291611852
},
"trackingParams": "CCIQuKEFGAUiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Party"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1lIYXpVckRyZ0Qy"
},
"clickTrackingParams": "CCEQuKEFGAYiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4290018303
},
"trackingParams": "CCEQuKEFGAYiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Romance"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uXzZBTTFqOGM1Ujda"
},
"clickTrackingParams": "CCAQuKEFGAciEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4291559424
},
"trackingParams": "CCAQuKEFGAciEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Sleep"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1MxaFQ3Z0JMZkN4"
},
"clickTrackingParams": "CB8QuKEFGAgiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4286267099
},
"trackingParams": "CB8QuKEFGAgiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Workout"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uXzIxYkNac21YZ2Z0"
},
"clickTrackingParams": "CB4QuKEFGAkiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4281564671
},
"trackingParams": "CB4QuKEFGAkiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
}
],
"trackingParams": "CB0Q6IsCGAAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"gridRenderer": {
"header": {
"gridHeaderRenderer": {
"title": {
"runs": [
{
"text": "Genres"
}
]
}
}
},
"itemSize": "COLLECTION_STYLE_ITEM_SIZE_MEDIUM",
"items": [
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "African"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX0UzWGxlRE5jMDVk"
},
"clickTrackingParams": "CBwQuKEFGAAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4278232339
},
"trackingParams": "CBwQuKEFGAAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Arabic"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX3VOQWxsblVZTFNE"
},
"clickTrackingParams": "CBsQuKEFGAEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4293020416
},
"trackingParams": "CBsQuKEFGAEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Blues"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2NXUkgxdW0zUHJp"
},
"clickTrackingParams": "CBoQuKEFGAIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4281564671
},
"trackingParams": "CBoQuKEFGAIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Bollywood & Indian"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX0JrcUJGUHhxaTFV"
},
"clickTrackingParams": "CBkQuKEFGAMiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4286267099
},
"trackingParams": "CBkQuKEFGAMiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Classical"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1N4VmduTmdUR3dm"
},
"clickTrackingParams": "CBgQuKEFGAQiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4291611852
},
"trackingParams": "CBgQuKEFGAQiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Country & Americana"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2dlOG5sUHdpOWtC"
},
"clickTrackingParams": "CBcQuKEFGAUiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4281564671
},
"trackingParams": "CBcQuKEFGAUiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Dance & Electronic"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uXzBRcGpUakRJQXJ0"
},
"clickTrackingParams": "CBYQuKEFGAYiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4278227647
},
"trackingParams": "CBYQuKEFGAYiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Decades"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX0JpQ0g0RUplQ3By"
},
"clickTrackingParams": "CBUQuKEFGAciEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4289003428
},
"trackingParams": "CBUQuKEFGAciEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Fall"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX3JBUDJTM2ZUUVJM"
},
"clickTrackingParams": "CBQQuKEFGAgiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4293020416
},
"trackingParams": "CBQQuKEFGAgiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Family"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX0dZanZjMGNoTEw1"
},
"clickTrackingParams": "CBMQuKEFGAkiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4278227647
},
"trackingParams": "CBMQuKEFGAkiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Folk & Acoustic"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX3luWTExakRvajZl"
},
"clickTrackingParams": "CBIQuKEFGAoiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4278232339
},
"trackingParams": "CBIQuKEFGAoiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Hip-Hop"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1NSeDlUaUU5Nlc2"
},
"clickTrackingParams": "CBEQuKEFGAsiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4293020416
},
"trackingParams": "CBEQuKEFGAsiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Indie & Alternative"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2hhQTl4QVUzN2V5"
},
"clickTrackingParams": "CBAQuKEFGAwiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4291611852
},
"trackingParams": "CBAQuKEFGAwiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "J-Pop"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uXzAwSjVITDBZckJR"
},
"clickTrackingParams": "CA8QuKEFGA0iEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4294932735
},
"trackingParams": "CA8QuKEFGA0iEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Jazz"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX3lPcDFRaE9wM1BS"
},
"clickTrackingParams": "CA4QuKEFGA4iEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4288988671
},
"trackingParams": "CA4QuKEFGA4iEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "K-Pop"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX0JrbjBDOFFPSzJW"
},
"clickTrackingParams": "CA0QuKEFGA8iEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4290018303
},
"trackingParams": "CA0QuKEFGA8iEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Latin"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1NXRmgzQ21ySXlU"
},
"clickTrackingParams": "CAwQuKEFGBAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4294951424
},
"trackingParams": "CAwQuKEFGBAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Metal"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uXzJxdDZFTHlDdUdW"
},
"clickTrackingParams": "CAsQuKEFGBEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4287401100
},
"trackingParams": "CAsQuKEFGBEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Pop"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1lMbVZmbzl6NlJ3"
},
"clickTrackingParams": "CAoQuKEFGBIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4294932735
},
"trackingParams": "CAoQuKEFGBIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "R&B & Soul"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1kxNFJCWUR3WTBM"
},
"clickTrackingParams": "CAkQuKEFGBMiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4286267099
},
"trackingParams": "CAkQuKEFGBMiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Reggae & Caribbean"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1JUc2lFcDFuUUth"
},
"clickTrackingParams": "CAgQuKEFGBQiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4294961024
},
"trackingParams": "CAgQuKEFGBQiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Rock"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2VQZldWYXA5YUp1"
},
"clickTrackingParams": "CAcQuKEFGBUiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4291559424
},
"trackingParams": "CAcQuKEFGBUiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Schlager"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX1IxN1FMRWozWXZ5"
},
"clickTrackingParams": "CAYQuKEFGBYiEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4290018303
},
"trackingParams": "CAYQuKEFGBYiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
{
"musicNavigationButtonRenderer": {
"buttonText": {
"runs": [
{
"text": "Soundtracks & Musicals"
}
]
},
"clickCommand": {
"browseEndpoint": {
"browseId": "FEmusic_moods_and_genres_category",
"params": "ggMPOg1uX2xKb0I5eHVvNUQw"
},
"clickTrackingParams": "CAUQuKEFGBciEwj3pPKSotn7AhVEgXwKHWeCDO8="
},
"solid": {
"leftStripeColor": 4283297535
},
"trackingParams": "CAUQuKEFGBciEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
}
],
"trackingParams": "CAQQ6IsCGAEiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
}
],
"trackingParams": "CAMQui8iEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
"trackingParams": "CAIQ8JMBGAAiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
}
]
}
},
"header": {
"musicHeaderRenderer": {
"title": {
"runs": [
{
"text": "Moods & genres"
}
]
},
"trackingParams": "CAEQ4HIiEwj3pPKSotn7AhVEgXwKHWeCDO8="
}
},
"maxAgeStoreSeconds": 300,
"responseContext": {
"serviceTrackingParams": [
{
"params": [
{
"key": "browse_id",
"value": "FEmusic_moods_and_genres"
},
{
"key": "browse_id_prefix",
"value": ""
},
{
"key": "logged_in",
"value": "0"
},
{
"key": "e",
"value": "1714255,23804281,23882503,23918597,23934970,23946420,23966208,23983296,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24120819,24135310,24140247,24161116,24162919,24164186,24169501,24181174,24187043,24187377,24191629,24197450,24199724,24200839,24211178,24217535,24219713,24241378,24248092,24255165,24255543,24255545,24262346,24263796,24265425,24267564,24268142,24272788,24279196,24282154,24287327,24288043,24288441,24290376,24290971,24292955,24293803,24294554,24297392,24299747,24390374,24390674,24390918,24391018,24391543,24392268,24392499,24394017,24401557,24402891,24405024,24406605,24406620,24407200,24414076,24414162,24415580,24415864,24415866,24416290,24416708,24416786,24417873,24418299,24418762,24419035,24420756,24421162,24421892,24423785,24424028,24424572,24425861,24425905,24426900,24426909,24427242,24428900,24430665,24431335,24590921,39322504,39322574"
}
],
"service": "GFEEDBACK"
},
{
"params": [
{
"key": "c",
"value": "WEB_REMIX"
},
{
"key": "cver",
"value": "1.20221128.01.00"
},
{
"key": "yt_li",
"value": "0"
},
{
"key": "GetBrowseMoodsAndGenresPage_rid",
"value": "0x9ff4996d90fe6995"
}
],
"service": "CSI"
},
{
"params": [
{
"key": "client.version",
"value": "1.20000101"
},
{
"key": "client.name",
"value": "WEB_REMIX"
},
{
"key": "client.fexp",
"value": "24418299,24430665,24265425,24036948,24392499,24241378,24415866,24392268,24416786,24267564,24263796,24406620,24002025,24421892,24262346,24197450,24248092,24255165,24004644,24402891,24288441,24423785,24431335,24199724,24390674,24007246,24426900,24290376,24162919,24421162,24187377,24293803,24391018,24394017,24297392,24424572,24401557,23934970,1714255,24425861,23998056,24120819,39322574,24135310,24424028,24294554,24407200,24211178,24414076,39322504,24287327,24279196,24140247,24219713,24415864,24191629,24080738,24255545,24419035,24164186,24282154,24288043,24299747,24405024,24428900,24420756,24187043,24268142,24415580,24427242,24217535,23882503,24417873,24272788,24390374,24001373,23804281,23946420,24200839,24077241,24181174,23966208,24391543,24390918,24426909,24292955,24161116,24290971,24169501,24418762,24590921,24414162,24255543,24406605,24416290,23983296,23918597,24034168,24425905,24002022,24416708"
}
],
"service": "ECATCHER"
}
],
"visitorData": "CgtKb0FjYk10YUFqWSian6ScBg%3D%3D"
},
"trackingParams": "CAAQhGciEwj3pPKSotn7AhVEgXwKHWeCDO8="
}

View file

@ -12,8 +12,8 @@ use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
use rustypipe::error::{Error, ExtractionError}; use rustypipe::error::{Error, ExtractionError};
use rustypipe::model::richtext::ToPlaintext; use rustypipe::model::richtext::ToPlaintext;
use rustypipe::model::{ use rustypipe::model::{
AlbumType, AudioCodec, AudioFormat, Channel, FromYtItem, MusicEntityType, Paginator, UrlTarget, AlbumType, AudioCodec, AudioFormat, Channel, FromYtItem, MusicEntityType, MusicGenre,
Verification, VideoCodec, VideoFormat, YouTubeItem, YtStream, Paginator, UrlTarget, Verification, VideoCodec, VideoFormat, YouTubeItem, YtStream,
}; };
use rustypipe::param::search_filter::{self, SearchFilter}; use rustypipe::param::search_filter::{self, SearchFilter};
@ -1625,14 +1625,13 @@ async fn music_search_episode() {
#[rstest] #[rstest]
#[case::single( #[case::single(
"black mamba", "black mamba aespa",
"Black Mamba", "Black Mamba",
"MPREb_OpHWHwyNOuY", "MPREb_OpHWHwyNOuY",
"aespa", "aespa",
"UCEdZAdnnKqbaHOlv8nM6OtA", "UCEdZAdnnKqbaHOlv8nM6OtA",
2020, 2020,
AlbumType::Single, AlbumType::Single
2
)] )]
#[case::ep( #[case::ep(
"waldbrand", "waldbrand",
@ -1641,8 +1640,7 @@ async fn music_search_episode() {
"Madeline Juno", "Madeline Juno",
"UCpJyCbFbdTrx0M90HCNBHFQ", "UCpJyCbFbdTrx0M90HCNBHFQ",
2016, 2016,
AlbumType::Ep, AlbumType::Ep
1
)] )]
#[case::album( #[case::album(
"märchen enden gut", "märchen enden gut",
@ -1651,8 +1649,7 @@ async fn music_search_episode() {
"Oonagh", "Oonagh",
"UC_vmjW5e1xEHhYjY2a0kK1A", "UC_vmjW5e1xEHhYjY2a0kK1A",
2016, 2016,
AlbumType::Album, AlbumType::Album
1
)] )]
#[tokio::test] #[tokio::test]
async fn music_search_albums( async fn music_search_albums(
@ -1663,7 +1660,6 @@ async fn music_search_albums(
#[case] artist_id: &str, #[case] artist_id: &str,
#[case] year: u16, #[case] year: u16,
#[case] album_type: AlbumType, #[case] album_type: AlbumType,
#[case] n_pages: usize,
) { ) {
let rp = RustyPipe::builder().strict().build(); let rp = RustyPipe::builder().strict().build();
let res = rp.query().music_search_albums(query).await.unwrap(); let res = rp.query().music_search_albums(query).await.unwrap();
@ -1689,7 +1685,7 @@ async fn music_search_albums(
assert_eq!(res.corrected_query, None); assert_eq!(res.corrected_query, None);
assert_next(res.items, rp.query(), 15, n_pages).await; assert_next(res.items, rp.query(), 15, 1).await;
} }
#[tokio::test] #[tokio::test]
@ -2108,6 +2104,85 @@ async fn music_new_videos() {
} }
} }
#[tokio::test]
async fn music_genres() {
let rp = RustyPipe::builder().strict().build();
let genres = rp.query().music_genres().await.unwrap();
let chill = genres
.iter()
.find(|g| g.id == "ggMPOg1uX1JOQWZFeDByc2Jm")
.unwrap();
assert_eq!(chill.name, "Chill");
assert!(chill.is_mood);
let pop = genres
.iter()
.find(|g| g.id == "ggMPOg1uX1lMbVZmbzl6NlJ3")
.unwrap();
assert_eq!(pop.name, "Pop");
assert!(!pop.is_mood);
genres.iter().for_each(|g| {
assert_gte(g.color, 0xff000000, "color");
});
}
#[rstest]
#[case::chill("ggMPOg1uX1JOQWZFeDByc2Jm", "Chill")]
#[case::pop("ggMPOg1uX1lMbVZmbzl6NlJ3", "Pop")]
#[tokio::test]
async fn music_genre(#[case] id: &str, #[case] name: &str) {
let rp = RustyPipe::builder().strict().build();
let genre = rp.query().music_genre(id).await.unwrap();
fn check_music_genre(genre: MusicGenre, id: &str, name: &str) -> Vec<(String, String)> {
assert_eq!(genre.id, id);
assert_eq!(genre.name, name);
assert_gte(genre.sections.len(), 2, "genre sections");
let mut subgenres = Vec::new();
genre.sections.iter().for_each(|section| {
assert!(!section.name.is_empty());
section.playlists.iter().for_each(|playlist| {
assert_playlist_id(&playlist.id);
assert!(!playlist.name.is_empty());
assert!(!playlist.thumbnail.is_empty(), "got no cover");
if !playlist.from_ytm {
assert!(
playlist.channel.is_some(),
"pl: {}, got no channel",
playlist.id
);
let channel = playlist.channel.as_ref().unwrap();
assert_channel_id(&channel.id);
assert!(!channel.name.is_empty());
assert_gte(playlist.track_count.unwrap(), 2, "tracks");
} else {
assert!(playlist.channel.is_none());
}
});
if let Some(subgenre_id) = &section.subgenre_id {
subgenres.push((subgenre_id.to_owned(), section.name.to_owned()));
}
});
subgenres
}
let subgenres = check_music_genre(genre, id, name);
if name == "Chill" {
assert_gte(subgenres.len(), 2, "subgenres");
}
for (id, name) in subgenres {
let genre = rp.query().music_genre(&id).await.unwrap();
check_music_genre(genre, &id, &name);
}
}
//#AB TESTS //#AB TESTS
const VISITOR_DATA_SEARCH_CHANNEL_HANDLES: &str = "CgszYlc1Yk1WZGRCSSjrwOSbBg%3D%3D"; const VISITOR_DATA_SEARCH_CHANNEL_HANDLES: &str = "CgszYlc1Yk1WZGRCSSjrwOSbBg%3D%3D";