371 lines
10 KiB
Rust
371 lines
10 KiB
Rust
pub(crate) mod channel;
|
|
pub(crate) mod music_artist;
|
|
pub(crate) mod music_charts;
|
|
pub(crate) mod music_details;
|
|
pub(crate) mod music_genres;
|
|
pub(crate) mod music_item;
|
|
pub(crate) mod music_new;
|
|
pub(crate) mod music_playlist;
|
|
pub(crate) mod music_search;
|
|
pub(crate) mod player;
|
|
pub(crate) mod playlist;
|
|
pub(crate) mod search;
|
|
pub(crate) mod trends;
|
|
pub(crate) mod url_endpoint;
|
|
pub(crate) mod video_details;
|
|
pub(crate) mod video_item;
|
|
|
|
pub(crate) use channel::Channel;
|
|
pub(crate) use channel::ChannelAbout;
|
|
pub(crate) use music_artist::MusicArtist;
|
|
pub(crate) use music_artist::MusicArtistAlbums;
|
|
pub(crate) use music_charts::MusicCharts;
|
|
pub(crate) use music_details::MusicDetails;
|
|
pub(crate) use music_details::MusicLyrics;
|
|
pub(crate) use music_details::MusicRelated;
|
|
pub(crate) use music_genres::MusicGenre;
|
|
pub(crate) use music_genres::MusicGenres;
|
|
pub(crate) use music_item::MusicContinuation;
|
|
pub(crate) use music_new::MusicNew;
|
|
pub(crate) use music_playlist::MusicPlaylist;
|
|
pub(crate) use music_search::MusicSearch;
|
|
pub(crate) use music_search::MusicSearchSuggestion;
|
|
pub(crate) use player::Player;
|
|
pub(crate) use playlist::Playlist;
|
|
pub(crate) use search::Search;
|
|
pub(crate) use search::SearchSuggestion;
|
|
pub(crate) use trends::Startpage;
|
|
pub(crate) use trends::Trending;
|
|
pub(crate) use url_endpoint::ResolvedUrl;
|
|
pub(crate) use video_details::VideoComments;
|
|
pub(crate) use video_details::VideoDetails;
|
|
pub(crate) use video_item::YouTubeListItem;
|
|
pub(crate) use video_item::YouTubeListMapper;
|
|
|
|
#[cfg(feature = "rss")]
|
|
pub(crate) mod channel_rss;
|
|
#[cfg(feature = "rss")]
|
|
pub(crate) use channel_rss::ChannelRss;
|
|
|
|
use std::borrow::Cow;
|
|
use std::marker::PhantomData;
|
|
|
|
use serde::{
|
|
de::{IgnoredAny, Visitor},
|
|
Deserialize,
|
|
};
|
|
use serde_with::{serde_as, DisplayFromStr, VecSkipError};
|
|
|
|
use crate::error::ExtractionError;
|
|
use crate::serializer::{text::Text, MapResult, VecSkipErrorWrap};
|
|
|
|
use self::video_item::YouTubeListRenderer;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ContentRenderer<T> {
|
|
pub content: T,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct ContentsRenderer<T> {
|
|
pub contents: Vec<T>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub(crate) struct ContentsRendererLogged<T> {
|
|
#[serde(alias = "items")]
|
|
pub contents: MapResult<Vec<T>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct Tab<T> {
|
|
pub tab_renderer: ContentRenderer<T>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct SectionList<T> {
|
|
pub section_list_renderer: ContentsRenderer<T>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct TwoColumnBrowseResults<T> {
|
|
pub two_column_browse_results_renderer: ContentsRenderer<T>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ThumbnailsWrap {
|
|
#[serde(default)]
|
|
pub thumbnail: Thumbnails,
|
|
}
|
|
|
|
/// List of images in different resolutions.
|
|
/// Not only used for thumbnails, but also for avatars and banners.
|
|
#[derive(Default, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct Thumbnails {
|
|
#[serde(default)]
|
|
pub thumbnails: Vec<Thumbnail>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct Thumbnail {
|
|
pub url: String,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ContinuationItemRenderer {
|
|
pub continuation_endpoint: ContinuationEndpoint,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ContinuationEndpoint {
|
|
pub continuation_command: ContinuationCommand,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ContinuationCommand {
|
|
pub token: String,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct Icon {
|
|
pub icon_type: IconType,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
pub(crate) enum IconType {
|
|
/// Checkmark for verified channels
|
|
Check,
|
|
/// Music note for verified artists
|
|
OfficialArtistBadge,
|
|
/// Like button
|
|
Like,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ChannelBadge {
|
|
pub metadata_badge_renderer: ChannelBadgeRenderer,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ChannelBadgeRenderer {
|
|
pub style: ChannelBadgeStyle,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
pub(crate) enum ChannelBadgeStyle {
|
|
BadgeStyleTypeVerified,
|
|
BadgeStyleTypeVerifiedArtist,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct Alert {
|
|
pub alert_renderer: AlertRenderer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct AlertRenderer {
|
|
#[serde_as(as = "Text")]
|
|
pub text: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ResponseContext {
|
|
pub visitor_data: Option<String>,
|
|
}
|
|
|
|
// CONTINUATION
|
|
|
|
#[serde_as]
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct Continuation {
|
|
/// Number of search results
|
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
|
pub estimated_results: Option<u64>,
|
|
#[serde(
|
|
alias = "onResponseReceivedCommands",
|
|
alias = "onResponseReceivedEndpoints"
|
|
)]
|
|
#[serde_as(as = "Option<VecSkipError<_>>")]
|
|
pub on_response_received_actions: Option<Vec<ContinuationActionWrap<YouTubeListItem>>>,
|
|
/// Used for channel video rich grid renderer
|
|
///
|
|
/// A/B test seen on 19.10.2022
|
|
pub continuation_contents: Option<RichGridContinuationContents>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ContinuationActionWrap<T> {
|
|
#[serde(alias = "reloadContinuationItemsCommand")]
|
|
pub append_continuation_items_action: ContinuationAction<T>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ContinuationAction<T> {
|
|
pub continuation_items: MapResult<Vec<T>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct RichGridContinuationContents {
|
|
pub rich_grid_continuation: YouTubeListRenderer,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct MusicContinuationData {
|
|
#[serde(alias = "nextRadioContinuationData")]
|
|
pub next_continuation_data: MusicContinuationDataInner,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct MusicContinuationDataInner {
|
|
pub continuation: String,
|
|
}
|
|
|
|
// ERROR
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ErrorResponse {
|
|
pub error: ErrorResponseContent,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct ErrorResponseContent {
|
|
pub message: String,
|
|
}
|
|
|
|
// DESERIALIZER
|
|
|
|
impl<'de, T> Deserialize<'de> for ContentsRenderer<T>
|
|
where
|
|
T: Deserialize<'de>,
|
|
{
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
struct ItemVisitor<T>(PhantomData<T>);
|
|
|
|
impl<'de, T> Visitor<'de> for ItemVisitor<T>
|
|
where
|
|
T: Deserialize<'de>,
|
|
{
|
|
type Value = ContentsRenderer<T>;
|
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
formatter.write_str("map")
|
|
}
|
|
|
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
where
|
|
A: serde::de::MapAccess<'de>,
|
|
{
|
|
let mut contents = None;
|
|
|
|
while let Some(k) = map.next_key::<Cow<'de, str>>()? {
|
|
if k == "contents" || k == "tabs" || k == "items" {
|
|
contents = Some(ContentsRenderer {
|
|
contents: map.next_value::<VecSkipErrorWrap<T>>()?.0,
|
|
});
|
|
} else {
|
|
map.next_value::<IgnoredAny>()?;
|
|
}
|
|
}
|
|
|
|
contents.ok_or(serde::de::Error::missing_field("contents"))
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_map(ItemVisitor(PhantomData::<T>))
|
|
}
|
|
}
|
|
|
|
// MAPPING
|
|
|
|
impl From<Thumbnail> for crate::model::Thumbnail {
|
|
fn from(tn: Thumbnail) -> Self {
|
|
crate::model::Thumbnail {
|
|
url: tn.url,
|
|
width: tn.width,
|
|
height: tn.height,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Thumbnails> for Vec<crate::model::Thumbnail> {
|
|
fn from(ts: Thumbnails) -> Self {
|
|
ts.thumbnails
|
|
.into_iter()
|
|
.map(|t| crate::model::Thumbnail {
|
|
url: t.url,
|
|
width: t.width,
|
|
height: t.height,
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl From<Vec<ChannelBadge>> for crate::model::Verification {
|
|
fn from(badges: Vec<ChannelBadge>) -> Self {
|
|
badges.get(0).map_or(crate::model::Verification::None, |b| {
|
|
match b.metadata_badge_renderer.style {
|
|
ChannelBadgeStyle::BadgeStyleTypeVerified => Self::Verified,
|
|
ChannelBadgeStyle::BadgeStyleTypeVerifiedArtist => Self::Artist,
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<Icon> for crate::model::Verification {
|
|
fn from(icon: Icon) -> Self {
|
|
match icon.icon_type {
|
|
IconType::Check => Self::Verified,
|
|
IconType::OfficialArtistBadge => Self::Artist,
|
|
IconType::Like => Self::None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn alerts_to_err(id: &str, alerts: Option<Vec<Alert>>) -> ExtractionError {
|
|
ExtractionError::NotFound {
|
|
id: id.to_owned(),
|
|
msg: alerts
|
|
.map(|alerts| {
|
|
alerts
|
|
.into_iter()
|
|
.map(|a| a.alert_renderer.text)
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
.into()
|
|
})
|
|
.unwrap_or_default(),
|
|
}
|
|
}
|