add playlist date collector
This commit is contained in:
parent
513bf1dc9c
commit
c9433d721d
14 changed files with 20408 additions and 75 deletions
|
|
@ -1,10 +1,11 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
model::{Channel, Playlist, Thumbnail, Video},
|
||||
serializer::text::{PageType, Text, TextLink},
|
||||
serializer::text::{PageType, TextLink},
|
||||
util,
|
||||
};
|
||||
|
||||
use super::{response, ClientType, ContextYT, RustyTube};
|
||||
|
|
@ -41,7 +42,9 @@ impl RustyTube {
|
|||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
let playlist_response = resp.json::<response::Playlist>().await?;
|
||||
let resp_body = resp.text().await?;
|
||||
let playlist_response =
|
||||
serde_json::from_str::<response::Playlist>(&resp_body).context(resp_body)?;
|
||||
|
||||
map_playlist(&playlist_response)
|
||||
}
|
||||
|
|
@ -120,55 +123,54 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
|
||||
let (videos, ctoken) = map_playlist_items(video_items);
|
||||
|
||||
let thumbnail_renderer = some_or_bail!(
|
||||
response
|
||||
.sidebar
|
||||
.playlist_sidebar_renderer
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
response::playlist::SidebarRendererItem::PlaylistSidebarPrimaryInfoRenderer {
|
||||
thumbnail_renderer,
|
||||
} => Some(thumbnail_renderer),
|
||||
_ => None,
|
||||
}),
|
||||
Err(anyhow!("no primary sidebar"))
|
||||
);
|
||||
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"))
|
||||
);
|
||||
|
||||
let video_owner_wrap = response
|
||||
.sidebar
|
||||
.playlist_sidebar_renderer
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
response::playlist::SidebarRendererItem::PlaylistSidebarSecondaryInfoRenderer {
|
||||
video_owner,
|
||||
} => Some(video_owner),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let n_videos = match ctoken {
|
||||
Some(_) => {
|
||||
some_or_bail!(
|
||||
match &response.header.playlist_header_renderer.num_videos_text {
|
||||
Text::Multiple { runs } =>
|
||||
if runs.len() == 2 && runs[1] == " videos" {
|
||||
runs[0].replace(",", "").replace(".", "").parse().ok()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
Err(anyhow!("no video count"))
|
||||
(
|
||||
&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,
|
||||
)
|
||||
}
|
||||
None => videos.len() as u32,
|
||||
};
|
||||
|
||||
let thumbnails = thumbnail_renderer
|
||||
.playlist_video_thumbnail_renderer
|
||||
.thumbnail
|
||||
.thumbnails
|
||||
let thumbnails = thumbnails
|
||||
.iter()
|
||||
.map(|t| Thumbnail {
|
||||
url: t.url.to_owned(),
|
||||
|
|
@ -177,6 +179,16 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
|
|
@ -189,8 +201,8 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
.description_text
|
||||
.to_owned();
|
||||
|
||||
let channel = match video_owner_wrap {
|
||||
Some(o) => match &o.video_owner_renderer.title {
|
||||
let channel = match &response.header.playlist_header_renderer.owner_text {
|
||||
Some(owner_text) => match owner_text {
|
||||
TextLink::Browse {
|
||||
text,
|
||||
page_type,
|
||||
|
|
@ -217,6 +229,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
description,
|
||||
channel,
|
||||
last_update: None,
|
||||
last_update_txt,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,16 @@ use serde::Deserialize;
|
|||
use serde_with::serde_as;
|
||||
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::serializer::text::{Text, TextLink};
|
||||
use crate::serializer::text::TextLink;
|
||||
|
||||
use super::{
|
||||
ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem, VideoOwner,
|
||||
};
|
||||
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Playlist {
|
||||
pub contents: Contents,
|
||||
pub header: Header,
|
||||
pub sidebar: Sidebar,
|
||||
pub sidebar: Option<Sidebar>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -93,8 +91,35 @@ pub struct HeaderRenderer {
|
|||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
|
||||
pub description_text: Option<String>,
|
||||
/// `"495", " videos"`
|
||||
pub num_videos_text: Text,
|
||||
#[serde_as(as = "crate::serializer::text::Text")]
|
||||
pub num_videos_text: String,
|
||||
#[serde_as(as = "Option<crate::serializer::text::TextLink>")]
|
||||
pub owner_text: Option<TextLink>,
|
||||
|
||||
// Alternative layout
|
||||
pub playlist_header_banner: Option<PlaylistHeaderBanner>,
|
||||
#[serde(default)]
|
||||
pub byline: Vec<Byline>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistHeaderBanner {
|
||||
pub hero_playlist_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Byline {
|
||||
pub playlist_byline_renderer: BylineRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BylineRenderer {
|
||||
#[serde_as(as = "crate::serializer::text::Text")]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
@ -108,22 +133,25 @@ pub struct Sidebar {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SidebarRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub items: Vec<SidebarRendererItem>,
|
||||
pub items: Vec<SidebarItemPrimary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SidebarRendererItem {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PlaylistSidebarPrimaryInfoRenderer {
|
||||
thumbnail_renderer: PlaylistThumbnailRenderer,
|
||||
// - `"495", " videos"`
|
||||
// - `"3,310,996 views"`
|
||||
// - `"Last updated on ", "Aug 7, 2022"`
|
||||
// stats: Vec<Text>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwner },
|
||||
pub struct SidebarItemPrimary {
|
||||
pub playlist_sidebar_primary_info_renderer: SidebarPrimaryInfoRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SidebarPrimaryInfoRenderer {
|
||||
pub thumbnail_renderer: PlaylistThumbnailRenderer,
|
||||
// - `"495", " videos"`
|
||||
// - `"3,310,996 views"`
|
||||
// - `"Last updated on ", "Aug 7, 2022"`
|
||||
#[serde_as(as = "Vec<crate::serializer::text::Text>")]
|
||||
pub stats: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1925,4 +1925,5 @@ channel:
|
|||
id: UCIekuFeMaV78xYfvpmoCnPg
|
||||
name: Best Music
|
||||
last_update: ~
|
||||
last_update_txt: "Last updated on Aug 7, 2022"
|
||||
|
||||
|
|
|
|||
|
|
@ -1279,4 +1279,5 @@ channel:
|
|||
id: UCQM0bS4_04-Y4JuYrgmnpZQ
|
||||
name: Chaosflo44
|
||||
last_update: ~
|
||||
last_update_txt: "Last updated on Jul 2, 2014"
|
||||
|
||||
|
|
|
|||
|
|
@ -1863,4 +1863,5 @@ thumbnails:
|
|||
description: ~
|
||||
channel: ~
|
||||
last_update: ~
|
||||
last_update_txt: Updated today
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct QVideoCont {
|
|||
}
|
||||
|
||||
impl RustyTube {
|
||||
pub async fn get_video_response(&self, video_id: &str) -> Result<response::Video> {
|
||||
async fn get_video_response(&self, video_id: &str) -> Result<response::Video> {
|
||||
let client = self.get_ytclient(ClientType::Desktop);
|
||||
let context = client.get_context(true).await;
|
||||
let request_body = QVideo {
|
||||
|
|
@ -43,7 +43,7 @@ impl RustyTube {
|
|||
Ok(resp.json::<response::Video>().await?)
|
||||
}
|
||||
|
||||
pub async fn get_comments_response(&self, ctoken: &str) -> Result<response::VideoComments> {
|
||||
async fn get_comments_response(&self, ctoken: &str) -> Result<response::VideoComments> {
|
||||
let client = self.get_ytclient(ClientType::Desktop);
|
||||
let context = client.get_context(true).await;
|
||||
let request_body = QVideoCont {
|
||||
|
|
@ -62,7 +62,7 @@ impl RustyTube {
|
|||
Ok(resp.json::<response::VideoComments>().await?)
|
||||
}
|
||||
|
||||
pub async fn get_recommendations_response(
|
||||
async fn get_recommendations_response(
|
||||
&self,
|
||||
ctoken: &str,
|
||||
) -> Result<response::VideoRecommendations> {
|
||||
|
|
|
|||
Reference in a new issue