Compare commits
No commits in common. "db6a479bcfab96deed27be9b6c3a00f41ab3ef0b" and "a3e3269fb322712bc746b88146553d3d86d930ce" have entirely different histories.
db6a479bcf
...
a3e3269fb3
24 changed files with 280 additions and 887 deletions
|
@ -11,4 +11,3 @@ repos:
|
|||
- id: cargo-fmt
|
||||
- id: cargo-check
|
||||
- id: cargo-clippy
|
||||
args: ["--", "-D", "warnings"]
|
||||
|
|
|
@ -19,9 +19,9 @@ use super::{
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QChannel<'a> {
|
||||
struct QChannel {
|
||||
context: YTContext,
|
||||
browse_id: &'a str,
|
||||
browse_id: String,
|
||||
params: Params,
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
browse_id: channel_id,
|
||||
browse_id: channel_id.to_owned(),
|
||||
params: match order {
|
||||
ChannelOrder::Latest => Params::VideosLatest,
|
||||
ChannelOrder::Oldest => Params::VideosOldest,
|
||||
|
@ -81,7 +81,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
continuation: ctoken.to_owned(),
|
||||
};
|
||||
|
||||
self.execute_request::<response::ChannelCont, _, _>(
|
||||
|
@ -101,7 +101,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
browse_id: channel_id,
|
||||
browse_id: channel_id.to_owned(),
|
||||
params: Params::Playlists,
|
||||
};
|
||||
|
||||
|
@ -122,7 +122,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
continuation: ctoken.to_owned(),
|
||||
};
|
||||
|
||||
self.execute_request::<response::ChannelCont, _, _>(
|
||||
|
@ -139,7 +139,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
browse_id: channel_id,
|
||||
browse_id: channel_id.to_owned(),
|
||||
params: Params::Info,
|
||||
};
|
||||
|
||||
|
@ -180,7 +180,7 @@ impl MapResponse<Channel<Paginator<ChannelVideo>>> for response::Channel {
|
|||
id,
|
||||
lang,
|
||||
)?,
|
||||
warnings,
|
||||
warnings: warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ impl MapResponse<Channel<Paginator<ChannelPlaylist>>> for response::Channel {
|
|||
id,
|
||||
lang,
|
||||
)?,
|
||||
warnings,
|
||||
warnings: warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ impl RustyPipeQuery {
|
|||
msgs: Vec::new(),
|
||||
deobf_data: None,
|
||||
http_request: crate::report::HTTPRequest {
|
||||
url,
|
||||
url: url,
|
||||
method: "GET".to_owned(),
|
||||
req_header: BTreeMap::new(),
|
||||
req_body: String::new(),
|
||||
|
|
|
@ -5,7 +5,6 @@ mod pagination;
|
|||
mod player;
|
||||
mod playlist;
|
||||
mod response;
|
||||
mod search;
|
||||
mod video_details;
|
||||
|
||||
#[cfg(feature = "rss")]
|
||||
|
@ -125,9 +124,9 @@ struct ThirdParty {
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QContinuation<'a> {
|
||||
struct QContinuation {
|
||||
context: YTContext,
|
||||
continuation: &'a str,
|
||||
continuation: String,
|
||||
}
|
||||
|
||||
const DEFAULT_UA: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0";
|
||||
|
@ -515,11 +514,11 @@ impl RustyPipe {
|
|||
)
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or_else(|| {
|
||||
Error::from(ExtractionError::InvalidData(
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData(
|
||||
"Could not find desktop client version in sw.js".into(),
|
||||
))
|
||||
})
|
||||
),
|
||||
))
|
||||
};
|
||||
|
||||
let from_html = async {
|
||||
|
@ -533,11 +532,11 @@ impl RustyPipe {
|
|||
)
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or_else(|| {
|
||||
Error::from(ExtractionError::InvalidData(
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData(
|
||||
"Could not find desktop client version in sw.js".into(),
|
||||
))
|
||||
})
|
||||
),
|
||||
))
|
||||
};
|
||||
|
||||
match from_swjs.await {
|
||||
|
@ -562,11 +561,9 @@ impl RustyPipe {
|
|||
)
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or_else(|| {
|
||||
Error::from(ExtractionError::InvalidData(
|
||||
"Could not find music client version in sw.js".into(),
|
||||
))
|
||||
})
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData("Could not find music client version in sw.js".into()),
|
||||
))
|
||||
};
|
||||
|
||||
let from_html = async {
|
||||
|
@ -580,11 +577,11 @@ impl RustyPipe {
|
|||
)
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or_else(|| {
|
||||
Error::from(ExtractionError::InvalidData(
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData(
|
||||
"Could not find music client version on html page".into(),
|
||||
))
|
||||
})
|
||||
),
|
||||
))
|
||||
};
|
||||
|
||||
match from_swjs.await {
|
||||
|
@ -994,14 +991,12 @@ impl RustyPipeQuery {
|
|||
if !mapres.warnings.is_empty() {
|
||||
create_report(
|
||||
Level::WRN,
|
||||
Some(ExtractionError::DeserializationWarnings.to_string()),
|
||||
Some(ExtractionError::Warnings.to_string()),
|
||||
mapres.warnings,
|
||||
);
|
||||
|
||||
if self.opts.strict {
|
||||
return Err(Error::Extraction(
|
||||
ExtractionError::DeserializationWarnings,
|
||||
));
|
||||
return Err(Error::Extraction(ExtractionError::Warnings));
|
||||
}
|
||||
} else if self.opts.report {
|
||||
create_report(Level::DBG, None, vec![]);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::error::Result;
|
||||
|
||||
use crate::model::{
|
||||
ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo, SearchItem,
|
||||
ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo,
|
||||
};
|
||||
|
||||
use super::RustyPipeQuery;
|
||||
|
@ -225,47 +225,3 @@ impl Paginator<Comment> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Paginator<SearchItem> {
|
||||
pub async fn next(&self, query: RustyPipeQuery) -> Result<Option<Self>> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(query.search_continuation(ctoken).await?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn extend(&mut self, query: RustyPipeQuery) -> Result<bool> {
|
||||
match self.next(query).await {
|
||||
Ok(Some(paginator)) => {
|
||||
let mut items = paginator.items;
|
||||
self.items.append(&mut items);
|
||||
self.ctoken = paginator.ctoken;
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn extend_pages(&mut self, query: RustyPipeQuery, n_pages: usize) -> Result<()> {
|
||||
for _ in 0..n_pages {
|
||||
match self.extend(query.clone()).await {
|
||||
Ok(false) => break,
|
||||
Err(e) => return Err(e),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn extend_limit(&mut self, query: RustyPipeQuery, n_items: usize) -> Result<()> {
|
||||
while self.items.len() < n_items {
|
||||
match self.extend(query.clone()).await {
|
||||
Ok(false) => break,
|
||||
Err(e) => return Err(e),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ use super::{
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QPlayer<'a> {
|
||||
struct QPlayer {
|
||||
context: YTContext,
|
||||
/// Website playback context
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -35,7 +35,7 @@ struct QPlayer<'a> {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cpn: Option<String>,
|
||||
/// YouTube video ID
|
||||
video_id: &'a str,
|
||||
video_id: String,
|
||||
/// Set to true to allow extraction of streams with sensitive content
|
||||
content_check_ok: bool,
|
||||
/// Probably refers to allowing sensitive content, too
|
||||
|
@ -82,7 +82,7 @@ impl RustyPipeQuery {
|
|||
},
|
||||
}),
|
||||
cpn: None,
|
||||
video_id,
|
||||
video_id: video_id.to_owned(),
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ impl RustyPipeQuery {
|
|||
context,
|
||||
playback_context: None,
|
||||
cpn: Some(util::generate_content_playback_nonce()),
|
||||
video_id,
|
||||
video_id: video_id.to_owned(),
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
continuation: ctoken.to_owned(),
|
||||
};
|
||||
|
||||
self.execute_request::<response::PlaylistCont, _, _>(
|
||||
|
@ -143,7 +143,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
Err(ExtractionError::InvalidData("no video count".into()))
|
||||
)
|
||||
}
|
||||
None => videos.len() as u64,
|
||||
None => videos.len() as u32,
|
||||
};
|
||||
|
||||
let playlist_id = self.header.playlist_header_renderer.playlist_id;
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::serializer::ignore_any;
|
|||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Channel {
|
||||
pub header: Header,
|
||||
|
@ -19,13 +19,13 @@ pub struct Channel {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelCont {
|
||||
pub on_response_received_actions: Vec<OnResponseReceivedAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contents {
|
||||
pub two_column_browse_results_renderer: TabsRenderer,
|
||||
|
@ -34,26 +34,26 @@ pub struct Contents {
|
|||
/// YouTube channel tab view. Contains multiple tabs
|
||||
/// (Home, Videos, Playlists, About...). We can ignore unknown tabs.
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TabsRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub tabs: Vec<TabRendererWrap>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TabRendererWrap {
|
||||
pub tab_renderer: ContentRenderer<SectionListRendererWrap>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SectionListRendererWrap {
|
||||
pub section_list_renderer: SectionListRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SectionListRenderer {
|
||||
pub contents: Vec<ItemSectionRendererWrap>,
|
||||
|
@ -63,14 +63,14 @@ pub struct SectionListRenderer {
|
|||
pub target_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ItemSectionRendererWrap {
|
||||
pub item_section_renderer: ContentsRenderer<ChannelContent>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ChannelContent {
|
||||
GridRenderer {
|
||||
|
@ -83,14 +83,14 @@ pub enum ChannelContent {
|
|||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Header {
|
||||
pub c4_tabbed_header_renderer: HeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HeaderRenderer {
|
||||
pub channel_id: String,
|
||||
|
@ -114,26 +114,26 @@ pub struct HeaderRenderer {
|
|||
pub tv_banner: Thumbnails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Metadata {
|
||||
pub channel_metadata_renderer: ChannelMetadataRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelMetadataRenderer {
|
||||
pub description: String,
|
||||
pub vanity_channel_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Microformat {
|
||||
pub microformat_data_renderer: MicroformatDataRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MicroformatDataRenderer {
|
||||
#[serde(default)]
|
||||
|
@ -141,7 +141,7 @@ pub struct MicroformatDataRenderer {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelFullMetadata {
|
||||
#[serde_as(as = "Text")]
|
||||
|
@ -153,7 +153,7 @@ pub struct ChannelFullMetadata {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrimaryLink {
|
||||
#[serde_as(as = "Text")]
|
||||
|
@ -161,26 +161,26 @@ pub struct PrimaryLink {
|
|||
pub navigation_endpoint: NavigationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NavigationEndpoint {
|
||||
pub url_endpoint: UrlEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UrlEndpoint {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OnResponseReceivedAction {
|
||||
pub append_continuation_items_action: AppendAction,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppendAction {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
|
|
|
@ -3,7 +3,7 @@ use serde::Deserialize;
|
|||
|
||||
use super::Thumbnail;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ChannelRss {
|
||||
#[serde(rename = "$unflatten=yt:channelId")]
|
||||
pub channel_id: String,
|
||||
|
@ -14,7 +14,7 @@ pub struct ChannelRss {
|
|||
pub entry: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Entry {
|
||||
#[serde(rename = "$unflatten=yt:videoId")]
|
||||
pub video_id: String,
|
||||
|
@ -28,7 +28,7 @@ pub struct Entry {
|
|||
pub media_group: MediaGroup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct MediaGroup {
|
||||
#[serde(rename = "$unflatten=media:thumbnail")]
|
||||
pub thumbnail: Thumbnail,
|
||||
|
@ -38,7 +38,7 @@ pub struct MediaGroup {
|
|||
pub community: Community,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Community {
|
||||
#[serde(rename = "$unflatten=media:starRating")]
|
||||
pub rating: Rating,
|
||||
|
@ -46,12 +46,12 @@ pub struct Community {
|
|||
pub statistics: Statistics,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Rating {
|
||||
pub count: u64,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Statistics {
|
||||
pub views: u64,
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ pub mod channel;
|
|||
pub mod player;
|
||||
pub mod playlist;
|
||||
pub mod playlist_music;
|
||||
pub mod search;
|
||||
pub mod video_details;
|
||||
|
||||
pub use channel::Channel;
|
||||
|
@ -11,8 +10,6 @@ pub use player::Player;
|
|||
pub use playlist::Playlist;
|
||||
pub use playlist::PlaylistCont;
|
||||
pub use playlist_music::PlaylistMusic;
|
||||
pub use search::Search;
|
||||
pub use search::SearchCont;
|
||||
pub use video_details::VideoComments;
|
||||
pub use video_details::VideoDetails;
|
||||
pub use video_details::VideoRecommendations;
|
||||
|
@ -30,20 +27,20 @@ use crate::serializer::{
|
|||
text::{Text, TextComponent},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContentRenderer<T> {
|
||||
pub content: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContentsRenderer<T> {
|
||||
#[serde(alias = "tabs")]
|
||||
pub contents: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ThumbnailsWrap {
|
||||
#[serde(default)]
|
||||
|
@ -52,14 +49,14 @@ pub struct ThumbnailsWrap {
|
|||
|
||||
/// List of images in different resolutions.
|
||||
/// Not only used for thumbnails, but also for avatars and banners.
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Thumbnails {
|
||||
#[serde(default)]
|
||||
pub thumbnails: Vec<Thumbnail>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Thumbnail {
|
||||
pub url: String,
|
||||
|
@ -67,17 +64,13 @@ pub struct Thumbnail {
|
|||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum VideoListItem {
|
||||
/// Video on channel page
|
||||
GridVideoRenderer(GridVideoRenderer),
|
||||
/// Video in recommendations
|
||||
CompactVideoRenderer(CompactVideoRenderer),
|
||||
/// Video in playlist
|
||||
PlaylistVideoRenderer(PlaylistVideoRenderer),
|
||||
|
||||
/// Playlist on channel page
|
||||
GridPlaylistRenderer(GridPlaylistRenderer),
|
||||
|
||||
/// Continauation items are located at the end of a list
|
||||
|
@ -97,7 +90,7 @@ pub enum VideoListItem {
|
|||
|
||||
/// Video displayed on a channel page
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GridVideoRenderer {
|
||||
pub video_id: String,
|
||||
|
@ -118,7 +111,7 @@ pub struct GridVideoRenderer {
|
|||
|
||||
/// Video displayed in recommendations
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompactVideoRenderer {
|
||||
pub video_id: String,
|
||||
|
@ -152,7 +145,7 @@ pub struct CompactVideoRenderer {
|
|||
|
||||
/// Video displayed in a playlist
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistVideoRenderer {
|
||||
pub video_id: String,
|
||||
|
@ -167,7 +160,7 @@ pub struct PlaylistVideoRenderer {
|
|||
|
||||
/// Playlist displayed on a channel page
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GridPlaylistRenderer {
|
||||
pub playlist_id: String,
|
||||
|
@ -179,7 +172,7 @@ pub struct GridPlaylistRenderer {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpcomingEventData {
|
||||
/// Unixtime in seconds
|
||||
|
@ -187,26 +180,26 @@ pub struct UpcomingEventData {
|
|||
pub start_time: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContinuationItemRenderer {
|
||||
pub continuation_endpoint: ContinuationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContinuationEndpoint {
|
||||
pub continuation_command: ContinuationCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContinuationCommand {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Icon {
|
||||
pub icon_type: IconType,
|
||||
|
@ -223,14 +216,14 @@ pub enum IconType {
|
|||
Like,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoOwner {
|
||||
pub video_owner_renderer: VideoOwnerRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoOwnerRenderer {
|
||||
pub title: TextComponent,
|
||||
|
@ -242,13 +235,13 @@ pub struct VideoOwnerRenderer {
|
|||
pub badges: Vec<ChannelBadge>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelBadge {
|
||||
pub metadata_badge_renderer: ChannelBadgeRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelBadgeRenderer {
|
||||
pub style: ChannelBadgeStyle,
|
||||
|
@ -261,19 +254,19 @@ pub enum ChannelBadgeStyle {
|
|||
BadgeStyleTypeVerifiedArtist,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TimeOverlay {
|
||||
pub thumbnail_overlay_time_status_renderer: TimeOverlayRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TimeOverlayRenderer {
|
||||
/// `29:54`
|
||||
///
|
||||
/// Is `LIVE` in case of a livestream and `SHORTS` in case of a short video
|
||||
/// Is `LIVE` in case of a livestream
|
||||
#[serde_as(as = "Text")]
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
|
@ -281,7 +274,7 @@ pub struct TimeOverlayRenderer {
|
|||
pub style: TimeOverlayStyle,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum TimeOverlayStyle {
|
||||
#[default]
|
||||
|
@ -292,7 +285,7 @@ pub enum TimeOverlayStyle {
|
|||
|
||||
/// Badges are displayed on the video thumbnail and
|
||||
/// show certain video properties (e.g. active livestream)
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoBadge {
|
||||
pub metadata_badge_renderer: VideoBadgeRenderer,
|
||||
|
@ -300,7 +293,7 @@ pub struct VideoBadge {
|
|||
|
||||
/// Badges are displayed on the video thumbnail and
|
||||
/// show certain video properties (e.g. active livestream)
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoBadgeRenderer {
|
||||
pub style: VideoBadgeStyle,
|
||||
|
@ -316,7 +309,7 @@ pub enum VideoBadgeStyle {
|
|||
// YouTube Music
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicItem {
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
|
@ -329,21 +322,21 @@ pub struct MusicItem {
|
|||
pub fixed_columns: Vec<MusicColumn>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicThumbnailRenderer {
|
||||
#[serde(alias = "croppedSquareThumbnailRenderer")]
|
||||
pub music_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistItemData {
|
||||
pub video_id: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicContentsRenderer<T> {
|
||||
pub contents: Vec<T>,
|
||||
|
@ -351,7 +344,7 @@ pub struct MusicContentsRenderer<T> {
|
|||
pub continuations: Option<Vec<MusicContinuation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct MusicColumn {
|
||||
#[serde(
|
||||
rename = "musicResponsiveListItemFlexColumnRenderer",
|
||||
|
@ -361,18 +354,18 @@ pub struct MusicColumn {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct MusicColumnRenderer {
|
||||
pub text: TextComponent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicContinuation {
|
||||
pub next_continuation_data: MusicContinuationData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicContinuationData {
|
||||
pub continuation: String,
|
||||
|
|
|
@ -8,7 +8,7 @@ use serde_with::{json::JsonString, DefaultOnError};
|
|||
use super::Thumbnails;
|
||||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Player {
|
||||
pub playability_status: PlayabilityStatus,
|
||||
|
@ -18,7 +18,7 @@ pub struct Player {
|
|||
pub microformat: Option<Microformat>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(tag = "status", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PlayabilityStatus {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -36,11 +36,11 @@ pub enum PlayabilityStatus {
|
|||
Error { reason: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Empty {}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StreamingData {
|
||||
#[serde_as(as = "JsonString")]
|
||||
|
@ -58,7 +58,7 @@ pub struct StreamingData {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Format {
|
||||
pub itag: u32,
|
||||
|
@ -152,7 +152,7 @@ pub enum FormatType {
|
|||
FormatStreamTypeOtf,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct ColorInfo {
|
||||
pub primaries: Primaries,
|
||||
|
@ -166,7 +166,7 @@ pub enum Primaries {
|
|||
ColorPrimariesBt2020,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct AudioTrack {
|
||||
pub id: String,
|
||||
|
@ -174,20 +174,20 @@ pub struct AudioTrack {
|
|||
pub audio_is_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Captions {
|
||||
pub player_captions_tracklist_renderer: PlayerCaptionsTracklistRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlayerCaptionsTracklistRenderer {
|
||||
pub caption_tracks: Vec<CaptionTrack>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CaptionTrack {
|
||||
pub base_url: String,
|
||||
|
@ -197,7 +197,7 @@ pub struct CaptionTrack {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoDetails {
|
||||
pub video_id: String,
|
||||
|
@ -215,7 +215,7 @@ pub struct VideoDetails {
|
|||
pub is_live_content: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Microformat {
|
||||
#[serde(alias = "microformatDataRenderer")]
|
||||
|
@ -223,7 +223,7 @@ pub struct Microformat {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlayerMicroformatRenderer {
|
||||
#[serde(alias = "familySafe")]
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::serializer::{MapResult, VecLogError};
|
|||
|
||||
use super::{ContentRenderer, ContentsRenderer, ThumbnailsWrap, VideoListItem};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Playlist {
|
||||
pub contents: Contents,
|
||||
|
@ -16,59 +16,59 @@ pub struct Playlist {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistCont {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub on_response_received_actions: Vec<OnResponseReceivedAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contents {
|
||||
pub two_column_browse_results_renderer: ContentsRenderer<Tab>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tab {
|
||||
pub tab_renderer: ContentRenderer<SectionList>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SectionList {
|
||||
pub section_list_renderer: ContentsRenderer<ItemSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ItemSection {
|
||||
pub item_section_renderer: ContentsRenderer<PlaylistVideoListRenderer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistVideoListRenderer {
|
||||
pub playlist_video_list_renderer: PlaylistVideoList,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistVideoList {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
pub contents: MapResult<Vec<VideoListItem>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Header {
|
||||
pub playlist_header_renderer: HeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HeaderRenderer {
|
||||
pub playlist_id: String,
|
||||
|
@ -87,48 +87,48 @@ pub struct HeaderRenderer {
|
|||
pub byline: Vec<Byline>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistHeaderBanner {
|
||||
pub hero_playlist_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Byline {
|
||||
pub playlist_byline_renderer: BylineRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BylineRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Sidebar {
|
||||
pub playlist_sidebar_renderer: SidebarRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SidebarRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub items: Vec<SidebarItemPrimary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SidebarItemPrimary {
|
||||
pub playlist_sidebar_primary_info_renderer: SidebarPrimaryInfoRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SidebarPrimaryInfoRenderer {
|
||||
pub thumbnail_renderer: PlaylistThumbnailRenderer,
|
||||
|
@ -139,7 +139,7 @@ pub struct SidebarPrimaryInfoRenderer {
|
|||
pub stats: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistThumbnailRenderer {
|
||||
// the alternative field name is used by YTM playlists
|
||||
|
@ -147,14 +147,14 @@ pub struct PlaylistThumbnailRenderer {
|
|||
pub playlist_video_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OnResponseReceivedAction {
|
||||
pub append_continuation_items_action: AppendAction,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppendAction {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
|
|
|
@ -9,33 +9,33 @@ use super::{
|
|||
ContentRenderer, ContentsRenderer, MusicContentsRenderer, MusicContinuation, MusicItem,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistMusic {
|
||||
pub contents: Contents,
|
||||
pub header: Header,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contents {
|
||||
pub single_column_browse_results_renderer: ContentsRenderer<Tab>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tab {
|
||||
pub tab_renderer: ContentRenderer<SectionList>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SectionList {
|
||||
/// Includes a continuation token for fetching recommendations
|
||||
pub section_list_renderer: MusicContentsRenderer<ItemSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ItemSection {
|
||||
#[serde(alias = "musicPlaylistShelfRenderer")]
|
||||
|
@ -43,7 +43,7 @@ pub struct ItemSection {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicShelf {
|
||||
/// Playlist ID (only for playlists)
|
||||
|
@ -55,20 +55,20 @@ pub struct MusicShelf {
|
|||
pub continuations: Option<Vec<MusicContinuation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistMusicItem {
|
||||
pub music_responsive_list_item_renderer: MusicItem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Header {
|
||||
pub music_detail_header_renderer: HeaderRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HeaderRenderer {
|
||||
#[serde_as(as = "crate::serializer::text::Text")]
|
||||
|
|
|
@ -1,215 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::json::JsonString;
|
||||
use serde_with::{serde_as, VecSkipError};
|
||||
|
||||
use crate::serializer::ignore_any;
|
||||
use crate::serializer::{
|
||||
text::{Text, TextComponent},
|
||||
MapResult, VecLogError,
|
||||
};
|
||||
|
||||
use super::{ChannelBadge, ContentsRenderer, ContinuationEndpoint, Thumbnails, TimeOverlay};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Search {
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub estimated_results: Option<u64>,
|
||||
pub contents: Contents,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchCont {
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub estimated_results: Option<u64>,
|
||||
pub on_response_received_commands: Vec<SearchContCommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchContCommand {
|
||||
pub append_continuation_items_action: SearchContAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchContAction {
|
||||
pub continuation_items: Vec<SectionListItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contents {
|
||||
pub two_column_search_results_renderer: TwoColumnSearchResultsRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TwoColumnSearchResultsRenderer {
|
||||
pub primary_contents: PrimaryContents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrimaryContents {
|
||||
pub section_list_renderer: ContentsRenderer<SectionListItem>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SectionListItem {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ItemSectionRenderer {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
contents: MapResult<Vec<SearchItem>>,
|
||||
},
|
||||
/// Continuation token to fetch more search results
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SearchItem {
|
||||
/// Video in search results
|
||||
VideoRenderer(VideoRenderer),
|
||||
/// Playlist in search results
|
||||
PlaylistRenderer(PlaylistRenderer),
|
||||
/// Channel displayed in search results
|
||||
ChannelRenderer(ChannelRenderer),
|
||||
|
||||
/// Corrected search query
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ShowingResultsForRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
corrected_query: String,
|
||||
},
|
||||
/// No search result item (e.g. ad) or unimplemented item
|
||||
///
|
||||
/// Unimplemented:
|
||||
/// - shelfRenderer (e.g. Latest from channel, For you)
|
||||
#[serde(other, deserialize_with = "ignore_any")]
|
||||
None,
|
||||
}
|
||||
|
||||
/// Video displayed in search results
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoRenderer {
|
||||
pub video_id: String,
|
||||
pub thumbnail: Thumbnails,
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
#[serde(rename = "shortBylineText")]
|
||||
pub channel: TextComponent,
|
||||
pub channel_thumbnail_supported_renderers: ChannelThumbnailSupportedRenderers,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub published_time_text: Option<String>,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub length_text: Option<String>,
|
||||
/// Contains `No views` if the view count is zero
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub view_count_text: Option<String>,
|
||||
/// Channel verification badge
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub owner_badges: Vec<ChannelBadge>,
|
||||
/// Contains Short/Live tag
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub thumbnail_overlays: Vec<TimeOverlay>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub detailed_metadata_snippets: Vec<DetailedMetadataSnippet>,
|
||||
}
|
||||
|
||||
/// Playlist displayed in search results
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistRenderer {
|
||||
pub playlist_id: String,
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
/// The first item of this list contains the playlist thumbnail,
|
||||
/// subsequent items contain very small thumbnails of the next playlist videos
|
||||
pub thumbnails: Vec<Thumbnails>,
|
||||
#[serde_as(as = "JsonString")]
|
||||
pub video_count: u64,
|
||||
#[serde(rename = "shortBylineText")]
|
||||
pub channel: TextComponent,
|
||||
/// Channel verification badge
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub owner_badges: Vec<ChannelBadge>,
|
||||
/// First 2 videos
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub videos: Vec<ChildVideoRendererWrap>,
|
||||
}
|
||||
|
||||
/// Channel displayed in search results
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelRenderer {
|
||||
pub channel_id: String,
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
pub thumbnail: Thumbnails,
|
||||
/// Abbreviated channel description
|
||||
#[serde_as(as = "Text")]
|
||||
pub description_snippet: String,
|
||||
#[serde_as(as = "Text")]
|
||||
pub video_count_text: String,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub subscriber_count_text: Option<String>,
|
||||
/// Channel verification badge
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub owner_badges: Vec<ChannelBadge>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelThumbnailSupportedRenderers {
|
||||
pub channel_thumbnail_with_link_renderer: ChannelThumbnailWithLinkRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelThumbnailWithLinkRenderer {
|
||||
pub thumbnail: Thumbnails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChildVideoRendererWrap {
|
||||
pub child_video_renderer: ChildVideoRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChildVideoRenderer {
|
||||
pub video_id: String,
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub length_text: Option<String>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DetailedMetadataSnippet {
|
||||
#[serde_as(as = "Text")]
|
||||
pub snippet_text: String,
|
||||
}
|
|
@ -4,10 +4,12 @@ use serde::Deserialize;
|
|||
use serde_with::serde_as;
|
||||
use serde_with::{DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::serializer::text::TextComponents;
|
||||
use crate::serializer::MapResult;
|
||||
use crate::serializer::{
|
||||
ignore_any,
|
||||
text::{AccessibilityText, AttributedText, Text, TextComponents},
|
||||
MapResult, VecLogError,
|
||||
text::{AccessibilityText, AttributedText, Text},
|
||||
VecLogError,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -20,26 +22,26 @@ use super::{
|
|||
|
||||
/// Video details response
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoDetails {
|
||||
/// Video metadata + recommended videos
|
||||
pub contents: Contents,
|
||||
/// Video ID
|
||||
pub current_video_endpoint: CurrentVideoEndpoint,
|
||||
/// Video chapters + comment section
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
/// Video chapters + comment section
|
||||
pub engagement_panels: MapResult<Vec<EngagementPanel>>,
|
||||
}
|
||||
|
||||
/// Video details main object, contains video metadata and recommended videos
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contents {
|
||||
pub two_column_watch_next_results: TwoColumnWatchNextResults,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TwoColumnWatchNextResults {
|
||||
/// Metadata about the video
|
||||
|
@ -51,7 +53,7 @@ pub struct TwoColumnWatchNextResults {
|
|||
}
|
||||
|
||||
/// Metadata about the video
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoResultsWrap {
|
||||
pub results: VideoResults,
|
||||
|
@ -59,7 +61,7 @@ pub struct VideoResultsWrap {
|
|||
|
||||
/// Video metadata items
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoResults {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
|
@ -68,7 +70,7 @@ pub struct VideoResults {
|
|||
|
||||
/// Video metadata item
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum VideoResultsItem {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -102,14 +104,14 @@ pub enum VideoResultsItem {
|
|||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ViewCount {
|
||||
pub video_view_count_renderer: ViewCountRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ViewCountRenderer {
|
||||
/// View count (`232,975,196 views`)
|
||||
|
@ -120,7 +122,7 @@ pub struct ViewCountRenderer {
|
|||
}
|
||||
|
||||
/// Like/Dislike buttons
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoActions {
|
||||
pub menu_renderer: VideoActionsMenu,
|
||||
|
@ -128,7 +130,7 @@ pub struct VideoActions {
|
|||
|
||||
/// Like/Dislike buttons
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoActionsMenu {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
|
@ -141,7 +143,7 @@ pub struct VideoActionsMenu {
|
|||
///
|
||||
/// See: https://github.com/TeamNewPipe/NewPipeExtractor/pull/926
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TopLevelButton {
|
||||
ToggleButtonRenderer(ToggleButton),
|
||||
|
@ -152,7 +154,7 @@ pub enum TopLevelButton {
|
|||
}
|
||||
|
||||
/// Like/Dislike button
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToggleButtonWrap {
|
||||
pub toggle_button_renderer: ToggleButton,
|
||||
|
@ -160,7 +162,7 @@ pub struct ToggleButtonWrap {
|
|||
|
||||
/// Like/Dislike button
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToggleButton {
|
||||
/// Icon type: `LIKE` / `DISLIKE`
|
||||
|
@ -174,7 +176,7 @@ pub struct ToggleButton {
|
|||
|
||||
/// Shows additional video metadata. Its only known use is for
|
||||
/// the Creative Commonse License.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetadataRowContainer {
|
||||
pub metadata_row_container_renderer: MetadataRowContainerRenderer,
|
||||
|
@ -182,14 +184,14 @@ pub struct MetadataRowContainer {
|
|||
|
||||
/// Shows additional video metadata. Its only known use is for
|
||||
/// the Creative Commonse License.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetadataRowContainerRenderer {
|
||||
pub rows: Vec<MetadataRow>,
|
||||
}
|
||||
|
||||
/// Additional video metadata item (Creative Commons License)
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetadataRow {
|
||||
pub metadata_row_renderer: MetadataRowRenderer,
|
||||
|
@ -197,7 +199,7 @@ pub struct MetadataRow {
|
|||
|
||||
/// Additional video metadata item (Creative Commons License)
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetadataRowRenderer {
|
||||
// `License`
|
||||
|
@ -212,13 +214,13 @@ pub struct MetadataRowRenderer {
|
|||
}
|
||||
|
||||
/// Contains current video ID
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CurrentVideoEndpoint {
|
||||
pub watch_endpoint: CurrentVideoWatchEndpoint,
|
||||
}
|
||||
/// Contains current video ID
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CurrentVideoWatchEndpoint {
|
||||
pub video_id: String,
|
||||
|
@ -229,7 +231,7 @@ pub struct CurrentVideoWatchEndpoint {
|
|||
/// 1. CommentsEntryPointHeaderRenderer: contains number of comments
|
||||
/// 2. ContinuationItemRenderer: contains continuation token
|
||||
#[serde_as]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", tag = "sectionIdentifier")]
|
||||
pub enum ItemSection {
|
||||
CommentsEntryPoint {
|
||||
|
@ -245,7 +247,7 @@ pub enum ItemSection {
|
|||
}
|
||||
|
||||
/// Item section containing comment count
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ItemSectionCommentCount {
|
||||
pub comments_entry_point_header_renderer: CommentsEntryPointHeaderRenderer,
|
||||
|
@ -253,7 +255,7 @@ pub struct ItemSectionCommentCount {
|
|||
|
||||
/// Renderer of item section containing comment count
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentsEntryPointHeaderRenderer {
|
||||
#[serde_as(as = "Text")]
|
||||
|
@ -261,14 +263,14 @@ pub struct CommentsEntryPointHeaderRenderer {
|
|||
}
|
||||
|
||||
/// Item section containing comments ctoken
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ItemSectionComments {
|
||||
pub continuation_item_renderer: ContinuationItemRenderer,
|
||||
}
|
||||
|
||||
/// Video recommendations
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RecommendationResultsWrap {
|
||||
pub secondary_results: RecommendationResults,
|
||||
|
@ -276,7 +278,7 @@ pub struct RecommendationResultsWrap {
|
|||
|
||||
/// Video recommendations
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RecommendationResults {
|
||||
/// Can be `None` for age-restricted videos
|
||||
|
@ -286,7 +288,7 @@ pub struct RecommendationResults {
|
|||
|
||||
/// The engagement panels are displayed below the video and contain chapter markers
|
||||
/// and the comment section.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EngagementPanel {
|
||||
pub engagement_panel_section_list_renderer: EngagementPanelRenderer,
|
||||
|
@ -294,7 +296,7 @@ pub struct EngagementPanel {
|
|||
|
||||
/// The engagement panels are displayed below the video and contain chapter markers
|
||||
/// and the comment section.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", tag = "targetId")]
|
||||
pub enum EngagementPanelRenderer {
|
||||
/// Chapter markers
|
||||
|
@ -313,7 +315,7 @@ pub enum EngagementPanelRenderer {
|
|||
}
|
||||
|
||||
/// Chapter markers
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterMarkersContent {
|
||||
pub macro_markers_list_renderer: MacroMarkersListRenderer,
|
||||
|
@ -321,7 +323,7 @@ pub struct ChapterMarkersContent {
|
|||
|
||||
/// Chapter markers
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MacroMarkersListRenderer {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
|
@ -329,7 +331,7 @@ pub struct MacroMarkersListRenderer {
|
|||
}
|
||||
|
||||
/// Chapter marker
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MacroMarkersListItem {
|
||||
pub macro_markers_list_item_renderer: MacroMarkersListItemRenderer,
|
||||
|
@ -337,7 +339,7 @@ pub struct MacroMarkersListItem {
|
|||
|
||||
/// Chapter marker
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MacroMarkersListItemRenderer {
|
||||
/// Contains chapter start time in seconds
|
||||
|
@ -350,13 +352,13 @@ pub struct MacroMarkersListItemRenderer {
|
|||
}
|
||||
|
||||
/// Contains chapter start time in seconds
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MacroMarkersListItemOnTap {
|
||||
pub watch_endpoint: MacroMarkersListItemWatchEndpoint,
|
||||
}
|
||||
/// Contains chapter start time in seconds
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MacroMarkersListItemWatchEndpoint {
|
||||
/// Chapter start time in seconds
|
||||
|
@ -365,7 +367,7 @@ pub struct MacroMarkersListItemWatchEndpoint {
|
|||
|
||||
/// Comment section header
|
||||
/// (contains continuation tokens for fetching top/latest comments)
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentItemSectionHeader {
|
||||
pub engagement_panel_title_header_renderer: CommentItemSectionHeaderRenderer,
|
||||
|
@ -374,7 +376,7 @@ pub struct CommentItemSectionHeader {
|
|||
/// Comment section header
|
||||
/// (contains continuation tokens for fetching top/latest comments)
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentItemSectionHeaderRenderer {
|
||||
/// Approximate comment count (e.g. `81`, `2.2K`, `705K`)
|
||||
|
@ -388,7 +390,7 @@ pub struct CommentItemSectionHeaderRenderer {
|
|||
}
|
||||
|
||||
/// Comment section menu
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentItemSectionHeaderMenu {
|
||||
pub sort_filter_sub_menu_renderer: CommentItemSectionHeaderMenuRenderer,
|
||||
|
@ -399,14 +401,14 @@ pub struct CommentItemSectionHeaderMenu {
|
|||
/// Items:
|
||||
/// - Top comments
|
||||
/// - Latest comments
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentItemSectionHeaderMenuRenderer {
|
||||
pub sub_menu_items: Vec<CommentItemSectionHeaderMenuItem>,
|
||||
}
|
||||
|
||||
/// Comment section menu item
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentItemSectionHeaderMenuItem {
|
||||
/// Continuation token for fetching comments
|
||||
|
@ -418,14 +420,14 @@ pub struct CommentItemSectionHeaderMenuItem {
|
|||
*/
|
||||
|
||||
/// Video recommendations continuation response
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoRecommendations {
|
||||
pub on_response_received_endpoints: Vec<RecommendationsContItem>,
|
||||
}
|
||||
|
||||
/// Video recommendations continuation
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RecommendationsContItem {
|
||||
pub append_continuation_items_action: AppendRecommendations,
|
||||
|
@ -433,7 +435,7 @@ pub struct RecommendationsContItem {
|
|||
|
||||
/// Video recommendations continuation
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppendRecommendations {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
|
@ -446,7 +448,7 @@ pub struct AppendRecommendations {
|
|||
|
||||
/// Video comments continuation response
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoComments {
|
||||
/// - Initial response: 2*reloadContinuationItemsCommand
|
||||
|
@ -464,7 +466,7 @@ pub struct VideoComments {
|
|||
}
|
||||
|
||||
/// Video comments continuation
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentsContItem {
|
||||
#[serde(alias = "reloadContinuationItemsCommand")]
|
||||
|
@ -473,7 +475,7 @@ pub struct CommentsContItem {
|
|||
|
||||
/// Video comments continuation action
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppendComments {
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
|
@ -481,7 +483,7 @@ pub struct AppendComments {
|
|||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum CommentListItem {
|
||||
/// Top-level comment
|
||||
|
@ -511,14 +513,14 @@ pub enum CommentListItem {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Comment {
|
||||
pub comment_renderer: CommentRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentRenderer {
|
||||
/// Author name
|
||||
|
@ -544,24 +546,24 @@ pub struct CommentRenderer {
|
|||
// pub vote_count: Option<String>,
|
||||
pub author_comment_badge: Option<AuthorCommentBadge>,
|
||||
#[serde(default)]
|
||||
pub reply_count: u64,
|
||||
pub reply_count: u32,
|
||||
/// Buttons for comment interaction (Like/Dislike/Reply)
|
||||
pub action_buttons: CommentActionButtons,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthorEndpoint {
|
||||
pub browse_endpoint: BrowseEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BrowseEndpoint {
|
||||
pub browse_id: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum CommentPriority {
|
||||
/// Default rendering priority
|
||||
|
@ -573,7 +575,7 @@ pub enum CommentPriority {
|
|||
|
||||
/// Does not contain replies directly but a continuation token
|
||||
/// for fetching them.
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Replies {
|
||||
pub comment_replies_renderer: RepliesRenderer,
|
||||
|
@ -582,7 +584,7 @@ pub struct Replies {
|
|||
/// Does not contain replies directly but a continuation token
|
||||
/// for fetching them.
|
||||
#[serde_as]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RepliesRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
|
@ -591,7 +593,7 @@ pub struct RepliesRenderer {
|
|||
|
||||
/// These are the buttons for comment interaction (Like/Dislike/Reply).
|
||||
/// Contains the CreatorHeart.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentActionButtons {
|
||||
pub comment_action_buttons_renderer: CommentActionButtonsRenderer,
|
||||
|
@ -599,7 +601,7 @@ pub struct CommentActionButtons {
|
|||
|
||||
/// These are the buttons for comment interaction (Like/Dislike/Reply).
|
||||
/// Contains the CreatorHeart.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommentActionButtonsRenderer {
|
||||
pub like_button: ToggleButtonWrap,
|
||||
|
@ -607,27 +609,27 @@ pub struct CommentActionButtonsRenderer {
|
|||
}
|
||||
|
||||
/// Video creators can endorse comments by marking them with a ❤️.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatorHeart {
|
||||
pub creator_heart_renderer: CreatorHeartRenderer,
|
||||
}
|
||||
|
||||
/// Video creators can endorse comments by marking them with a ❤️.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatorHeartRenderer {
|
||||
pub is_hearted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthorCommentBadge {
|
||||
pub author_comment_badge_renderer: AuthorCommentBadgeRenderer,
|
||||
}
|
||||
|
||||
/// YouTube channel badge (verified) of the comment author
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthorCommentBadgeRenderer {
|
||||
/// Verified: `CHECK`
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
deobfuscate::Deobfuscator,
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
ChannelId, ChannelTag, Language, Paginator, SearchChannel, SearchItem, SearchPlaylist,
|
||||
SearchPlaylistVideo, SearchResult, SearchVideo,
|
||||
},
|
||||
timeago,
|
||||
util::{self, TryRemove},
|
||||
};
|
||||
|
||||
use super::{
|
||||
response::{self, IsLive, IsShort},
|
||||
ClientType, MapResponse, MapResult, QContinuation, RustyPipeQuery, YTContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QSearch<'a> {
|
||||
context: YTContext,
|
||||
query: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
params: Option<String>,
|
||||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
pub async fn search(self, query: &str) -> Result<SearchResult, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QSearch {
|
||||
context,
|
||||
query,
|
||||
params: None,
|
||||
};
|
||||
|
||||
self.execute_request::<response::Search, _, _>(
|
||||
ClientType::Desktop,
|
||||
"search",
|
||||
query,
|
||||
"search",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn search_continuation(self, ctoken: &str) -> Result<Paginator<SearchItem>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
};
|
||||
|
||||
self.execute_request::<response::SearchCont, _, _>(
|
||||
ClientType::Desktop,
|
||||
"search",
|
||||
ctoken,
|
||||
"search",
|
||||
&request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<SearchResult> for response::Search {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&Deobfuscator>,
|
||||
) -> Result<MapResult<SearchResult>, ExtractionError> {
|
||||
let section_list_items = self
|
||||
.contents
|
||||
.two_column_search_results_renderer
|
||||
.primary_contents
|
||||
.section_list_renderer
|
||||
.contents;
|
||||
|
||||
let (items, ctoken) = map_section_list_items(section_list_items)?;
|
||||
|
||||
let mut warnings = items.warnings;
|
||||
let (mut mapped, corrected_query) = map_search_items(items.c, lang);
|
||||
warnings.append(&mut mapped.warnings);
|
||||
|
||||
Ok(MapResult {
|
||||
c: SearchResult {
|
||||
items: Paginator::new(self.estimated_results, mapped.c, ctoken),
|
||||
corrected_query,
|
||||
},
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MapResponse<Paginator<SearchItem>> for response::SearchCont {
|
||||
fn map_response(
|
||||
self,
|
||||
_id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<SearchItem>>, ExtractionError> {
|
||||
let mut commands = self.on_response_received_commands;
|
||||
let cont_command = some_or_bail!(
|
||||
commands.try_swap_remove(0),
|
||||
Err(ExtractionError::InvalidData(
|
||||
"no item section renderer".into()
|
||||
))
|
||||
);
|
||||
|
||||
let (items, ctoken) = map_section_list_items(
|
||||
cont_command
|
||||
.append_continuation_items_action
|
||||
.continuation_items,
|
||||
)?;
|
||||
|
||||
let mut warnings = items.warnings;
|
||||
let (mut mapped, _) = map_search_items(items.c, lang);
|
||||
warnings.append(&mut mapped.warnings);
|
||||
|
||||
Ok(MapResult {
|
||||
c: Paginator::new(self.estimated_results, mapped.c, ctoken),
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_section_list_items(
|
||||
section_list_items: Vec<response::search::SectionListItem>,
|
||||
) -> Result<(MapResult<Vec<response::search::SearchItem>>, Option<String>), ExtractionError> {
|
||||
let mut items = None;
|
||||
let mut ctoken = None;
|
||||
section_list_items.into_iter().for_each(|item| match item {
|
||||
response::search::SectionListItem::ItemSectionRenderer { contents } => {
|
||||
items = Some(contents);
|
||||
}
|
||||
response::search::SectionListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||
}
|
||||
});
|
||||
|
||||
let items = some_or_bail!(
|
||||
items,
|
||||
Err(ExtractionError::InvalidData(
|
||||
"no item section renderer".into()
|
||||
))
|
||||
);
|
||||
|
||||
Ok((items, ctoken))
|
||||
}
|
||||
|
||||
fn map_search_items(
|
||||
items: Vec<response::search::SearchItem>,
|
||||
lang: Language,
|
||||
) -> (MapResult<Vec<SearchItem>>, Option<String>) {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let mut c_query = None;
|
||||
let mapped_items = items
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
response::search::SearchItem::VideoRenderer(mut video) => {
|
||||
match ChannelId::try_from(video.channel) {
|
||||
Ok(channel) => Some(SearchItem::Video(SearchVideo {
|
||||
id: video.video_id,
|
||||
title: video.title,
|
||||
length: video
|
||||
.length_text
|
||||
.and_then(|txt| util::parse_video_length_or_warn(&txt, &mut warnings)),
|
||||
thumbnail: video.thumbnail.into(),
|
||||
channel: ChannelTag {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
avatar: video
|
||||
.channel_thumbnail_supported_renderers
|
||||
.channel_thumbnail_with_link_renderer
|
||||
.thumbnail
|
||||
.into(),
|
||||
verification: video.owner_badges.into(),
|
||||
subscriber_count: None,
|
||||
},
|
||||
publish_date: video.published_time_text.as_ref().and_then(|txt| {
|
||||
timeago::parse_timeago_or_warn(lang, txt, &mut warnings)
|
||||
}),
|
||||
publish_date_txt: video.published_time_text,
|
||||
view_count: video
|
||||
.view_count_text
|
||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||
is_live: video.thumbnail_overlays.is_live(),
|
||||
is_short: video.thumbnail_overlays.is_short(),
|
||||
short_description: video
|
||||
.detailed_metadata_snippets
|
||||
.try_swap_remove(0)
|
||||
.map(|s| s.snippet_text)
|
||||
.unwrap_or_default(),
|
||||
})),
|
||||
Err(e) => {
|
||||
warnings.push(e.to_string());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
response::search::SearchItem::PlaylistRenderer(mut playlist) => {
|
||||
Some(SearchItem::Playlist(SearchPlaylist {
|
||||
id: playlist.playlist_id,
|
||||
name: playlist.title,
|
||||
thumbnail: playlist
|
||||
.thumbnails
|
||||
.try_swap_remove(0)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
video_count: playlist.video_count,
|
||||
first_videos: playlist
|
||||
.videos
|
||||
.into_iter()
|
||||
.map(|v| SearchPlaylistVideo {
|
||||
id: v.child_video_renderer.video_id,
|
||||
title: v.child_video_renderer.title,
|
||||
length: v.child_video_renderer.length_text.and_then(|txt| {
|
||||
util::parse_video_length_or_warn(&txt, &mut warnings)
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
response::search::SearchItem::ChannelRenderer(channel) => {
|
||||
Some(SearchItem::Channel(SearchChannel {
|
||||
id: channel.channel_id,
|
||||
name: channel.title,
|
||||
avatar: channel.thumbnail.into(),
|
||||
verification: channel.owner_badges.into(),
|
||||
subscriber_count: channel
|
||||
.subscriber_count_text
|
||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||
short_description: channel.description_snippet,
|
||||
}))
|
||||
}
|
||||
response::search::SearchItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
c_query = Some(corrected_query);
|
||||
None
|
||||
}
|
||||
response::search::SearchItem::None => None,
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
MapResult {
|
||||
c: mapped_items,
|
||||
warnings,
|
||||
},
|
||||
c_query,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::RustyPipe;
|
||||
|
||||
#[tokio::test]
|
||||
async fn t1() {
|
||||
let rp = RustyPipe::builder().strict().build();
|
||||
let result = rp.query().search("doobydoobap").await.unwrap();
|
||||
dbg!(&result);
|
||||
}
|
||||
}
|
|
@ -19,10 +19,10 @@ use super::{
|
|||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct QVideo<'a> {
|
||||
struct QVideo {
|
||||
context: YTContext,
|
||||
/// YouTube video ID
|
||||
video_id: &'a str,
|
||||
video_id: String,
|
||||
/// Set to true to allow extraction of streams with sensitive content
|
||||
content_check_ok: bool,
|
||||
/// Probably refers to allowing sensitive content, too
|
||||
|
@ -34,7 +34,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QVideo {
|
||||
context,
|
||||
video_id,
|
||||
video_id: video_id.to_owned(),
|
||||
content_check_ok: true,
|
||||
racy_check_ok: true,
|
||||
};
|
||||
|
@ -56,7 +56,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
continuation: ctoken.to_owned(),
|
||||
};
|
||||
|
||||
self.execute_request::<response::VideoRecommendations, _, _>(
|
||||
|
@ -73,7 +73,7 @@ impl RustyPipeQuery {
|
|||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
continuation: ctoken,
|
||||
continuation: ctoken.to_owned(),
|
||||
};
|
||||
|
||||
self.execute_request::<response::VideoComments, _, _>(
|
||||
|
@ -181,7 +181,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
};
|
||||
|
||||
let comment_count = comment_count_section.and_then(|s| {
|
||||
util::parse_large_numstr::<u64>(
|
||||
util::parse_large_numstr::<u32>(
|
||||
&s.comments_entry_point_header_renderer.comment_count,
|
||||
lang,
|
||||
)
|
||||
|
@ -411,7 +411,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
count_text,
|
||||
} => {
|
||||
comment_count = count_text.and_then(|txt| {
|
||||
util::parse_numeric_or_warn::<u64>(&txt, &mut warnings)
|
||||
util::parse_numeric_or_warn::<u32>(&txt, &mut warnings)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -56,12 +56,22 @@ fn parse_cr_header(cr_header: &str) -> Result<(u64, u64)> {
|
|||
);
|
||||
|
||||
Ok((
|
||||
captures.get(2).unwrap().as_str().parse().map_err(|_| {
|
||||
DownloadError::Progressive("could not parse range header number".into())
|
||||
})?,
|
||||
captures.get(3).unwrap().as_str().parse().map_err(|_| {
|
||||
DownloadError::Progressive("could not parse range header number".into())
|
||||
})?,
|
||||
captures
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse()
|
||||
.or(Err(DownloadError::Progressive(
|
||||
"could not parse range header number".into(),
|
||||
)))?,
|
||||
captures
|
||||
.get(3)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse()
|
||||
.or(Err(DownloadError::Progressive(
|
||||
"could not parse range header number".into(),
|
||||
)))?,
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -86,7 +96,7 @@ async fn download_single_file<P: Into<PathBuf>>(
|
|||
|
||||
// If the url is from googlevideo, extract file size from clen parameter
|
||||
let (url_base, url_params) =
|
||||
util::url_to_params(url).map_err(|e| DownloadError::Other(e.to_string().into()))?;
|
||||
util::url_to_params(url).or_else(|e| Err(DownloadError::Other(e.to_string().into())))?;
|
||||
let is_gvideo = url_base.ends_with(".googlevideo.com/videoplayback");
|
||||
if is_gvideo {
|
||||
size = url_params.get("clen").and_then(|s| s.parse::<u64>().ok());
|
||||
|
@ -110,9 +120,9 @@ async fn download_single_file<P: Into<PathBuf>>(
|
|||
))
|
||||
)
|
||||
.to_str()
|
||||
.map_err(|_| {
|
||||
DownloadError::Progressive("could not convert Content-Range header to string".into())
|
||||
})?;
|
||||
.or(Err(DownloadError::Progressive(
|
||||
"could not convert Content-Range header to string".into(),
|
||||
)))?;
|
||||
|
||||
let (_, original_size) = parse_cr_header(cr_header)?;
|
||||
|
||||
|
@ -197,9 +207,9 @@ async fn download_chunks_by_header(
|
|||
))
|
||||
)
|
||||
.to_str()
|
||||
.map_err(|_| {
|
||||
DownloadError::Progressive("could not convert Content-Range header to string".into())
|
||||
})?;
|
||||
.or(Err(DownloadError::Progressive(
|
||||
"could not convert Content-Range header to string".into(),
|
||||
)))?;
|
||||
|
||||
let (parsed_offset, parsed_size) = parse_cr_header(cr_header)?;
|
||||
|
||||
|
|
|
@ -81,8 +81,8 @@ pub enum ExtractionError {
|
|||
InvalidData(Cow<'static, str>),
|
||||
#[error("got wrong result from YT: {0}")]
|
||||
WrongResult(String),
|
||||
#[error("Warnings during deserialization/mapping in strict mode")]
|
||||
DeserializationWarnings,
|
||||
#[error("Warnings during deserialization/mapping")]
|
||||
Warnings,
|
||||
}
|
||||
|
||||
/// Internal error
|
||||
|
|
127
src/model/mod.rs
127
src/model/mod.rs
|
@ -393,7 +393,7 @@ pub struct Playlist {
|
|||
/// Playlist videos
|
||||
pub videos: Paginator<PlaylistVideo>,
|
||||
/// Number of videos in the playlist
|
||||
pub video_count: u64,
|
||||
pub video_count: u32,
|
||||
/// Playlist thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Playlist description in plaintext format
|
||||
|
@ -453,11 +453,11 @@ pub struct VideoDetails {
|
|||
pub view_count: u64,
|
||||
/// Number of likes
|
||||
///
|
||||
/// [`None`] if the like count was hidden by the creator.
|
||||
/// `None` if the like count was hidden by the creator.
|
||||
pub like_count: Option<u32>,
|
||||
/// Video publishing date. Start date in case of a livestream.
|
||||
///
|
||||
/// [`None`] if the date could not be parsed.
|
||||
/// `None` if the date could not be parsed.
|
||||
pub publish_date: Option<DateTime<Local>>,
|
||||
/// Textual video publishing date (e.g. `Aug 2, 2013`, depends on language)
|
||||
pub publish_date_txt: String,
|
||||
|
@ -516,7 +516,7 @@ pub struct RecommendedVideo {
|
|||
pub title: String,
|
||||
/// Video length in seconds.
|
||||
///
|
||||
/// Is [`None`] for livestreams.
|
||||
/// Is `None` for livestreams.
|
||||
pub length: Option<u32>,
|
||||
/// Video thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
|
@ -524,15 +524,15 @@ pub struct RecommendedVideo {
|
|||
pub channel: ChannelTag,
|
||||
/// Video publishing date.
|
||||
///
|
||||
/// [`None`] if the date could not be parsed.
|
||||
/// `None` if the date could not be parsed.
|
||||
pub publish_date: Option<DateTime<Local>>,
|
||||
/// Textual video publish date (e.g. `11 months ago`, depends on language)
|
||||
///
|
||||
/// Is [`None`] for livestreams.
|
||||
/// Is `None` for livestreams.
|
||||
pub publish_date_txt: Option<String>,
|
||||
/// View count
|
||||
///
|
||||
/// [`None`] if it could not be extracted.
|
||||
/// `None` if it could not be extracted.
|
||||
pub view_count: Option<u64>,
|
||||
/// Is the video an active livestream?
|
||||
pub is_live: bool,
|
||||
|
@ -554,7 +554,7 @@ pub struct ChannelTag {
|
|||
pub verification: Verification,
|
||||
/// Approximate number of subscribers
|
||||
///
|
||||
/// [`None`] if hidden by the owner or not present.
|
||||
/// `None` if hidden by the owner or not present.
|
||||
///
|
||||
/// Info: This is only present in the `VideoDetails` response
|
||||
pub subscriber_count: Option<u64>,
|
||||
|
@ -600,14 +600,14 @@ pub struct Comment {
|
|||
pub author: Option<ChannelTag>,
|
||||
/// Comment publishing date.
|
||||
///
|
||||
/// [`None`] if the date could not be parsed.
|
||||
/// `None` if the date could not be parsed.
|
||||
pub publish_date: Option<DateTime<Local>>,
|
||||
/// Textual comment publish date (e.g. `14 hours ago`), depends on language setting
|
||||
pub publish_date_txt: String,
|
||||
/// Number of comment likes
|
||||
pub like_count: Option<u32>,
|
||||
/// Number of replies
|
||||
pub reply_count: u64,
|
||||
pub reply_count: u32,
|
||||
/// Paginator to fetch comment replies
|
||||
pub replies: Paginator<Comment>,
|
||||
/// Is the comment from the channel owner?
|
||||
|
@ -635,7 +635,7 @@ pub struct Channel<T> {
|
|||
pub name: String,
|
||||
/// Channel subscriber count
|
||||
///
|
||||
/// [`None`] if the subscriber count was hidden by the owner
|
||||
/// `None` if the subscriber count was hidden by the owner
|
||||
/// or could not be parsed.
|
||||
pub subscriber_count: Option<u64>,
|
||||
/// Channel avatar / profile picture
|
||||
|
@ -667,22 +667,22 @@ pub struct ChannelVideo {
|
|||
pub title: String,
|
||||
/// Video length in seconds.
|
||||
///
|
||||
/// Is [`None`] for livestreams.
|
||||
/// Is `None` for livestreams.
|
||||
pub length: Option<u32>,
|
||||
/// Video thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Video publishing date.
|
||||
///
|
||||
/// [`None`] if the date could not be parsed.
|
||||
/// `None` if the date could not be parsed.
|
||||
/// May be in the future for upcoming videos
|
||||
pub publish_date: Option<DateTime<Local>>,
|
||||
/// Textual video publish date (e.g. `11 months ago`, depends on language)
|
||||
///
|
||||
/// Is [`None`] for livestreams and upcoming videos.
|
||||
/// Is `None` for livestreams and upcoming videos.
|
||||
pub publish_date_txt: Option<String>,
|
||||
/// Number of views / current viewers in case of a livestream.
|
||||
///
|
||||
/// [`None`] if it could not be extracted.
|
||||
/// `None` if it could not be extracted.
|
||||
pub view_count: Option<u64>,
|
||||
/// Is the video an active livestream?
|
||||
pub is_live: bool,
|
||||
|
@ -703,7 +703,7 @@ pub struct ChannelPlaylist {
|
|||
/// Playlist thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Number of playlist videos
|
||||
pub video_count: Option<u64>,
|
||||
pub video_count: Option<u32>,
|
||||
}
|
||||
|
||||
/// Additional channel metadata fetched from the "About" tab.
|
||||
|
@ -753,98 +753,5 @@ pub struct ChannelRssVideo {
|
|||
/// Number of likes
|
||||
///
|
||||
/// Zero if the like count was hidden by the creator.
|
||||
pub like_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SearchResult {
|
||||
pub items: Paginator<SearchItem>,
|
||||
pub corrected_query: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum SearchItem {
|
||||
Video(SearchVideo),
|
||||
Playlist(SearchPlaylist),
|
||||
Channel(SearchChannel),
|
||||
}
|
||||
|
||||
/// YouTube video from the search results
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct SearchVideo {
|
||||
/// Unique YouTube video ID
|
||||
pub id: String,
|
||||
/// Video title
|
||||
pub title: String,
|
||||
/// Video length in seconds.
|
||||
///
|
||||
/// Is [`None`] for livestreams.
|
||||
pub length: Option<u32>,
|
||||
/// Video thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Channel of the video
|
||||
pub channel: ChannelTag,
|
||||
/// Video publishing date.
|
||||
///
|
||||
/// [`None`] if the date could not be parsed.
|
||||
pub publish_date: Option<DateTime<Local>>,
|
||||
/// Textual video publish date (e.g. `11 months ago`, depends on language)
|
||||
///
|
||||
/// Is [`None`] for livestreams.
|
||||
pub publish_date_txt: Option<String>,
|
||||
/// View count
|
||||
///
|
||||
/// [`None`] if it could not be extracted.
|
||||
pub view_count: Option<u64>,
|
||||
/// Is the video an active livestream?
|
||||
pub is_live: bool,
|
||||
/// Is the video a YouTube Short video (vertical and <60s)?
|
||||
pub is_short: bool,
|
||||
/// Abbreviated video description
|
||||
pub short_description: String,
|
||||
}
|
||||
|
||||
/// Playlist from the search results
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct SearchPlaylist {
|
||||
/// Unique YouTube Playlist-ID (e.g. `PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ`)
|
||||
pub id: String,
|
||||
/// Playlist name
|
||||
pub name: String,
|
||||
/// Playlist thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Number of playlist videos
|
||||
pub video_count: u64,
|
||||
/// First 2 videos
|
||||
pub first_videos: Vec<SearchPlaylistVideo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct SearchPlaylistVideo {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub length: Option<u32>,
|
||||
}
|
||||
|
||||
/// Channel from the search results
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct SearchChannel {
|
||||
/// Unique YouTube channel ID
|
||||
pub id: String,
|
||||
/// Channel name
|
||||
pub name: String,
|
||||
/// Channel avatar/profile picture
|
||||
pub avatar: Vec<Thumbnail>,
|
||||
/// Channel verification mark
|
||||
pub verification: Verification,
|
||||
/// Approximate number of subscribers
|
||||
///
|
||||
/// [`None`] if hidden by the owner or not present.
|
||||
pub subscriber_count: Option<u64>,
|
||||
/// Abbreviated channel description
|
||||
pub short_description: String,
|
||||
pub like_count: u32,
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ pub struct Paginator<T> {
|
|||
///
|
||||
/// Don't use this number to check if all items were fetched or for
|
||||
/// iterating over the items.
|
||||
pub count: Option<u64>,
|
||||
pub count: Option<u32>,
|
||||
/// Content of the paginator
|
||||
pub items: Vec<T>,
|
||||
/// The continuation token is passed to the YouTube API to fetch
|
||||
|
@ -41,7 +41,7 @@ impl<T> Default for Paginator<T> {
|
|||
}
|
||||
|
||||
impl<T> Paginator<T> {
|
||||
pub(crate) fn new(count: Option<u64>, items: Vec<T>, ctoken: Option<String>) -> Self {
|
||||
pub(crate) fn new(count: Option<u32>, items: Vec<T>, ctoken: Option<String>) -> Self {
|
||||
Self {
|
||||
count: match ctoken {
|
||||
Some(_) => count,
|
||||
|
|
|
@ -98,8 +98,11 @@ impl FileReporter {
|
|||
|
||||
fn _report(&self, report: &Report) -> Result<()> {
|
||||
let report_path = get_report_path(&self.path, report, "json")?;
|
||||
serde_json::to_writer_pretty(&File::create(report_path)?, &report)
|
||||
.map_err(|e| Error::Other(format!("could not serialize report. err: {}", e).into()))?;
|
||||
serde_json::to_writer_pretty(&File::create(report_path)?, &report).or_else(|e| {
|
||||
Err(Error::Other(
|
||||
format!("could not serialize report. err: {}", e).into(),
|
||||
))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,18 +305,23 @@ impl<'de> DeserializeAs<'de, TextComponents> for AttributedText {
|
|||
}
|
||||
|
||||
let mut buf = String::with_capacity(until - i_utf16);
|
||||
for c in chars.by_ref() {
|
||||
buf.push(c);
|
||||
loop {
|
||||
match chars.next() {
|
||||
Some(c) => {
|
||||
buf.push(c);
|
||||
|
||||
// is character on Basic Multilingual Plane -> 16bit in UTF-16,
|
||||
// counts as 1 JS character, otherwise 32bit, counts as 2 JS characters
|
||||
if (c as u32) > 0xffff {
|
||||
i_utf16 += 1;
|
||||
};
|
||||
i_utf16 += 1;
|
||||
// is character on Basic Multilingual Plane -> 16bit in UTF-16,
|
||||
// counts as 1 JS character, otherwise 32bit, counts as 2 JS characters
|
||||
if (c as u32) > 0xffff {
|
||||
i_utf16 += 1;
|
||||
};
|
||||
i_utf16 += 1;
|
||||
|
||||
if i_utf16 >= until {
|
||||
break;
|
||||
if i_utf16 >= until {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
buf
|
||||
|
@ -334,7 +339,7 @@ impl<'de> DeserializeAs<'de, TextComponents> for AttributedText {
|
|||
|
||||
// Replace no-break spaces, trim off whitespace and prefix character
|
||||
let txt_link = txt_link.trim();
|
||||
let txt_link = txt_link.replace('\u{a0}', " ");
|
||||
let txt_link = txt_link.replace("\u{a0}", " ");
|
||||
|
||||
static LINK_PREFIX: Lazy<Regex> = Lazy::new(|| Regex::new("^[/•] *").unwrap());
|
||||
let txt_link = LINK_PREFIX.replace(&txt_link, "");
|
||||
|
|
|
@ -43,8 +43,11 @@ pub fn generate_content_playback_nonce() -> String {
|
|||
///
|
||||
/// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}`
|
||||
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>)> {
|
||||
let mut parsed_url = Url::parse(url)
|
||||
.map_err(|e| Error::Other(format!("could not parse url `{}` err: {}", url, e).into()))?;
|
||||
let mut parsed_url = Url::parse(url).or_else(|e| {
|
||||
Err(Error::Other(
|
||||
format!("could not parse url `{}` err: {}", url, e).into(),
|
||||
))
|
||||
})?;
|
||||
let url_params: BTreeMap<String, String> = parsed_url
|
||||
.query_pairs()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
Loading…
Add table
Reference in a new issue