use anyhow::{anyhow, Context, Result}; use reqwest::Method; use serde::Serialize; use crate::{ model::{Channel, Playlist, Thumbnail, Video}, serializer::text::{PageType, TextLink}, util, }; use super::{response, ClientType, ContextYT, RustyTube}; #[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 RustyTube { pub async fn get_playlist(&self, playlist_id: &str) -> Result { let client = self.get_ytclient(ClientType::Desktop); let context = client.get_context(true).await; let request_body = QPlaylist { context, browse_id: "VL".to_owned() + playlist_id, }; let resp = client .request_builder(Method::POST, "browse") .await .json(&request_body) .send() .await? .error_for_status()?; let resp_body = resp.text().await?; let playlist_response = serde_json::from_str::(&resp_body).context(resp_body)?; map_playlist(&playlist_response) } pub async fn get_playlist_cont(&self, playlist: &mut Playlist) -> Result<()> { match &playlist.ctoken { Some(ctoken) => { let client = self.get_ytclient(ClientType::Desktop); let context = client.get_context(true).await; let request_body = QPlaylistCont { context, continuation: ctoken.to_owned(), }; let resp = client .request_builder(Method::POST, "browse") .await .json(&request_body) .send() .await? .error_for_status()?; let cont_response = resp.json::().await?; let action = some_or_bail!( cont_response .on_response_received_actions .iter() .find(|a| a.append_continuation_items_action.target_id == playlist.id), Err(anyhow!("no continuation action")) ); let (mut videos, ctoken) = map_playlist_items(&action.append_continuation_items_action.continuation_items); 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")), } } } fn map_playlist(response: &response::Playlist) -> Result { let video_items = &some_or_bail!( some_or_bail!( some_or_bail!( response .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); let (thumbnails, last_update_txt) = match &response.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!( &response .header .playlist_header_renderer .playlist_header_banner, Err(anyhow!("no thumbnail found")) ); let last_update_txt = response .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(&response.header.playlist_header_renderer.num_videos_text), Err(anyhow!("no video count")) ) } None => videos.len() as u32, }; let id = response .header .playlist_header_renderer .playlist_id .to_owned(); let name = response.header.playlist_header_renderer.title.to_owned(); let description = response .header .playlist_header_renderer .description_text .to_owned(); let channel = match &response.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.to_owned(), name: text.to_owned(), }), _ => None, }, _ => None, }, None => None, }; Ok(Playlist { id, name, videos, n_videos, ctoken, thumbnails, description, channel, last_update: None, last_update_txt, }) } fn map_playlist_items( items: &Vec>, ) -> (Vec