feat: add music album
This commit is contained in:
parent
566b3e5bfc
commit
3b738a55ad
12 changed files with 14904 additions and 17 deletions
|
|
@ -38,6 +38,7 @@ pub async fn download_testfiles(project_root: &Path) {
|
|||
|
||||
music_playlist(&testfiles).await;
|
||||
music_playlist_cont(&testfiles).await;
|
||||
music_album(&testfiles).await;
|
||||
}
|
||||
|
||||
const CLIENT_TYPES: [ClientType; 5] = [
|
||||
|
|
@ -498,3 +499,21 @@ async fn music_playlist_cont(testfiles: &Path) {
|
|||
let rp = rp_testfile(&json_path);
|
||||
playlist.tracks.next(&rp.query()).await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
async fn music_album(testfiles: &Path) {
|
||||
for (name, id) in [
|
||||
("one_artist", "MPREb_nlBWQROfvjo"),
|
||||
("various_artists", "MPREb_8QkDeEIawvX"),
|
||||
("single", "MPREb_bHfHGoy7vuv"),
|
||||
] {
|
||||
let mut json_path = testfiles.to_path_buf();
|
||||
json_path.push("music_playlist");
|
||||
json_path.push(format!("album_{}.json", name));
|
||||
if json_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_album(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::borrow::Cow;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{ChannelId, MusicPlaylist, Paginator, TrackItem},
|
||||
model::{AlbumType, ChannelId, MusicAlbum, MusicPlaylist, Paginator, TrackItem},
|
||||
serializer::MapResult,
|
||||
util::{self, TryRemove},
|
||||
};
|
||||
|
|
@ -49,6 +49,23 @@ impl RustyPipeQuery {
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn music_album(&self, album_id: &str) -> Result<MusicAlbum, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
context,
|
||||
browse_id: album_id.to_owned(),
|
||||
};
|
||||
|
||||
self.execute_request::<response::MusicPlaylist, _, _>(
|
||||
ClientType::DesktopMusic,
|
||||
"music_album",
|
||||
album_id,
|
||||
"browse",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||
|
|
@ -70,11 +87,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
.content
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.try_swap_remove(0)
|
||||
.into_iter()
|
||||
.find_map(|section| match section {
|
||||
response::music_playlist::ItemSection::MusicShelfRenderer(shelf) => Some(shelf),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
|
||||
"no sectionListRenderer content",
|
||||
)))?
|
||||
.music_shelf_renderer;
|
||||
)))?;
|
||||
|
||||
let playlist_id = shelf
|
||||
.playlist_id
|
||||
|
|
@ -156,6 +176,118 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicPlaylistCont {
|
|||
}
|
||||
}
|
||||
|
||||
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_lang: crate::param::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let header = self.header.music_detail_header_renderer;
|
||||
|
||||
let mut content = self.contents.single_column_browse_results_renderer.contents;
|
||||
let sections = content
|
||||
.try_swap_remove(0)
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut shelf = None;
|
||||
let mut album_versions = None;
|
||||
for section in sections {
|
||||
match section {
|
||||
response::music_playlist::ItemSection::MusicShelfRenderer(sh) => shelf = Some(sh),
|
||||
response::music_playlist::ItemSection::MusicCarouselShelfRenderer { contents } => {
|
||||
album_versions = Some(contents)
|
||||
}
|
||||
response::music_playlist::ItemSection::None => (),
|
||||
}
|
||||
}
|
||||
let shelf = shelf.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
|
||||
"no sectionListRenderer content",
|
||||
)))?;
|
||||
|
||||
let playlist_id = header.menu.and_then(|mut menu| {
|
||||
menu.menu_renderer
|
||||
.top_level_buttons
|
||||
.try_swap_remove(0)
|
||||
.map(|btn| {
|
||||
btn.button_renderer
|
||||
.navigation_endpoint
|
||||
.watch_playlist_endpoint
|
||||
.playlist_id
|
||||
})
|
||||
});
|
||||
|
||||
let subtitle_len = header.subtitle.0.len();
|
||||
if subtitle_len < 5 {
|
||||
return Err(ExtractionError::InvalidData(Cow::Owned(format!(
|
||||
"header text is missing elements: {}",
|
||||
header.subtitle.to_string()
|
||||
))));
|
||||
}
|
||||
|
||||
let mut artists = Vec::new();
|
||||
let mut artists_txt = String::new();
|
||||
|
||||
let mut st_parts = header.subtitle.0.into_iter();
|
||||
let album_type_txt = st_parts.next().unwrap();
|
||||
st_parts.next();
|
||||
|
||||
for _ in 0..subtitle_len - 4 {
|
||||
let part = st_parts.next().unwrap();
|
||||
artists_txt += part.as_str();
|
||||
|
||||
if let Ok(a) = ChannelId::try_from(part) {
|
||||
artists.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
st_parts.next();
|
||||
let year_txt = st_parts.next().unwrap();
|
||||
|
||||
let by_va = artists_txt == "Various Artists";
|
||||
|
||||
// TODO: add support for different languages
|
||||
let album_type = match album_type_txt.as_str() {
|
||||
"Single" => AlbumType::Single,
|
||||
"EP" => AlbumType::Ep,
|
||||
_ => AlbumType::Album,
|
||||
};
|
||||
let year = util::parse_numeric(year_txt.as_str())
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut mapper = match by_va {
|
||||
true => MusicListMapper::<TrackItem>::new(),
|
||||
false => {
|
||||
MusicListMapper::<TrackItem>::with_artists(artists.clone(), artists_txt.clone())
|
||||
}
|
||||
};
|
||||
mapper.map_response(shelf.contents);
|
||||
|
||||
Ok(MapResult {
|
||||
c: MusicAlbum {
|
||||
id: id.to_owned(),
|
||||
playlist_id,
|
||||
name: header.title,
|
||||
cover: header.thumbnail.into(),
|
||||
artists,
|
||||
artists_txt,
|
||||
album_type,
|
||||
year,
|
||||
by_va,
|
||||
tracks: mapper.items,
|
||||
},
|
||||
warnings: mapper.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader, path::Path};
|
||||
|
|
@ -163,7 +295,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::param::Language;
|
||||
use crate::{model, param::Language};
|
||||
|
||||
#[rstest]
|
||||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
||||
|
|
@ -176,7 +308,8 @@ mod tests {
|
|||
|
||||
let playlist: response::MusicPlaylist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res = playlist.map_response(id, Language::En, None).unwrap();
|
||||
let map_res: MapResult<model::MusicPlaylist> =
|
||||
playlist.map_response(id, Language::En, None).unwrap();
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
|
|
@ -204,4 +337,28 @@ mod tests {
|
|||
);
|
||||
insta::assert_ron_snapshot!("map_music_playlist_cont", map_res.c);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::one_artist("one_artist", "MPREb_nlBWQROfvjo")]
|
||||
#[case::various_artists("various_artists", "MPREb_8QkDeEIawvX")]
|
||||
#[case::single("single", "MPREb_bHfHGoy7vuv")]
|
||||
fn map_music_album(#[case] name: &str, #[case] id: &str) {
|
||||
let filename = format!("testfiles/music_playlist/album_{}.json", name);
|
||||
let json_path = Path::new(&filename);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let playlist: response::MusicPlaylist =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<model::MusicAlbum> =
|
||||
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_album_{}", name), map_res.c, {
|
||||
".last_update" => "[date]"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub(crate) struct MusicItem {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct InnerMusicItem {
|
||||
#[serde(default)]
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
|
|
@ -79,7 +80,7 @@ impl From<MusicThumbnailRenderer> for Vec<model::Thumbnail> {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MusicListMapper<T> {
|
||||
artists: Option<Vec<ChannelId>>,
|
||||
artists: Option<(Vec<ChannelId>, String)>,
|
||||
|
||||
pub items: Vec<T>,
|
||||
pub warnings: Vec<String>,
|
||||
|
|
@ -94,9 +95,9 @@ impl<T> MusicListMapper<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_artists(artists: Vec<ChannelId>) -> Self {
|
||||
pub fn with_artists(artists: Vec<ChannelId>, artists_txt: String) -> Self {
|
||||
Self {
|
||||
artists: Some(artists),
|
||||
artists: Some((artists, artists_txt)),
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
|
|
@ -140,9 +141,9 @@ impl<T> MusicListMapper<T> {
|
|||
});
|
||||
|
||||
let artists_col = columns.try_swap_remove(1);
|
||||
let artists_txt = artists_col
|
||||
let mut artists_txt = artists_col
|
||||
.as_ref()
|
||||
.map(|col| col.renderer.text.to_string());
|
||||
.and_then(|col| col.renderer.text.to_opt_string());
|
||||
let mut artists = artists_col
|
||||
.map(|col| {
|
||||
col.renderer
|
||||
|
|
@ -154,8 +155,10 @@ impl<T> MusicListMapper<T> {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
if let Some(a) = &self.artists {
|
||||
if artists.is_empty() {
|
||||
artists = a.clone();
|
||||
if artists.is_empty() && artists_txt.is_none() {
|
||||
let xa = a.clone();
|
||||
artists = xa.0;
|
||||
artists_txt = Some(xa.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use serde_with::VecSkipError;
|
||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::serializer::{
|
||||
ignore_any,
|
||||
text::{Text, TextComponents},
|
||||
MapResult, VecLogError,
|
||||
};
|
||||
|
|
@ -10,6 +10,7 @@ use crate::serializer::{
|
|||
use super::music_item::{MusicContentsRenderer, MusicItem, MusicThumbnailRenderer};
|
||||
use super::{ContentRenderer, ContentsRenderer, MusicContinuation};
|
||||
|
||||
/// Response model for YouTube Music playlists and albums
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicPlaylist {
|
||||
|
|
@ -42,11 +43,18 @@ pub(crate) struct SectionList {
|
|||
pub section_list_renderer: MusicContentsRenderer<ItemSection>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ItemSection {
|
||||
pub(crate) enum ItemSection {
|
||||
#[serde(alias = "musicPlaylistShelfRenderer")]
|
||||
pub music_shelf_renderer: MusicShelf,
|
||||
MusicShelfRenderer(MusicShelf),
|
||||
MusicCarouselShelfRenderer {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
contents: MapResult<Vec<MusicItem>>,
|
||||
},
|
||||
#[serde(other, deserialize_with = "ignore_any")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -98,6 +106,48 @@ pub(crate) struct HeaderRenderer {
|
|||
#[serde(default)]
|
||||
#[serde_as(as = "Text")]
|
||||
pub second_subtitle: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub menu: Option<HeaderMenu>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HeaderMenu {
|
||||
pub menu_renderer: HeaderMenuRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HeaderMenuRenderer {
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub top_level_buttons: Vec<TopLevelButton>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct TopLevelButton {
|
||||
pub button_renderer: ButtonRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ButtonRenderer {
|
||||
pub navigation_endpoint: PlaylistEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlaylistEndpoint {
|
||||
pub watch_playlist_endpoint: PlaylistWatchEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlaylistWatchEndpoint {
|
||||
pub playlist_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
---
|
||||
source: src/client/music_playlist.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
MusicAlbum(
|
||||
id: "MPREb_nlBWQROfvjo",
|
||||
playlist_id: Some("OLAK5uy_myZkBX2d2TzcrlQhIwLy3hCj2MkAMaPR4"),
|
||||
name: "Märchen enden gut",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Z5CF2JCRD5o7fBywh9Spg_Wvmrqkg0M01FWsSm_mdmUSfplv--9NgIiBRExudt7s0TTd3tgpJ7CLRFal=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Z5CF2JCRD5o7fBywh9Spg_Wvmrqkg0M01FWsSm_mdmUSfplv--9NgIiBRExudt7s0TTd3tgpJ7CLRFal=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Z5CF2JCRD5o7fBywh9Spg_Wvmrqkg0M01FWsSm_mdmUSfplv--9NgIiBRExudt7s0TTd3tgpJ7CLRFal=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Z5CF2JCRD5o7fBywh9Spg_Wvmrqkg0M01FWsSm_mdmUSfplv--9NgIiBRExudt7s0TTd3tgpJ7CLRFal=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: "Oonagh",
|
||||
album_type: Album,
|
||||
year: 2016,
|
||||
by_va: false,
|
||||
tracks: [
|
||||
TrackItem(
|
||||
id: "g0iRiJ_ck48",
|
||||
title: "Aulë und Yavanna",
|
||||
duration: 216,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "rREEBXp0y9s",
|
||||
title: "Numenor",
|
||||
duration: 224,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "zvU5Y8Q19hU",
|
||||
title: "Das Mädchen und die Liebe (feat. Santiano)",
|
||||
duration: 176,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "ARKLrzzTQA0",
|
||||
title: "Niënna",
|
||||
duration: 215,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "tstLgN8A_Ng",
|
||||
title: "Der fahle Mond",
|
||||
duration: 268,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "k2DjgQOY3Ts",
|
||||
title: "Weise den Weg",
|
||||
duration: 202,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "azHwhecxEsI",
|
||||
title: "Zeit der Sommernächte",
|
||||
duration: 185,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "_FcsdYIQ2co",
|
||||
title: "Märchen enden gut",
|
||||
duration: 226,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "27bOWEbshyE",
|
||||
title: "Das Mädchen und der Tod",
|
||||
duration: 207,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "riD_3oZwt8w",
|
||||
title: "Wir sehn uns wieder",
|
||||
duration: 211,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "8GNvjF3no9s",
|
||||
title: "Tanz mit mir",
|
||||
duration: 179,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "YHMFzf1uN2U",
|
||||
title: "Nachtigall",
|
||||
duration: 218,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "jvV-z5F3oAo",
|
||||
title: "Gayatri Mantra",
|
||||
duration: 277,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "u8_9cxlrh8k",
|
||||
title: "Sing mir deine Lieder",
|
||||
duration: 204,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "gSvKcvM1Wk0",
|
||||
title: "Laurië lantar",
|
||||
duration: 202,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "wQHgKRJ0pDQ",
|
||||
title: "Wächter vor dem Tor",
|
||||
duration: 222,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "Ckz5i6-hzf0",
|
||||
title: "Stroh zu Gold",
|
||||
duration: 177,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "y5zuUgyFqrc",
|
||||
title: "Sonnenwendnacht",
|
||||
duration: 220,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UC_vmjW5e1xEHhYjY2a0kK1A",
|
||||
name: "Oonagh",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Oonagh"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
source: src/client/music_playlist.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
MusicAlbum(
|
||||
id: "MPREb_bHfHGoy7vuv",
|
||||
playlist_id: Some("OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0"),
|
||||
name: "Der Himmel reißt auf",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/sfYeIuiLljpCsDLSooCOkNON1jZwHsEui3fD1FnLSyCMYjLCPQtEgy4_6qBmSGOz3eNWyS-aW4WcZMo8=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UCXGYZ-OhdOpPBamHX3K9YRg",
|
||||
name: "Joel Brandenstein",
|
||||
),
|
||||
ChannelId(
|
||||
id: "UCFTcSVPYRWlDoHisR-ZKwgw",
|
||||
name: "Vanessa Mai",
|
||||
),
|
||||
],
|
||||
artists_txt: "Joel Brandenstein & Vanessa Mai",
|
||||
album_type: Single,
|
||||
year: 2020,
|
||||
by_va: false,
|
||||
tracks: [
|
||||
TrackItem(
|
||||
id: "XX0epju-YvY",
|
||||
title: "Der Himmel reißt auf",
|
||||
duration: 183,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UCXGYZ-OhdOpPBamHX3K9YRg",
|
||||
name: "Joel Brandenstein",
|
||||
),
|
||||
ChannelId(
|
||||
id: "UCFTcSVPYRWlDoHisR-ZKwgw",
|
||||
name: "Vanessa Mai",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Joel Brandenstein & Vanessa Mai"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
---
|
||||
source: src/client/music_playlist.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
MusicAlbum(
|
||||
id: "MPREb_8QkDeEIawvX",
|
||||
playlist_id: Some("OLAK5uy_mEX9ljZeeEWgTM1xLL1isyiGaWXoPyoOk"),
|
||||
name: "<Queendom2> FINAL",
|
||||
cover: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Imv7uGEOmI-jpyxbRv1Yk9sajaZMxzK2zs3bQuu9W9FyXmiVrPEZ8F7NsY-DCxDwDGIzBNDRGossSi2KVA=w60-h60-l90-rj",
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Imv7uGEOmI-jpyxbRv1Yk9sajaZMxzK2zs3bQuu9W9FyXmiVrPEZ8F7NsY-DCxDwDGIzBNDRGossSi2KVA=w120-h120-l90-rj",
|
||||
width: 120,
|
||||
height: 120,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Imv7uGEOmI-jpyxbRv1Yk9sajaZMxzK2zs3bQuu9W9FyXmiVrPEZ8F7NsY-DCxDwDGIzBNDRGossSi2KVA=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Imv7uGEOmI-jpyxbRv1Yk9sajaZMxzK2zs3bQuu9W9FyXmiVrPEZ8F7NsY-DCxDwDGIzBNDRGossSi2KVA=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
artists: [],
|
||||
artists_txt: "Various Artists",
|
||||
album_type: Single,
|
||||
year: 2022,
|
||||
by_va: true,
|
||||
tracks: [
|
||||
TrackItem(
|
||||
id: "8IqLxg0GqXc",
|
||||
title: "Waka Boom (My Way) (feat. Lee Young Ji)",
|
||||
duration: 274,
|
||||
cover: [],
|
||||
artists: [],
|
||||
artists_txt: Some("HYOLYN"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "9WYpLYAEub0",
|
||||
title: "AURA",
|
||||
duration: 216,
|
||||
cover: [],
|
||||
artists: [],
|
||||
artists_txt: Some("WJSN"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "R48tE237bW4",
|
||||
title: "THE GIRLS (Can’t turn me down)",
|
||||
duration: 239,
|
||||
cover: [],
|
||||
artists: [
|
||||
ChannelId(
|
||||
id: "UCAKvDuIX3m1AUdPpDSqV_3w",
|
||||
name: "Kep1er",
|
||||
),
|
||||
],
|
||||
artists_txt: Some("Kep1er"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "-UzsoR6z-vg",
|
||||
title: "Red Sun!",
|
||||
duration: 254,
|
||||
cover: [],
|
||||
artists: [],
|
||||
artists_txt: Some("VIVIZ"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "kbNVyn8Ex28",
|
||||
title: "POSE",
|
||||
duration: 187,
|
||||
cover: [],
|
||||
artists: [],
|
||||
artists_txt: Some("LOONA"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
TrackItem(
|
||||
id: "NJrQZUzWP5Y",
|
||||
title: "Whistle",
|
||||
duration: 224,
|
||||
cover: [],
|
||||
artists: [],
|
||||
artists_txt: Some("Brave Girls"),
|
||||
album: None,
|
||||
view_count: None,
|
||||
is_video: true,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -1008,10 +1008,17 @@ pub struct MusicAlbum {
|
|||
pub cover: Vec<Thumbnail>,
|
||||
/// Artists of the album
|
||||
pub artists: Vec<ChannelId>,
|
||||
/// Full content of the artists column
|
||||
///
|
||||
/// Conjunction words/characters depend on language and fetched page.
|
||||
/// Includes unlinked artists.
|
||||
pub artists_txt: String,
|
||||
/// Music album type
|
||||
pub album_type: AlbumType,
|
||||
/// Release year
|
||||
pub year: u16,
|
||||
/// Is the album by 'Various artists'?
|
||||
pub by_va: bool,
|
||||
/// Album tracks
|
||||
pub tracks: Vec<TrackItem>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,6 +373,16 @@ impl TextComponent {
|
|||
}
|
||||
}
|
||||
|
||||
impl TextComponents {
|
||||
pub fn to_opt_string(&self) -> Option<String> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for TextComponents {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.iter().map(|x| x.as_str()).collect::<String>()
|
||||
|
|
|
|||
9720
testfiles/music_playlist/album_one_artist.json
Normal file
9720
testfiles/music_playlist/album_one_artist.json
Normal file
File diff suppressed because it is too large
Load diff
1073
testfiles/music_playlist/album_single.json
Normal file
1073
testfiles/music_playlist/album_single.json
Normal file
File diff suppressed because it is too large
Load diff
3341
testfiles/music_playlist/album_various_artists.json
Normal file
3341
testfiles/music_playlist/album_various_artists.json
Normal file
File diff suppressed because it is too large
Load diff
Reference in a new issue