use serde::Deserialize; use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError}; use crate::{ model::{ self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId, MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem, }, param::Language, serializer::{ text::{Text, TextComponent, TextComponents}, MapResult, }, util::{self, dictionary, timeago}, }; use super::{ url_endpoint::{ BrowseEndpointWrap, MusicPage, MusicPageType, MusicVideoType, NavigationEndpoint, PageType, }, ContentsRenderer, ContinuationActionWrap, ContinuationEndpoint, MusicContinuationData, SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap, }; #[cfg(feature = "userdata")] use crate::model::HistoryItem; #[cfg(feature = "userdata")] use time::UtcOffset; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) enum ItemSection { #[serde(alias = "musicPlaylistShelfRenderer")] MusicShelfRenderer(MusicShelf), MusicCarouselShelfRenderer(MusicCarouselShelf), GridRenderer(GridRenderer), #[serde(other, deserialize_with = "deserialize_ignore_any")] None, } /// MusicShelf represents the standard, vertical list of music items /// (used in search results, playlist, album). #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicShelf { #[cfg(feature = "userdata")] #[serde_as(as = "Option")] pub title: Option, /// Playlist ID (only for playlists) pub playlist_id: Option, pub contents: MapResult>, /// Continuation token for fetching more (>100) playlist items #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub continuations: Vec, /// "More" button at the bottom (artist pages) #[serde(default)] #[serde_as(as = "DefaultOnError")] pub bottom_endpoint: Option, } /// MusicCarouselShelf represents a horizontal list of music items displayed with /// large covers. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicCarouselShelf { pub header: Option, pub contents: MapResult>, } /// MusicCardShelf is used to display the top search result. It contains /// one main item and optionally a list of sub-items (like an artist + top tracks). #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicCardShelf { #[serde_as(as = "Text")] pub title: String, pub on_tap: NavigationEndpoint, #[serde(default)] pub subtitle: TextComponents, #[serde(default)] pub thumbnail: MusicThumbnailRenderer, #[serde(default)] pub contents: MapResult>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] #[allow(clippy::enum_variant_names)] pub(crate) enum MusicResponseItem { MusicResponsiveListItemRenderer(ListMusicItem), MusicTwoRowItemRenderer(CoverMusicItem), MessageRenderer(serde::de::IgnoredAny), #[serde(rename_all = "camelCase")] ContinuationItemRenderer { continuation_endpoint: ContinuationEndpoint, }, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ListMusicItem { #[serde(default)] pub thumbnail: MusicThumbnailRenderer, #[serde(default)] #[serde_as(deserialize_as = "DefaultOnError")] pub playlist_item_data: Option, /// ### Playlist track /// /// `[<"Das Beste">], [<"Silbermond">], [<"Laut Gedacht (Re-Edition)">]` /// /// (title, artist, album) /// /// ### Album track /// /// `[<"Der Himmel reißt auf">]` /// /// (title) /// /// ### Search track /// /// `[<"Girls">], ["Song", " • ", <"aespa">, " • ", <"Girls - The 2nd Mini Album">, " • ", "4:01"]` /// /// (title, artist, album, duration) /// /// Info: "Song" label is missing in the "Songs" tab /// /// ### Search video /// /// `[<"Black Mamba">], ["Video", " • ", <"aespa">, " • ", "235M views", " • ", "3:50"]` /// /// (title, artist, view count, duration) /// /// Info: "Video" label is missing in the "Videos" tab /// /// ### Search podcast episode /// /// `["Blond - Da muss man dabei..."], ["Episode", " • ", "Dec 24, 2020", " • ", <"BLOND_OFFICIAL">], ["Dec 24, 2020"]` /// /// (title, date, artist, date again?) /// /// Info: "Episode" label is missing in the "Videos" tab /// /// ### Search album /// /// `["Next Level"], ["Single", " • ", <"aespa">, " • ", "2021"]` /// /// (title, type, artist, year) /// /// ### Search artist /// /// `["Test Shot Starfish"], ["Artist", " • ", "1660 subscribers"]` /// /// (subscriber count) /// /// ### Search playlist /// /// `["aespa - All Songs & MV"], ["Playlist", " • ", <"Jerwen">, " • ", "49 songs"]` /// /// (title, creator, track count) /// /// Info: "Playlist" label is missing in the "Playlists" tab pub flex_columns: Vec, /// Track duration (playlist/album tracks) /// /// `"3:32"` #[serde(default)] pub fixed_columns: Vec, /// Content type + ID (for non-track search items) pub navigation_endpoint: Option, #[serde(default)] pub flex_column_display_style: FlexColumnDisplayStyle, #[serde(default)] pub item_height: ItemHeight, #[serde(default)] pub music_item_renderer_display_policy: DisplayPolicy, /// Album track number #[serde_as(as = "Option")] pub index: Option, pub menu: Option, #[serde(default)] #[serde_as(deserialize_as = "VecSkipError<_>")] pub badges: Vec, } #[derive(Default, Debug, Copy, Clone, Deserialize)] pub(crate) enum FlexColumnDisplayStyle { #[serde(rename = "MUSIC_RESPONSIVE_LIST_ITEM_FLEX_COLUMN_DISPLAY_STYLE_TWO_LINE_STACK")] TwoLines, #[default] #[serde(other)] Default, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Deserialize)] pub(crate) enum ItemHeight { #[serde(rename = "MUSIC_RESPONSIVE_LIST_ITEM_HEIGHT_MEDIUM_COMPACT")] Compact, #[default] #[serde(other)] Default, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Deserialize)] pub(crate) enum DisplayPolicy { #[serde(rename = "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT")] GreyOut, #[default] #[serde(other)] Default, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct CoverMusicItem { #[serde_as(as = "Text")] pub title: String, /// Content type + Channel/Artist /// /// `"Album", " • ", <"Oonagh">` Album variants, new releases /// /// `"Album", " • ", "2022"` Artist albums /// /// `"2022"` Artist singles /// /// `"Playlist", " • ", <"YouTube Music"> " • ", "53 songs"` /// /// `"Playlist", " • ", <"Vevo Playlists"> " • ", "13M views"` /// /// `"Playlist", " • ", "YouTube Music" Featured on #[serde(default)] pub subtitle: TextComponents, #[serde(default)] pub thumbnail_renderer: MusicThumbnailRenderer, /// Content type + ID pub navigation_endpoint: NavigationEndpoint, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PlaylistPanelRenderer { pub contents: MapResult>, /// Continuation token for fetching more radio items #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub continuations: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) enum PlaylistPanelVideo { PlaylistPanelVideoRenderer(QueueMusicItem), #[serde(other, deserialize_with = "deserialize_ignore_any")] None, } /// Music item from a playback queue (`playlistPanelVideoRenderer`) #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct QueueMusicItem { pub video_id: String, #[serde_as(as = "Text")] pub title: String, #[serde_as(as = "Option")] pub length_text: Option, /// Artist + Album + Year (for tracks) /// `<"IVE">, " • ", <"LOVE DIVE (LOVE DIVE)">, " • ", "2022"` /// /// Artist + view count + like count (for videos) /// `<"aespa">, " • ", "250M views", " • ", "3.6M likes"` #[serde(default)] pub long_byline_text: TextComponents, #[serde(default)] pub thumbnail: Thumbnails, pub menu: Option, } #[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicThumbnailRenderer { #[serde(default, alias = "croppedSquareThumbnailRenderer")] pub music_thumbnail_renderer: ThumbnailsWrap, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PlaylistItemData { pub video_id: String, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicContentsRenderer { pub contents: Vec, /// Continuation token for fetching recommended items #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub continuations: Vec, } #[derive(Debug, Deserialize)] pub(crate) struct MusicColumn { #[serde( rename = "musicResponsiveListItemFlexColumnRenderer", alias = "musicResponsiveListItemFixedColumnRenderer" )] pub renderer: MusicColumnRenderer, } #[serde_as] #[derive(Debug, Deserialize)] pub(crate) struct MusicColumnRenderer { pub text: TextComponents, } impl From for TextComponents { fn from(col: MusicColumn) -> Self { col.renderer.text } } impl From for Vec { fn from(tr: MusicThumbnailRenderer) -> Self { tr.music_thumbnail_renderer.thumbnail.into() } } /// Music list continuation response model #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicContinuation { pub continuation_contents: Option, #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub on_response_received_actions: Vec>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] #[allow(clippy::enum_variant_names)] pub(crate) enum ContinuationContents { #[serde(alias = "musicPlaylistShelfContinuation")] MusicShelfContinuation(MusicShelf), SectionListContinuation(ContentsRenderer), PlaylistPanelContinuation(PlaylistPanelRenderer), GridContinuation(GridRenderer), } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicCarouselShelfHeader { pub music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicCarouselShelfHeaderRenderer { pub more_content_button: Option