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

View file

@ -10,7 +10,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
- [X] **Player** (video/audio streams, subtitles)
- TODO: Livestream support
- [X] **Playlist**
- [ ] **VideoDetails** (metadata, comments, recommended videos)
- [X] **VideoDetails** (metadata, comments, recommended videos)
- [ ] **Channel**
- [ ] **ChannelRSS**
- [ ] **Search**

View file

@ -18,6 +18,7 @@ pub async fn download_testfiles(project_root: &Path) {
playlist(&testfiles),
video_details(&testfiles),
comments_top(&testfiles),
channel_videos(&testfiles),
);
}
@ -159,3 +160,22 @@ async fn comments_top(testfiles: &Path) {
.await
.unwrap();
}
async fn channel_videos(testfiles: &Path) {
for (name, id) in [
("base", "UC2DjFE7Xf11URZqWBigcVOQ"),
("music", "UC_vmjW5e1xEHhYjY2a0kK1A"), // YouTube Music channels have no videos
("shorts", "UCh8gHdtzO2tXd593_bjErWg"), // shorts and livestreams are rendered differently
("live", "UChs0pSaEoNLV4mevBFGaoKA"),
] {
let mut json_path = testfiles.to_path_buf();
json_path.push("channel");
json_path.push(format!("channel_videos_{}.json", name));
if json_path.exists() {
continue;
}
let rp = rp_testfile(&json_path);
rp.query().channel_videos(id).await.unwrap();
}
}

View file

@ -52,3 +52,13 @@ Sep PL1J-6JOckZtHVs0JhBW_qfsW-dtXuM0mQ 16.09.2018
Oct PL1J-6JOckZtE4g-XgZkL_N0kkoKui5Eys 31.10.2014
Nov PL1J-6JOckZtEzjMUEyPyPpG836pjeIapw 03.11.2016
Dec PL1J-6JOckZtHo91uApeb10Qlf2XhkfM-9 24.12.2021
# Channels
10e8: 225M UCq-Fj5jknLsUf-MWSy4_brA
10e7: 52M UC0C-w0YjGpqDXGB8IHb662A
10e6: 1.7M UC6mIxFTvXkWQVEHPsEdflzQ
10e5: 125K UCD0y51PJfvkZNe3y3FR5riw
10e4: 27K UCNcN0dW43zE0Om3278fjY8A
10e3: 5K UCNcN0dW43zE0Om3278fjY8A
10e2: 388 UCllyEQfcoiPN68zHv6mGHDQ
10e1: 37 UCNcN0dW43zE0Om3278fjY8A

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),

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff