feat: add music album

This commit is contained in:
ThetaDev 2022-10-29 23:45:03 +02:00
parent 566b3e5bfc
commit 3b738a55ad
12 changed files with 14904 additions and 17 deletions

View file

@ -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();
}
}

View file

@ -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]"
});
}
}

View file

@ -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);
}
}

View file

@ -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)]

View file

@ -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,
),
],
)

View file

@ -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,
),
],
)

View file

@ -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 (Cant 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,
),
],
)

View file

@ -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>,
}

View file

@ -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>()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff