fix: add support for A/B-13 (2-column layout for music playlists/albums)
This commit is contained in:
parent
bd04a87ad5
commit
76c27f0324
14 changed files with 63535 additions and 89 deletions
|
|
@ -1,6 +1,7 @@
|
|||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
use crate::{
|
||||
client::response::url_endpoint::NavigationEndpoint,
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
|
|
@ -143,16 +144,35 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let music_contents = 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;
|
||||
let (header, music_contents) = match self.contents {
|
||||
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
|
||||
self.header,
|
||||
c.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer,
|
||||
),
|
||||
response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
|
||||
secondary_contents,
|
||||
tabs,
|
||||
} => (
|
||||
tabs.into_iter()
|
||||
.next()
|
||||
.and_then(|t| {
|
||||
t.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
})
|
||||
.or(self.header),
|
||||
secondary_contents.section_list_renderer,
|
||||
),
|
||||
};
|
||||
let shelf = music_contents
|
||||
.contents
|
||||
.into_iter()
|
||||
|
|
@ -183,7 +203,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
.map(|cont| cont.next_continuation_data.continuation);
|
||||
|
||||
let track_count = if ctoken.is_some() {
|
||||
self.header.as_ref().and_then(|h| {
|
||||
header.as_ref().and_then(|h| {
|
||||
let parts = h
|
||||
.music_detail_header_renderer
|
||||
.second_subtitle
|
||||
|
|
@ -203,23 +223,24 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
.next()
|
||||
.map(|c| c.next_continuation_data.continuation);
|
||||
|
||||
let (from_ytm, channel, name, thumbnail, description) = match self.header {
|
||||
let (from_ytm, channel, name, thumbnail, description) = match header {
|
||||
Some(header) => {
|
||||
let h = header.music_detail_header_renderer;
|
||||
|
||||
let from_ytm = h.subtitle.0.iter().any(util::is_ytm);
|
||||
let channel = h
|
||||
.subtitle
|
||||
.0
|
||||
.into_iter()
|
||||
.find_map(|c| ChannelId::try_from(c).ok());
|
||||
let st = match h.strapline_text_one {
|
||||
Some(s) => s,
|
||||
None => h.subtitle,
|
||||
};
|
||||
|
||||
let from_ytm = st.0.iter().any(util::is_ytm);
|
||||
let channel = st.0.into_iter().find_map(|c| ChannelId::try_from(c).ok());
|
||||
|
||||
(
|
||||
from_ytm,
|
||||
channel,
|
||||
h.title,
|
||||
h.thumbnail.into(),
|
||||
h.description,
|
||||
h.description.map(String::from),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
|
|
@ -288,23 +309,40 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||
// dbg!(&self);
|
||||
|
||||
let header = self
|
||||
.header
|
||||
let (header, sections) = match self.contents {
|
||||
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => (
|
||||
self.header,
|
||||
c.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents,
|
||||
),
|
||||
response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
|
||||
secondary_contents,
|
||||
tabs,
|
||||
} => (
|
||||
tabs.into_iter()
|
||||
.next()
|
||||
.and_then(|t| {
|
||||
t.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.into_iter()
|
||||
.next()
|
||||
})
|
||||
.or(self.header),
|
||||
secondary_contents.section_list_renderer.contents,
|
||||
),
|
||||
};
|
||||
let header = header
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no header")))?
|
||||
.music_detail_header_renderer;
|
||||
|
||||
let sections = 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 shelf = None;
|
||||
let mut album_variants = None;
|
||||
for section in sections {
|
||||
|
|
@ -322,27 +360,37 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
|
||||
let mut subtitle_split = header.subtitle.split(util::DOT_SEPARATOR);
|
||||
|
||||
let (year_txt, artists_p) = match subtitle_split.len() {
|
||||
3.. => {
|
||||
let (year_txt, artists_p) = match header.strapline_text_one {
|
||||
Some(sl) => {
|
||||
let year_txt = subtitle_split
|
||||
.swap_remove(2)
|
||||
.swap_remove(1)
|
||||
.0
|
||||
.first()
|
||||
.map(|c| c.as_str().to_owned());
|
||||
(year_txt, subtitle_split.try_swap_remove(1))
|
||||
(year_txt, Some(sl))
|
||||
}
|
||||
2 => {
|
||||
// The second part may either be the year or the artist
|
||||
let p2 = subtitle_split.swap_remove(1);
|
||||
let is_year =
|
||||
p2.0.len() == 1 && p2.0[0].as_str().chars().all(|c| c.is_ascii_digit());
|
||||
if is_year {
|
||||
(Some(p2.0[0].as_str().to_owned()), None)
|
||||
} else {
|
||||
(None, Some(p2))
|
||||
None => match subtitle_split.len() {
|
||||
3.. => {
|
||||
let year_txt = subtitle_split
|
||||
.swap_remove(2)
|
||||
.0
|
||||
.first()
|
||||
.map(|c| c.as_str().to_owned());
|
||||
(year_txt, subtitle_split.try_swap_remove(1))
|
||||
}
|
||||
}
|
||||
_ => (None, None),
|
||||
2 => {
|
||||
// The second part may either be the year or the artist
|
||||
let p2 = subtitle_split.swap_remove(1);
|
||||
let is_year =
|
||||
p2.0.len() == 1 && p2.0[0].as_str().chars().all(|c| c.is_ascii_digit());
|
||||
if is_year {
|
||||
(Some(p2.0[0].as_str().to_owned()), None)
|
||||
} else {
|
||||
(None, Some(p2))
|
||||
}
|
||||
}
|
||||
_ => (None, None),
|
||||
},
|
||||
};
|
||||
|
||||
let (artists, by_va) = map_artists(artists_p);
|
||||
|
|
@ -355,21 +403,34 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
let album_type = map_album_type(album_type_txt.as_str(), lang);
|
||||
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
|
||||
|
||||
let (artist_id, playlist_id) = header
|
||||
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
|
||||
if let NavigationEndpoint::WatchPlaylist {
|
||||
watch_playlist_endpoint,
|
||||
} = ep
|
||||
{
|
||||
Some(watch_playlist_endpoint.playlist_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
let (playlist_id, artist_id) = header
|
||||
.menu
|
||||
.or_else(|| header.buttons.into_iter().next())
|
||||
.map(|menu| {
|
||||
(
|
||||
map_artist_id(menu.menu_renderer.items),
|
||||
menu.menu_renderer
|
||||
.top_level_buttons
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|btn| {
|
||||
btn.button_renderer
|
||||
.navigation_endpoint
|
||||
.watch_playlist_endpoint
|
||||
.playlist_id
|
||||
.iter()
|
||||
.find_map(|btn| map_playlist_id(&btn.button_renderer.navigation_endpoint))
|
||||
.or_else(|| {
|
||||
menu.menu_renderer.items.iter().find_map(|itm| {
|
||||
map_playlist_id(
|
||||
&itm.menu_navigation_item_renderer.navigation_endpoint,
|
||||
)
|
||||
})
|
||||
}),
|
||||
map_artist_id(menu.menu_renderer.items),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
|
@ -403,7 +464,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
|||
cover: header.thumbnail.into(),
|
||||
artists,
|
||||
artist_id,
|
||||
description: header.description,
|
||||
description: header.description.map(String::from),
|
||||
album_type,
|
||||
year,
|
||||
by_va,
|
||||
|
|
@ -429,6 +490,8 @@ mod tests {
|
|||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
||||
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
|
||||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||
#[case::two_columns("20240228_twoColumns", "RDCLAK5uy_kb7EBi6y3GrtJri4_ZH56Ms786DFEimbM")]
|
||||
#[case::n_album("20240228_album", "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0")]
|
||||
fn map_music_playlist(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "music_playlist" / format!("playlist_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
@ -454,6 +517,8 @@ mod tests {
|
|||
#[case::single("single", "MPREb_bHfHGoy7vuv")]
|
||||
#[case::description("description", "MPREb_PiyfuVl6aYd")]
|
||||
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
||||
#[case::unavailable("unavailable", "MPREb_AzuWg8qAVVl")]
|
||||
#[case::two_columns("20240228_twoColumns", "MPREb_bHfHGoy7vuv")]
|
||||
fn map_music_album(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "music_playlist" / format!("album_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ pub(crate) struct ContentRenderer<T> {
|
|||
pub content: T,
|
||||
}
|
||||
|
||||
/// Deserializes any object with an array field named `contents`, `tabs` or `items`.
|
||||
///
|
||||
/// Invalid items are skipped
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ContentsRenderer<T> {
|
||||
pub contents: Vec<T>,
|
||||
|
|
|
|||
|
|
@ -5,23 +5,37 @@ use crate::serializer::text::{Text, TextComponents};
|
|||
|
||||
use super::{
|
||||
music_item::{
|
||||
ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
|
||||
SingleColumnBrowseResult,
|
||||
Button, ItemSection, MusicContentsRenderer, MusicItemMenuEntry, MusicThumbnailRenderer,
|
||||
},
|
||||
Tab,
|
||||
ContentsRenderer, SectionList, Tab,
|
||||
};
|
||||
|
||||
/// Response model for YouTube Music playlists and albums
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicPlaylist {
|
||||
pub contents: SingleColumnBrowseResult<Tab<SectionList>>,
|
||||
pub contents: Contents,
|
||||
pub header: Option<Header>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum Contents {
|
||||
SingleColumnBrowseResultsRenderer(ContentsRenderer<Tab<PlSectionList>>),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
TwoColumnBrowseResultsRenderer {
|
||||
/// List content
|
||||
secondary_contents: PlSectionList,
|
||||
/// Header
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
tabs: Vec<Tab<SectionList<Header>>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SectionList {
|
||||
pub(crate) struct PlSectionList {
|
||||
/// Includes a continuation token for fetching recommendations
|
||||
pub section_list_renderer: MusicContentsRenderer<ItemSection>,
|
||||
}
|
||||
|
|
@ -29,6 +43,7 @@ pub(crate) struct SectionList {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Header {
|
||||
#[serde(alias = "musicResponsiveHeaderRenderer")]
|
||||
pub music_detail_header_renderer: HeaderRenderer,
|
||||
}
|
||||
|
||||
|
|
@ -48,12 +63,13 @@ pub(crate) struct HeaderRenderer {
|
|||
pub subtitle: TextComponents,
|
||||
/// Playlist/album description. May contain hashtags which are
|
||||
/// displayed as search links on the YouTube website.
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub description: Option<String>,
|
||||
pub description: Option<Description>,
|
||||
/// Playlist thumbnail / album cover.
|
||||
/// Missing on artist_tracks view.
|
||||
#[serde(default)]
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
/// Channel (only on TwoColumnBrowseResultsRenderer)
|
||||
pub strapline_text_one: Option<TextComponents>,
|
||||
/// Number of tracks + playtime.
|
||||
/// Missing on artist_tracks view.
|
||||
///
|
||||
|
|
@ -66,6 +82,28 @@ pub(crate) struct HeaderRenderer {
|
|||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub menu: Option<HeaderMenu>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub buttons: Vec<HeaderMenu>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Description {
|
||||
Text(#[serde_as(as = "Text")] String),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Shelf {
|
||||
music_description_shelf_renderer: DescriptionShelf,
|
||||
},
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DescriptionShelf {
|
||||
#[serde_as(as = "Text")]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -80,31 +118,18 @@ pub(crate) struct HeaderMenu {
|
|||
pub(crate) struct HeaderMenuRenderer {
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub top_level_buttons: Vec<TopLevelButton>,
|
||||
pub top_level_buttons: Vec<Button>,
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub items: Vec<MusicItemMenuEntry>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
impl From<Description> for String {
|
||||
fn from(value: Description) -> Self {
|
||||
match value {
|
||||
Description::Text(v) => v,
|
||||
Description::Shelf {
|
||||
music_description_shelf_renderer,
|
||||
} => music_description_shelf_renderer.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ pub(crate) enum NavigationEndpoint {
|
|||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Url { url_endpoint: UrlEndpoint },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WatchPlaylist {
|
||||
watch_playlist_endpoint: WatchPlaylistEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -54,6 +58,12 @@ pub(crate) struct BrowseEndpointWrap {
|
|||
pub browse_endpoint: BrowseEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct WatchPlaylistEndpoint {
|
||||
pub playlist_id: String,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BrowseEndpoint {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
|
@ -294,6 +304,12 @@ impl NavigationEndpoint {
|
|||
)
|
||||
}),
|
||||
NavigationEndpoint::Url { .. } => None,
|
||||
NavigationEndpoint::WatchPlaylist {
|
||||
watch_playlist_endpoint,
|
||||
} => Some(MusicPage {
|
||||
id: watch_playlist_endpoint.playlist_id,
|
||||
typ: MusicPageType::Playlist,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
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: [
|
||||
ArtistId(
|
||||
id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||
name: "Joel Brandenstein",
|
||||
),
|
||||
ArtistId(
|
||||
id: Some("UCFTcSVPYRWlDoHisR-ZKwgw"),
|
||||
name: "Vanessa Mai",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||
description: None,
|
||||
album_type: Single,
|
||||
year: Some(2020),
|
||||
by_va: false,
|
||||
tracks: [
|
||||
TrackItem(
|
||||
id: "XX0epju-YvY",
|
||||
name: "Der Himmel reißt auf",
|
||||
duration: Some(183),
|
||||
cover: [],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||
name: "Joel Brandenstein",
|
||||
),
|
||||
ArtistId(
|
||||
id: Some("UCFTcSVPYRWlDoHisR-ZKwgw"),
|
||||
name: "Vanessa Mai",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_bHfHGoy7vuv",
|
||||
name: "Der Himmel reißt auf",
|
||||
)),
|
||||
view_count: Some(12000000),
|
||||
is_video: true,
|
||||
track_nr: Some(1),
|
||||
by_va: false,
|
||||
),
|
||||
],
|
||||
variants: [],
|
||||
)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
source: src/client/music_playlist.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
MusicPlaylist(
|
||||
id: "OLAK5uy_kdSWBZ-9AZDkYkuy0QCc3p0KO9DEHVNH0",
|
||||
name: "Der Himmel reißt auf",
|
||||
thumbnail: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
description: None,
|
||||
track_count: Some(1),
|
||||
from_ytm: true,
|
||||
tracks: Paginator(
|
||||
count: Some(1),
|
||||
items: [
|
||||
TrackItem(
|
||||
id: "VU6lEv0PKAo",
|
||||
name: "Der Himmel reißt auf",
|
||||
duration: Some(183),
|
||||
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,
|
||||
),
|
||||
],
|
||||
artists: [
|
||||
ArtistId(
|
||||
id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||
name: "Joel Brandenstein",
|
||||
),
|
||||
ArtistId(
|
||||
id: Some("UCFTcSVPYRWlDoHisR-ZKwgw"),
|
||||
name: "Vanessa Mai",
|
||||
),
|
||||
],
|
||||
artist_id: Some("UCXGYZ-OhdOpPBamHX3K9YRg"),
|
||||
album: Some(AlbumId(
|
||||
id: "MPREb_bHfHGoy7vuv",
|
||||
name: "Der Himmel reißt auf",
|
||||
)),
|
||||
view_count: None,
|
||||
is_video: false,
|
||||
track_nr: None,
|
||||
by_va: false,
|
||||
),
|
||||
],
|
||||
ctoken: None,
|
||||
endpoint: music_browse,
|
||||
),
|
||||
related_playlists: Paginator(
|
||||
count: Some(0),
|
||||
items: [],
|
||||
ctoken: None,
|
||||
endpoint: music_browse,
|
||||
),
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -195,6 +195,13 @@ fn map_text_component(text: String, nav: Option<NavigationEndpoint>) -> TextComp
|
|||
text,
|
||||
url: url_endpoint.url,
|
||||
},
|
||||
Some(NavigationEndpoint::WatchPlaylist {
|
||||
watch_playlist_endpoint,
|
||||
}) => TextComponent::Browse {
|
||||
text,
|
||||
page_type: PageType::Playlist,
|
||||
browse_id: watch_playlist_endpoint.playlist_id,
|
||||
},
|
||||
None => TextComponent::Text { text },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue