fix: add support for A/B-13 (2-column layout for music playlists/albums)

This commit is contained in:
ThetaDev 2024-02-29 02:54:40 +01:00
parent bd04a87ad5
commit 76c27f0324
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
14 changed files with 63535 additions and 89 deletions

View file

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

View file

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

View file

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