Compare commits
3 commits
c6cd364b9e
...
e502ec5d20
Author | SHA1 | Date | |
---|---|---|---|
e502ec5d20 | |||
a24c2686b1 | |||
de7bd2a965 |
22 changed files with 154303 additions and 60 deletions
|
@ -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**
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -224,7 +224,7 @@ fn map_artist_page(
|
||||||
mapper.map_response(shelf.contents);
|
mapper.map_response(shelf.contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response::music_item::ItemSection::None => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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 => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
62
src/client/response/music_genres.rs
Normal file
62
src/client/response/music_genres.rs
Normal 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,
|
||||||
|
}
|
|
@ -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())
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>>>,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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>,
|
||||||
|
}
|
||||||
|
|
79266
testfiles/music_genres/genre_default.json
Normal file
79266
testfiles/music_genres/genre_default.json
Normal file
File diff suppressed because it is too large
Load diff
66670
testfiles/music_genres/genre_mood.json
Normal file
66670
testfiles/music_genres/genre_mood.json
Normal file
File diff suppressed because it is too large
Load diff
884
testfiles/music_genres/genres.json
Normal file
884
testfiles/music_genres/genres.json
Normal 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="
|
||||||
|
}
|
|
@ -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) = §ion.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";
|
||||||
|
|
Loading…
Reference in a new issue