use anyhow::{anyhow, bail, Result}; use reqwest::Method; use serde::Serialize; use crate::{ deobfuscate::Deobfuscator, model::{Channel, Language, Playlist, Thumbnail, Video}, serializer::text::{PageType, TextLink}, timeago, util, }; use super::{response, ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery}; #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] struct QPlaylist { context: ContextYT, browse_id: String, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] struct QPlaylistCont { context: ContextYT, continuation: String, } impl RustyPipeQuery { pub async fn get_playlist(self, playlist_id: &str) -> Result { let context = self.get_context(ClientType::Desktop, true).await; let request_body = QPlaylist { context, browse_id: "VL".to_owned() + playlist_id, }; self.execute_request::( ClientType::Desktop, "get_playlist", Method::POST, "browse", playlist_id, &request_body, ) .await } pub async fn get_playlist_cont(self, playlist: &mut Playlist) -> Result<()> { match &playlist.ctoken { Some(ctoken) => { let context = self.get_context(ClientType::Desktop, true).await; let request_body = QPlaylistCont { context, continuation: ctoken.to_owned(), }; let (mut videos, ctoken) = self .execute_request::( ClientType::Desktop, "get_playlist_cont", Method::POST, "browse", &playlist.id, &request_body, ) .await?; playlist.videos.append(&mut videos); playlist.ctoken = ctoken; if playlist.ctoken.is_none() { playlist.n_videos = playlist.videos.len() as u32; } Ok(()) } None => Err(anyhow!("no ctoken")), } } } impl MapResponse for response::Playlist { fn map_response( self, id: &str, lang: Language, _deobf: Option<&Deobfuscator>, ) -> Result> { let video_items = &some_or_bail!( some_or_bail!( some_or_bail!( self.contents .two_column_browse_results_renderer .contents .get(0), Err(anyhow!("twoColumnBrowseResultsRenderer empty")) ) .tab_renderer .content .section_list_renderer .contents .get(0), Err(anyhow!("sectionListRenderer empty")) ) .item_section_renderer .contents .get(0), Err(anyhow!("itemSectionRenderer empty")) ) .playlist_video_list_renderer .contents; let (videos, ctoken) = map_playlist_items(&video_items.c); let (thumbnails, last_update_txt) = match &self.sidebar { Some(sidebar) => { let primary = some_or_bail!( sidebar.playlist_sidebar_renderer.items.get(0), Err(anyhow!("no primary sidebar")) ); ( &primary .playlist_sidebar_primary_info_renderer .thumbnail_renderer .playlist_video_thumbnail_renderer .thumbnail .thumbnails, primary .playlist_sidebar_primary_info_renderer .stats .get(2) .map(|t| t.to_owned()), ) } None => { let header_banner = some_or_bail!( &self.header.playlist_header_renderer.playlist_header_banner, Err(anyhow!("no thumbnail found")) ); let last_update_txt = self .header .playlist_header_renderer .byline .get(1) .map(|b| b.playlist_byline_renderer.text.to_owned()); ( &header_banner .hero_playlist_thumbnail_renderer .thumbnail .thumbnails, last_update_txt, ) } }; let thumbnails = thumbnails .iter() .map(|t| Thumbnail { url: t.url.to_owned(), width: t.width, height: t.height, }) .collect::>(); let n_videos = match ctoken { Some(_) => { ok_or_bail!( util::parse_numeric(&self.header.playlist_header_renderer.num_videos_text), Err(anyhow!("no video count")) ) } None => videos.len() as u32, }; let playlist_id = self.header.playlist_header_renderer.playlist_id; if playlist_id != id { bail!("got wrong playlist id {}, expected {}", playlist_id, id); } let name = self.header.playlist_header_renderer.title; let description = self.header.playlist_header_renderer.description_text; let channel = match self.header.playlist_header_renderer.owner_text { Some(owner_text) => match owner_text { TextLink::Browse { text, page_type, browse_id, } => match page_type { PageType::Channel => Some(Channel { id: browse_id, name: text, }), _ => None, }, _ => None, }, None => None, }; let mut warnings = video_items.warnings.to_owned(); let last_update = match &last_update_txt { Some(textual_date) => { let parsed = timeago::parse_textual_date_to_dt(lang, textual_date); if parsed.is_none() { warnings.push(format!("could not parse textual date `{}`", textual_date)); } parsed } None => None, }; Ok(MapResult { c: Playlist { id: playlist_id, name, videos, n_videos, ctoken, thumbnails, description, channel, last_update, last_update_txt, }, warnings, }) } } impl MapResponse<(Vec