feat: add channel_videos

refactor: unify VideoListItem
This commit is contained in:
ThetaDev 2022-09-22 00:01:09 +02:00
parent 86a348f210
commit 67ae1eb21d
19 changed files with 39591 additions and 100 deletions

83
src/client/channel.rs Normal file
View file

@ -0,0 +1,83 @@
use anyhow::{bail, Result};
use reqwest::Method;
use serde::Serialize;
use crate::{model::ChannelVideos, serializer::MapResult};
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QChannel {
context: YTContext,
browse_id: String,
params: Params,
}
#[derive(Debug, Serialize)]
enum Params {
#[serde(rename = "EgZ2aWRlb3PyBgQKAjoA")]
VideosLatest,
#[serde(rename = "EgZ2aWRlb3MYAiAAMAE%3D")]
VideosOldest,
#[serde(rename = "EgZ2aWRlb3MYASAAMAE%3D")]
VideosPopular,
#[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
Playlists,
#[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
Info,
}
impl RustyPipeQuery {
pub async fn channel_videos(&self, channel_id: &str) -> Result<ChannelVideos> {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QChannel {
context,
browse_id: channel_id.to_owned(),
params: Params::VideosLatest,
};
self.execute_request::<response::Channel, _, _>(
ClientType::Desktop,
"channel_videos",
channel_id,
Method::POST,
"browse",
&request_body,
)
.await
}
}
impl MapResponse<ChannelVideos> for response::Channel {
fn map_response(
self,
id: &str,
_lang: crate::model::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<ChannelVideos>> {
let warnings = Vec::new();
// dbg!(&self);
let header = self.header.c4_tabbed_header_renderer;
if header.channel_id != id {
bail!(
"got wrong channel id {}, expected {}",
header.channel_id,
id
);
}
Ok(MapResult {
c: ChannelVideos {
id: header.channel_id,
name: header.title,
subscriber_count_txt: header.subscriber_count_text,
},
warnings,
})
}
}
#[cfg(test)]
mod tests {}

View file

@ -1,9 +1,9 @@
pub mod pagination;
pub mod player;
pub mod playlist;
pub mod video_details;
mod channel;
mod pagination;
mod player;
mod playlist;
mod response;
mod video_details;
use std::fmt::Debug;
use std::sync::Arc;

View file

@ -24,7 +24,7 @@ use super::{
ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext,
};
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlayer {
context: YTContext,
@ -42,13 +42,13 @@ struct QPlayer {
racy_check_ok: bool,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlaybackContext {
content_playback_context: QContentPlaybackContext,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QContentPlaybackContext {
/// Signature timestamp extracted from player.js

View file

@ -13,14 +13,14 @@ use crate::{
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlaylist {
context: YTContext,
browse_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlaylistCont {
context: YTContext,
@ -206,14 +206,12 @@ impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
}
}
fn map_playlist_items(
items: Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
) -> (Vec<PlaylistVideo>, Option<String>) {
fn map_playlist_items(items: Vec<response::VideoListItem>) -> (Vec<PlaylistVideo>, Option<String>) {
let mut ctoken: Option<String> = None;
let videos = items
.into_iter()
.filter_map(|it| match it {
response::VideoListItem::GridVideoRenderer { video } => {
response::VideoListItem::PlaylistVideoRenderer(video) => {
match ChannelId::try_from(video.channel) {
Ok(channel) => Some(PlaylistVideo {
id: video.video_id,
@ -231,7 +229,7 @@ fn map_playlist_items(
ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
response::VideoListItem::None => None,
_ => None,
})
.collect::<Vec<_>>();
(videos, ctoken)

View file

@ -2,13 +2,15 @@ use serde::Deserialize;
use serde_with::serde_as;
use serde_with::VecSkipError;
use super::TimeOverlay;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
use crate::serializer::text::Text;
use super::ChannelBadge;
use super::Thumbnails;
use super::{ContentRenderer, ContentsRenderer, VideoListItem};
use crate::serializer::{text::Text, MapResult, VecLogError};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Channel {
pub header: Header,
pub contents: Contents,
}
@ -18,6 +20,8 @@ pub struct Contents {
pub two_column_browse_results_renderer: TabsRenderer,
}
/// YouTube channel tab view. Contains multiple tabs
/// (Home, Videos, Playlists, About...). We can ignore unknown tabs.
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -35,7 +39,14 @@ pub struct TabRendererWrap {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectionListRendererWrap {
pub section_list_renderer: ContentsRenderer<ItemSectionRendererWrap>,
pub section_list_renderer: SectionListRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectionListRenderer {
pub contents: Vec<ItemSectionRendererWrap>,
pub target_id: String,
}
#[derive(Clone, Debug, Deserialize)]
@ -54,22 +65,31 @@ pub struct GridRendererWrap {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GridRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub items: Vec<VideoListItem<ChannelVideo>>,
#[serde_as(as = "VecLogError<_>")]
pub items: MapResult<Vec<VideoListItem>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Header {
pub c4_tabbed_header_renderer: HeaderRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub struct HeaderRenderer {
pub channel_id: String,
/// Channel name
pub title: String,
#[serde_as(as = "Option<Text>")]
pub published_time_text: Option<String>,
/// Approximate subscriber count (e.g. `880K subscribers`), depends on language
#[serde_as(as = "Text")]
pub view_count_text: String,
pub subscriber_count_text: String,
pub avatar: Thumbnails,
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlay>,
pub badges: Vec<ChannelBadge>,
pub banner: Thumbnails,
pub mobile_banner: Thumbnails,
/// Fullscreen (16:9) channel banner
pub tv_banner: Thumbnails,
}

View file

@ -14,7 +14,7 @@ pub use video_details::VideoDetails;
pub use video_details::VideoRecommendations;
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError};
use crate::serializer::{
ignore_any,
@ -40,6 +40,8 @@ pub struct ThumbnailsWrap {
pub thumbnail: Thumbnails,
}
/// List of images in different resolutions.
/// Not only used for thumbnails, but also for avatars and banners.
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Thumbnails {
@ -54,27 +56,107 @@ pub struct Thumbnail {
pub height: u32,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VideoListItem<T> {
#[serde(alias = "playlistVideoRenderer", alias = "compactVideoRenderer")]
GridVideoRenderer {
#[serde(flatten)]
video: T,
},
pub enum VideoListItem {
GridVideoRenderer(GridVideoRenderer),
CompactVideoRenderer(CompactVideoRenderer),
PlaylistVideoRenderer(PlaylistVideoRenderer),
GridPlaylistRenderer(GridPlaylistRenderer),
/// Continauation items are located at the end of a list
/// and contain the continuation token for progressive loading
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
/// No video list item (e.g. ad)
/// No video list item (e.g. ad) or unimplemented item
///
/// Note that there are sometimes playlists among the recommended
/// videos. They are currently ignored.
/// Unimplemented:
/// - compactPlaylistRenderer (recommended playlists)
/// - compactRadioRenderer (recommended mix)
#[serde(other, deserialize_with = "ignore_any")]
None,
}
/// Video displayed on a channel page
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GridVideoRenderer {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde_as(as = "Option<Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "Option<Text>")]
pub view_count_text: Option<String>,
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlay>,
}
/// Video displayed in recommendations
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompactVideoRenderer {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
pub channel: TextComponent,
pub channel_thumbnail: Thumbnails,
/// Channel verification badge
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub owner_badges: Vec<ChannelBadge>,
#[serde_as(as = "Option<Text>")]
pub length_text: Option<String>,
/// (e.g. `11 months ago`)
#[serde_as(as = "Option<Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "Option<Text>")]
pub view_count_text: Option<String>,
/// Badges are displayed on the video thumbnail and
/// show certain video properties (e.g. active livestream)
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub badges: Vec<VideoBadge>,
}
/// Video displayed in a playlist
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideoRenderer {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
pub channel: TextComponent,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
}
/// Playlist displayed on a channel page
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GridPlaylistRenderer {
pub playlist_id: String,
#[serde_as(as = "Text")]
pub title: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub published_time_text: String,
#[serde_as(as = "Text")]
pub video_count_short_text: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContinuationItemRenderer {

View file

@ -1,11 +1,11 @@
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use serde_with::{DefaultOnError, VecSkipError};
use crate::serializer::text::{Text, TextComponent};
use crate::serializer::{MapResult, VecLogError};
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
use super::{ContentRenderer, ContentsRenderer, ThumbnailsWrap, VideoListItem};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -58,21 +58,7 @@ pub struct PlaylistVideoListRenderer {
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideoList {
#[serde_as(as = "VecLogError<_>")]
pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
pub channel: TextComponent,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
pub contents: MapResult<Vec<VideoListItem>>,
}
#[derive(Clone, Debug, Deserialize)]
@ -172,5 +158,5 @@ pub struct OnResponseReceivedAction {
#[serde(rename_all = "camelCase")]
pub struct AppendAction {
#[serde_as(as = "VecLogError<_>")]
pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
pub continuation_items: MapResult<Vec<VideoListItem>>,
}

View file

@ -8,13 +8,12 @@ use crate::serializer::text::TextComponents;
use crate::serializer::MapResult;
use crate::serializer::{
ignore_any,
text::{AccessibilityText, Text, TextComponent},
text::{AccessibilityText, Text},
VecLogError,
};
use super::{
ChannelBadge, ContinuationEndpoint, ContinuationItemRenderer, Icon, Thumbnails, VideoBadge,
VideoListItem, VideoOwner,
ContinuationEndpoint, ContinuationItemRenderer, Icon, Thumbnails, VideoListItem, VideoOwner,
};
/*
@ -283,37 +282,7 @@ pub struct RecommendationResultsWrap {
pub struct RecommendationResults {
/// Can be `None` for age-restricted videos
#[serde_as(as = "Option<VecLogError<_>>")]
pub results: Option<MapResult<Vec<VideoListItem<RecommendedVideo>>>>,
}
/// Video recommendation item
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendedVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
pub channel: TextComponent,
pub channel_thumbnail: Thumbnails,
/// Channel verification badge
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub owner_badges: Vec<ChannelBadge>,
#[serde_as(as = "Option<Text>")]
pub length_text: Option<String>,
/// (e.g. `11 months ago`)
#[serde_as(as = "Option<Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "Option<Text>")]
pub view_count_text: Option<String>,
/// Badges are displayed on the video thumbnail and
/// show certain video properties (e.g. active livestream)
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub badges: Vec<VideoBadge>,
pub results: Option<MapResult<Vec<VideoListItem>>>,
}
/// The engagement panels are displayed below the video and contain chapter markers
@ -468,7 +437,7 @@ pub struct RecommendationsContItem {
#[serde(rename_all = "camelCase")]
pub struct AppendRecommendations {
#[serde_as(as = "VecLogError<_>")]
pub continuation_items: MapResult<Vec<VideoListItem<RecommendedVideo>>>,
pub continuation_items: MapResult<Vec<VideoListItem>>,
}
/*

View file

@ -18,7 +18,7 @@ use super::{
ClientType, MapResponse, RustyPipeQuery, YTContext,
};
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
struct QVideo {
context: YTContext,
/// YouTube video ID
@ -29,7 +29,7 @@ struct QVideo {
racy_check_ok: bool,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Debug, Serialize)]
struct QVideoCont {
context: YTContext,
continuation: String,
@ -407,7 +407,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
}
fn map_recommendations(
r: MapResult<Vec<response::VideoListItem<response::video_details::RecommendedVideo>>>,
r: MapResult<Vec<response::VideoListItem>>,
lang: Language,
) -> MapResult<Paginator<RecommendedVideo>> {
let mut warnings = r.warnings;
@ -416,7 +416,7 @@ fn map_recommendations(
let items =
r.c.into_iter()
.filter_map(|item| match item {
response::VideoListItem::GridVideoRenderer { video } => {
response::VideoListItem::CompactVideoRenderer(video) => {
match ChannelId::try_from(video.channel) {
Ok(channel) => Some(RecommendedVideo {
id: video.video_id,
@ -454,7 +454,7 @@ fn map_recommendations(
ctoken = Some(continuation_endpoint.continuation_command.token);
None
}
response::VideoListItem::None => None,
_ => None,
})
.collect::<Vec<_>>();

View file

@ -23,6 +23,7 @@ pub trait FileFormat {
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VideoPlayer {
pub details: VideoPlayerDetails,
pub video_streams: Vec<VideoStream>,
@ -33,6 +34,7 @@ pub struct VideoPlayer {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoPlayerDetails {
pub id: String,
pub title: String,
@ -49,6 +51,7 @@ pub struct VideoPlayerDetails {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoStream {
pub url: String,
pub itag: u32,
@ -69,6 +72,7 @@ pub struct VideoStream {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct AudioStream {
pub url: String,
pub itag: u32,
@ -128,6 +132,7 @@ pub enum VideoFormat {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct AudioTrack {
pub id: String,
pub lang: Option<String>,
@ -163,6 +168,7 @@ impl FileFormat for AudioFormat {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Thumbnail {
pub url: String,
pub width: u32,
@ -170,6 +176,7 @@ pub struct Thumbnail {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Subtitle {
pub url: String,
pub lang: String,
@ -182,6 +189,7 @@ pub struct Subtitle {
*/
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Playlist {
pub id: String,
pub name: String,
@ -195,6 +203,7 @@ pub struct Playlist {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct PlaylistVideo {
pub id: String,
pub title: String,
@ -204,6 +213,7 @@ pub struct PlaylistVideo {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelId {
pub id: String,
pub name: String,
@ -214,6 +224,7 @@ pub struct ChannelId {
*/
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoDetails {
/// Unique YouTube video ID
pub id: String,
@ -260,6 +271,7 @@ pub struct VideoDetails {
/// Videos can consist of different chapters, which YouTube shows
/// on the seek bar and below the description text.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Chapter {
/// Chapter title
pub title: String,
@ -274,6 +286,7 @@ pub struct Chapter {
*/
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct RecommendedVideo {
/// Unique YouTube video ID
pub id: String,
@ -304,6 +317,7 @@ pub struct RecommendedVideo {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Channel {
/// Unique YouTube channel ID
pub id: String,
@ -327,6 +341,7 @@ pub struct Channel {
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Verification {
#[default]
/// Unverified channel (default)
@ -343,8 +358,8 @@ impl Verification {
}
}
// TODO: impl popularity comparison
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Comment {
/// Unique YouTube Comment-ID (e.g. `UgynScMrsqGSL8qvePl4AaABAg`)
pub id: String,
@ -373,3 +388,18 @@ pub struct Comment {
/// Has the channel owner marked the comment with a ❤️ heart ?
pub hearted: bool,
}
/*
#CHANNEL
*/
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelVideos {
/// Unique YouTube Channel-ID (e.g. `UC-lHJZR3Gqxm24_Vd_AJ5Yw`)
pub id: String,
/// Channel name
pub name: String,
/// Textual subscriber count (e.g. `2.3M subscribers`), depends on language setting
pub subscriber_count_txt: String,
}

View file

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
/// in pages from the YouTube API (e.g. playlist items,
/// video recommendations or comments).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Paginator<T> {
/// Total number of items if finite and known.
///

View file

@ -1,9 +1,11 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct RichText(pub Vec<TextComponent>);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum TextComponent {
/// Plain text
Text(String),