use serde::Deserialize; use serde_with::{serde_as, DefaultOnError}; use crate::{ model::{TrackType, UrlTarget}, util, }; use super::Empty; /// navigation/resolve_url response model #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ResolvedUrl { pub endpoint: NavigationEndpoint, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(untagged)] pub(crate) enum NavigationEndpoint { #[serde(rename_all = "camelCase")] Watch { #[serde(alias = "reelWatchEndpoint")] watch_endpoint: WatchEndpoint, }, #[serde(rename_all = "camelCase")] Browse { browse_endpoint: BrowseEndpoint, #[serde(default)] #[serde_as(deserialize_as = "DefaultOnError")] command_metadata: Option, }, #[serde(rename_all = "camelCase")] Url { url_endpoint: UrlEndpoint }, #[serde(rename_all = "camelCase")] WatchPlaylist { watch_playlist_endpoint: WatchPlaylistEndpoint, }, #[serde(rename_all = "camelCase")] #[allow(unused)] CreatePlaylist { create_playlist_endpoint: Empty }, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct WatchEndpoint { pub video_id: String, pub playlist_id: Option, #[serde(default)] pub start_time_seconds: u32, #[serde(default)] pub watch_endpoint_music_supported_configs: WatchEndpointConfigWrap, } #[derive(Debug)] pub(crate) struct BrowseEndpoint { pub browse_id: String, pub params: String, pub browse_endpoint_context_supported_configs: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] 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(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct BEp { pub browse_id: String, #[serde(default)] pub params: String, pub browse_endpoint_context_supported_configs: Option, } let bep = BEp::deserialize(deserializer)?; // Remove the VL prefix from the playlist id #[allow(clippy::map_unwrap_or)] let browse_id = bep .browse_endpoint_context_supported_configs .as_ref() .and_then( |cfg| match cfg.browse_endpoint_context_music_config.page_type { PageType::Playlist => bep.browse_id.strip_prefix("VL"), _ => None, }, ) .map(str::to_owned) .unwrap_or(bep.browse_id); Ok(Self { browse_id, params: bep.params, browse_endpoint_context_supported_configs: bep .browse_endpoint_context_supported_configs, }) } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct UrlEndpoint { pub url: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct BrowseEndpointConfig { pub browse_endpoint_context_music_config: BrowseEndpointMusicConfig, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct BrowseEndpointMusicConfig { #[serde(default)] #[serde_as(as = "DefaultOnError")] pub page_type: PageType, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct CommandMetadata { pub web_command_metadata: WebCommandMetadata, } #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct WebCommandMetadata { #[serde(default)] #[serde_as(as = "DefaultOnError")] pub web_page_type: PageType, } #[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct WatchEndpointConfigWrap { pub watch_endpoint_music_config: WatchEndpointConfig, } #[serde_as] #[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct WatchEndpointConfig { #[serde(default)] #[serde_as(deserialize_as = "DefaultOnError")] pub music_video_type: MusicVideoType, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct OnTap { pub innertube_command: NavigationEndpoint, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct OnTapWrap { pub on_tap: OnTap, } #[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)] pub(crate) enum MusicVideoType { #[default] #[serde(rename = "MUSIC_VIDEO_TYPE_OMV", alias = "MUSIC_VIDEO_TYPE_UGC")] Video, #[serde(rename = "MUSIC_VIDEO_TYPE_ATV")] Track, #[serde(rename = "MUSIC_VIDEO_TYPE_PODCAST_EPISODE")] Episode, } impl MusicVideoType { pub fn is_video(self) -> bool { self != Self::Track } pub fn from_is_video(is_video: bool) -> Self { if is_video { Self::Video } else { Self::Track } } } impl From for TrackType { fn from(value: MusicVideoType) -> Self { match value { MusicVideoType::Video => Self::Video, MusicVideoType::Track => Self::Track, MusicVideoType::Episode => Self::Episode, } } } #[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)] pub(crate) enum PageType { #[serde( rename = "MUSIC_PAGE_TYPE_ARTIST", alias = "MUSIC_PAGE_TYPE_AUDIOBOOK_ARTIST" )] Artist, #[serde(rename = "MUSIC_PAGE_TYPE_ALBUM", alias = "MUSIC_PAGE_TYPE_AUDIOBOOK")] Album, #[serde( rename = "WEB_PAGE_TYPE_CHANNEL", alias = "MUSIC_PAGE_TYPE_USER_CHANNEL" )] Channel, #[serde(rename = "MUSIC_PAGE_TYPE_PLAYLIST", alias = "WEB_PAGE_TYPE_PLAYLIST")] Playlist, #[serde(rename = "MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE")] Podcast, #[serde(rename = "MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE")] Episode, #[default] Unknown, } impl PageType { pub(crate) fn to_url_target(self, id: String) -> Option { match self { PageType::Artist | PageType::Channel => Some(UrlTarget::Channel { id }), PageType::Album => Some(UrlTarget::Album { id }), PageType::Playlist => Some(UrlTarget::Playlist { id }), PageType::Podcast => Some(UrlTarget::Playlist { id: util::strip_prefix(&id, util::PODCAST_PLAYLIST_PREFIX), }), PageType::Episode => Some(UrlTarget::Video { id: util::strip_prefix(&id, util::PODCAST_EPISODE_PREFIX), start_time: 0, }), PageType::Unknown => None, } } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) enum MusicPageType { Artist, Album, Playlist { is_podcast: bool }, Track { vtype: MusicVideoType }, User, None, } impl From for MusicPageType { fn from(t: PageType) -> Self { match t { PageType::Artist => MusicPageType::Artist, PageType::Album => MusicPageType::Album, PageType::Playlist => MusicPageType::Playlist { is_podcast: false }, PageType::Podcast => MusicPageType::Playlist { is_podcast: true }, PageType::Channel => MusicPageType::User, PageType::Episode => MusicPageType::Track { vtype: MusicVideoType::Episode, }, PageType::Unknown => MusicPageType::None, } } } pub(crate) struct MusicPage { pub id: String, pub typ: MusicPageType, } impl MusicPage { /// Create a new MusicPage object, applying the required ID fixes when /// mapping a browse link pub fn from_browse(mut id: String, typ: PageType) -> Self { if typ == PageType::Podcast { id = util::strip_prefix(&id, util::PODCAST_PLAYLIST_PREFIX); } else if typ == PageType::Episode && id.len() == 15 { id = util::strip_prefix(&id, util::PODCAST_EPISODE_PREFIX); } Self { id, typ: typ.into(), } } } impl NavigationEndpoint { /// Get the YouTube Music page and id from a browse/watch endpoint pub(crate) fn music_page(self) -> Option { match self { NavigationEndpoint::Watch { watch_endpoint } => { if watch_endpoint .playlist_id .map(|plid| plid.starts_with("RDQM")) .unwrap_or_default() { // Genre radios (e.g. "pop radio") will be skipped Some(MusicPage { id: watch_endpoint.video_id, typ: MusicPageType::None, }) } else { Some(MusicPage { id: watch_endpoint.video_id, typ: MusicPageType::Track { vtype: watch_endpoint .watch_endpoint_music_supported_configs .watch_endpoint_music_config .music_video_type, }, }) } } NavigationEndpoint::Browse { browse_endpoint, .. } => browse_endpoint .browse_endpoint_context_supported_configs .map(|config| { MusicPage::from_browse( browse_endpoint.browse_id, config.browse_endpoint_context_music_config.page_type, ) }), NavigationEndpoint::Url { .. } => None, NavigationEndpoint::WatchPlaylist { watch_playlist_endpoint, } => Some(MusicPage { id: watch_playlist_endpoint.playlist_id, typ: MusicPageType::Playlist { is_podcast: false }, }), NavigationEndpoint::CreatePlaylist { .. } => Some(MusicPage { id: String::new(), typ: MusicPageType::None, }), } } /// Get the page type of a browse endpoint pub(crate) fn page_type(&self) -> Option { if let NavigationEndpoint::Browse { browse_endpoint, command_metadata, } = self { browse_endpoint .browse_endpoint_context_supported_configs .as_ref() .map(|c| c.browse_endpoint_context_music_config.page_type) .or_else(|| { command_metadata .as_ref() .map(|c| c.web_command_metadata.web_page_type) }) } else { None } } pub(crate) fn into_playlist_id(self) -> Option { match self { NavigationEndpoint::Watch { watch_endpoint } => watch_endpoint.playlist_id, NavigationEndpoint::Browse { browse_endpoint, command_metadata, } => Some(browse_endpoint.browse_id).filter(|_| { browse_endpoint .browse_endpoint_context_supported_configs .map(|c| c.browse_endpoint_context_music_config.page_type == PageType::Playlist) .unwrap_or_default() || command_metadata .map(|c| c.web_command_metadata.web_page_type == PageType::Playlist) .unwrap_or_default() }), NavigationEndpoint::Url { .. } => None, NavigationEndpoint::WatchPlaylist { watch_playlist_endpoint, } => Some(watch_playlist_endpoint.playlist_id), NavigationEndpoint::CreatePlaylist { .. } => None, } } }