Compare commits
No commits in common. "53a8ec680a3965a71b50ccbc7786fdf426c5fcdc" and "4d124c6d981ca3bd68a78361cf728ec41d637400" have entirely different histories.
53a8ec680a
...
4d124c6d98
19 changed files with 96 additions and 70225 deletions
|
@ -32,7 +32,7 @@ quick-js-dtp = { version = "0.4.1", default-features = false, features = [
|
|||
] }
|
||||
once_cell = "1.12.0"
|
||||
regex = "1.6.0"
|
||||
fancy-regex = "0.12.0"
|
||||
fancy-regex = "0.11.0"
|
||||
thiserror = "1.0.36"
|
||||
url = "2.2.2"
|
||||
reqwest = { version = "0.11.11", default-features = false, features = [
|
||||
|
@ -44,8 +44,8 @@ tokio = { version = "1.20.0", features = ["macros", "time"] }
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.82"
|
||||
serde_with = { version = "3.0.0", default-features = false, features = [
|
||||
"alloc",
|
||||
"macros",
|
||||
"json",
|
||||
] }
|
||||
serde_plain = "1.0.1"
|
||||
rand = "0.8.5"
|
||||
|
@ -59,7 +59,7 @@ ress = "0.11.4"
|
|||
phf = "0.11.1"
|
||||
base64 = "0.21.0"
|
||||
urlencoding = "2.1.2"
|
||||
quick-xml = { version = "0.31.0", features = ["serialize"], optional = true }
|
||||
quick-xml = { version = "0.30.0", features = ["serialize"], optional = true }
|
||||
tracing = { version = "0.1.37", features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -5,7 +5,6 @@ use time::OffsetDateTime;
|
|||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
client::response::YouTubeListItem,
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
|
@ -291,7 +290,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
|||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||
fn map_response(
|
||||
self,
|
||||
id: &str,
|
||||
_id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||
_visitor_data: Option<&str>,
|
||||
|
@ -300,21 +299,11 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
|||
// and it allows parsing the country name.
|
||||
let lang = Language::En;
|
||||
|
||||
let ep = match self {
|
||||
response::ChannelAbout::ReceivedEndpoints {
|
||||
on_response_received_endpoints,
|
||||
} => on_response_received_endpoints
|
||||
let ep = self
|
||||
.on_response_received_endpoints
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?,
|
||||
response::ChannelAbout::Content { contents } => {
|
||||
// Handle errors (e.g. age restriction) when regular channel content was returned
|
||||
map_channel_content(id, contents, None)?;
|
||||
return Err(ExtractionError::InvalidData(
|
||||
"could not extract aboutData".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?;
|
||||
let continuations = ep.append_continuation_items_action.continuation_items;
|
||||
let about = continuations
|
||||
.c
|
||||
|
@ -494,6 +483,13 @@ fn map_channel_content(
|
|||
match contents {
|
||||
Some(contents) => {
|
||||
let tabs = contents.two_column_browse_results_renderer.contents;
|
||||
if tabs.is_empty() {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
msg: "no tabs".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint,
|
||||
expect: &str| {
|
||||
endpoint
|
||||
|
@ -508,41 +504,19 @@ fn map_channel_content(
|
|||
let mut featured_tab = false;
|
||||
|
||||
for tab in &tabs {
|
||||
if let Some(endpoint) = &tab.tab_renderer.endpoint {
|
||||
if cmp_url_suffix(endpoint, "/featured")
|
||||
if cmp_url_suffix(&tab.tab_renderer.endpoint, "/featured")
|
||||
&& (tab.tab_renderer.content.section_list_renderer.is_some()
|
||||
|| tab.tab_renderer.content.rich_grid_renderer.is_some())
|
||||
{
|
||||
featured_tab = true;
|
||||
} else if cmp_url_suffix(endpoint, "/shorts") {
|
||||
} else if cmp_url_suffix(&tab.tab_renderer.endpoint, "/shorts") {
|
||||
has_shorts = true;
|
||||
} else if cmp_url_suffix(endpoint, "/streams") {
|
||||
} else if cmp_url_suffix(&tab.tab_renderer.endpoint, "/streams") {
|
||||
has_live = true;
|
||||
}
|
||||
} else {
|
||||
// Check for age gate
|
||||
if let Some(YouTubeListItem::ChannelAgeGateRenderer {
|
||||
channel_title,
|
||||
main_text,
|
||||
}) = &tab
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.as_ref()
|
||||
.and_then(|c| c.contents.c.get(0))
|
||||
{
|
||||
return Err(ExtractionError::Unavailable {
|
||||
reason: crate::error::UnavailabilityReason::AgeRestricted,
|
||||
msg: format!("{channel_title}: {main_text}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let channel_content = tabs
|
||||
.into_iter()
|
||||
.filter(|t| t.tab_renderer.endpoint.is_some())
|
||||
.find_map(|tab| {
|
||||
let channel_content = tabs.into_iter().find_map(|tab| {
|
||||
tab.tab_renderer
|
||||
.content
|
||||
.rich_grid_renderer
|
||||
|
@ -556,10 +530,9 @@ fn map_channel_content(
|
|||
match channel_content {
|
||||
Some(list) => list.contents,
|
||||
None => {
|
||||
return Err(ExtractionError::NotFound {
|
||||
id: id.to_owned(),
|
||||
msg: "no tabs".into(),
|
||||
});
|
||||
return Err(ExtractionError::InvalidData(
|
||||
"could not extract content".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -659,7 +632,6 @@ mod tests {
|
|||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
error::{ExtractionError, UnavailabilityReason},
|
||||
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
||||
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||
serializer::MapResult,
|
||||
|
@ -677,7 +649,7 @@ mod tests {
|
|||
#[case::upcoming("videos_upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
||||
#[case::richgrid("videos_20221011_richgrid", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||
#[case::richgrid2("videos_20221011_richgrid2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||
#[case::coachella("videos_20230415_coachella", "UCHF66aWLOxBW4l6VkSrS3cQ")]
|
||||
#[case::richgrid2("videos_20230415_coachella", "UCHF66aWLOxBW4l6VkSrS3cQ")]
|
||||
#[case::shorts("shorts", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
||||
|
@ -706,23 +678,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_agegate() {
|
||||
let json_path = path!(*TESTFILES / "channel" / format!("channel_agegate.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let channel: response::Channel =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let res: Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> =
|
||||
channel.map_response("UCbfnHqxXs_K3kvaH-WlNlig", Language::En, None, None);
|
||||
if let Err(ExtractionError::Unavailable { reason, msg }) = res {
|
||||
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
|
||||
assert!(msg.starts_with("Laphroaig Whisky: "));
|
||||
} else {
|
||||
panic!("invalid res: {res:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn map_channel_playlists() {
|
||||
let json_path = path!(*TESTFILES / "channel" / "channel_playlists.json");
|
||||
|
|
|
@ -112,14 +112,13 @@ mod tests {
|
|||
|
||||
#[rstest]
|
||||
#[case::default("default")]
|
||||
#[case::default("w_podcasts")]
|
||||
fn map_music_new_videos(#[case] name: &str) {
|
||||
let json_path = path!(*TESTFILES / "music_new" / format!("videos_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let new_videos: response::MusicNew =
|
||||
let new_albums: response::MusicNew =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Vec<TrackItem>> = new_videos
|
||||
let map_res: MapResult<Vec<TrackItem>> = new_albums
|
||||
.map_response("", Language::En, None, None)
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ impl RustyPipeQuery {
|
|||
|
||||
match tv_res {
|
||||
// Output desktop client error if the tv client is unsupported
|
||||
Err(Error::Extraction(ExtractionError::Unavailable {
|
||||
Err(Error::Extraction(ExtractionError::VideoUnavailable {
|
||||
reason: UnavailabilityReason::UnsupportedClient,
|
||||
..
|
||||
})) => Err(Error::Extraction(e)),
|
||||
|
@ -183,7 +183,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
return Err(ExtractionError::Unavailable { reason, msg });
|
||||
return Err(ExtractionError::VideoUnavailable { reason, msg });
|
||||
}
|
||||
response::player::PlayabilityStatus::LoginRequired { reason, messages } => {
|
||||
let mut msg = reason;
|
||||
|
@ -205,10 +205,10 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
return Err(ExtractionError::Unavailable { reason, msg });
|
||||
return Err(ExtractionError::VideoUnavailable { reason, msg });
|
||||
}
|
||||
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
|
||||
return Err(ExtractionError::Unavailable {
|
||||
return Err(ExtractionError::VideoUnavailable {
|
||||
reason: UnavailabilityReason::OfflineLivestream,
|
||||
msg: reason,
|
||||
});
|
||||
|
@ -216,7 +216,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
response::player::PlayabilityStatus::Error { reason } => {
|
||||
// reason (censored): "This video has been removed for violating YouTube's policy on hate speech. Learn more about combating hate speech in your country."
|
||||
// reason: "This video is unavailable"
|
||||
return Err(ExtractionError::Unavailable {
|
||||
return Err(ExtractionError::VideoUnavailable {
|
||||
reason: UnavailabilityReason::Deleted,
|
||||
msg: reason,
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ pub(crate) struct TabRendererWrap {
|
|||
pub(crate) struct TabRenderer {
|
||||
#[serde(default)]
|
||||
pub content: TabContent,
|
||||
pub endpoint: Option<ChannelTabEndpoint>,
|
||||
pub endpoint: ChannelTabEndpoint,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -148,16 +148,10 @@ pub(crate) struct MicroformatDataRenderer {
|
|||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum ChannelAbout {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ReceivedEndpoints {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ChannelAbout {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
on_response_received_endpoints: Vec<ContinuationActionWrap<AboutChannelRendererWrap>>,
|
||||
},
|
||||
Content {
|
||||
contents: Option<Contents>,
|
||||
},
|
||||
pub on_response_received_endpoints: Vec<ContinuationActionWrap<AboutChannelRendererWrap>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
@ -54,7 +54,7 @@ use serde::{
|
|||
de::{IgnoredAny, Visitor},
|
||||
Deserialize,
|
||||
};
|
||||
use serde_with::{serde_as, DisplayFromStr, VecSkipError};
|
||||
use serde_with::{json::JsonString, serde_as, VecSkipError};
|
||||
|
||||
use crate::error::ExtractionError;
|
||||
use crate::serializer::{text::Text, MapResult, VecSkipErrorWrap};
|
||||
|
@ -202,7 +202,7 @@ pub(crate) struct ResponseContext {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Continuation {
|
||||
/// Number of search results
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub estimated_results: Option<u64>,
|
||||
#[serde(
|
||||
alias = "onResponseReceivedCommands",
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
text::{Text, TextComponent, TextComponents},
|
||||
MapResult,
|
||||
},
|
||||
util::{self, dictionary, timeago},
|
||||
util::{self, dictionary},
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -780,16 +780,9 @@ impl MusicListMapper {
|
|||
.map(|st| map_album_type(st.first_str(), self.lang))
|
||||
.unwrap_or_default();
|
||||
|
||||
let (mut artists, by_va) = map_artists(subtitle_p2);
|
||||
let artist_id = map_artist_id_fallback(item.menu, artists.first());
|
||||
let (artists, by_va) = map_artists(subtitle_p2);
|
||||
|
||||
// Album artist links may be invisible on the search page, so
|
||||
// fall back to menu data
|
||||
if let Some(a1) = artists.first_mut() {
|
||||
if a1.id.is_none() {
|
||||
a1.id = artist_id.clone();
|
||||
}
|
||||
}
|
||||
let artist_id = map_artist_id_fallback(item.menu, artists.first());
|
||||
|
||||
let year =
|
||||
subtitle_p3.and_then(|st| util::parse_numeric(st.first_str()).ok());
|
||||
|
@ -862,38 +855,23 @@ impl MusicListMapper {
|
|||
match item.navigation_endpoint.music_page() {
|
||||
Some(music_page) => match music_page.typ {
|
||||
MusicPageType::Track { vtype } => {
|
||||
let (artists, by_va, view_count, duration) = if vtype == MusicVideoType::Episode
|
||||
{
|
||||
let (artists, by_va) = map_artists(subtitle_p2);
|
||||
let duration = subtitle_p1.and_then(|s| {
|
||||
timeago::parse_video_duration_or_warn(
|
||||
self.lang,
|
||||
s.first_str(),
|
||||
&mut self.warnings,
|
||||
)
|
||||
});
|
||||
(artists, by_va, None, duration)
|
||||
} else {
|
||||
let (artists, by_va) = map_artists(subtitle_p1);
|
||||
let view_count = subtitle_p2.and_then(|c| {
|
||||
|
||||
self.items.push(MusicItem::Track(TrackItem {
|
||||
id: music_page.id,
|
||||
name: item.title,
|
||||
duration: None,
|
||||
cover: item.thumbnail_renderer.into(),
|
||||
artist_id: artists.first().and_then(|a| a.id.clone()),
|
||||
artists,
|
||||
album: None,
|
||||
view_count: subtitle_p2.and_then(|c| {
|
||||
util::parse_large_numstr_or_warn(
|
||||
c.first_str(),
|
||||
self.lang,
|
||||
&mut self.warnings,
|
||||
)
|
||||
});
|
||||
(artists, by_va, view_count, None)
|
||||
};
|
||||
|
||||
self.items.push(MusicItem::Track(TrackItem {
|
||||
id: music_page.id,
|
||||
name: item.title,
|
||||
duration,
|
||||
cover: item.thumbnail_renderer.into(),
|
||||
artist_id: artists.first().and_then(|a| a.id.clone()),
|
||||
artists,
|
||||
album: None,
|
||||
view_count,
|
||||
}),
|
||||
is_video: vtype.is_video(),
|
||||
track_nr: None,
|
||||
by_va,
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::ops::Range;
|
|||
|
||||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use serde_with::{DefaultOnError, DisplayFromStr};
|
||||
use serde_with::{json::JsonString, DefaultOnError};
|
||||
|
||||
use super::{ResponseContext, Thumbnails};
|
||||
use crate::serializer::{text::Text, MapResult};
|
||||
|
@ -78,7 +78,7 @@ pub(crate) struct ErrorMessage {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct StreamingData {
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[serde_as(as = "JsonString")]
|
||||
pub expires_in_seconds: u32,
|
||||
#[serde(default)]
|
||||
pub formats: MapResult<Vec<Format>>,
|
||||
|
@ -106,7 +106,7 @@ pub(crate) struct Format {
|
|||
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub approx_duration_ms: Option<u32>,
|
||||
|
||||
#[serde_as(as = "Option<crate::serializer::Range>")]
|
||||
|
@ -114,7 +114,7 @@ pub(crate) struct Format {
|
|||
#[serde_as(as = "Option<crate::serializer::Range>")]
|
||||
pub init_range: Option<Range<u32>>,
|
||||
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub content_length: Option<u64>,
|
||||
|
||||
#[serde(default)]
|
||||
|
@ -129,7 +129,7 @@ pub(crate) struct Format {
|
|||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub audio_quality: Option<AudioQuality>,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub audio_sample_rate: Option<u32>,
|
||||
pub audio_channels: Option<u8>,
|
||||
pub loudness_db: Option<f32>,
|
||||
|
@ -237,7 +237,7 @@ pub(crate) struct CaptionTrack {
|
|||
pub(crate) struct VideoDetails {
|
||||
pub video_id: String,
|
||||
pub title: String,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[serde_as(as = "JsonString")]
|
||||
pub length_seconds: u32,
|
||||
#[serde(default)]
|
||||
pub keywords: Vec<String>,
|
||||
|
@ -245,7 +245,7 @@ pub(crate) struct VideoDetails {
|
|||
pub short_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub thumbnail: Thumbnails,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[serde_as(as = "JsonString")]
|
||||
pub view_count: u64,
|
||||
pub author: String,
|
||||
pub is_live_content: bool,
|
||||
|
|
|
@ -2,7 +2,7 @@ use serde::{
|
|||
de::{IgnoredAny, Visitor},
|
||||
Deserialize,
|
||||
};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use serde_with::{json::JsonString, serde_as};
|
||||
|
||||
use super::{video_item::YouTubeListRendererWrap, ResponseContext};
|
||||
|
||||
|
@ -10,7 +10,7 @@ use super::{video_item::YouTubeListRendererWrap, ResponseContext};
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Search {
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub estimated_results: Option<u64>,
|
||||
pub contents: Contents,
|
||||
pub response_context: ResponseContext,
|
||||
|
|
|
@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
|
|||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde_with::{
|
||||
rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError,
|
||||
json::JsonString, rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
|
@ -69,14 +69,6 @@ pub(crate) enum YouTubeListItem {
|
|||
contents: MapResult<Vec<YouTubeListItem>>,
|
||||
},
|
||||
|
||||
/// Age-restricted channel
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChannelAgeGateRenderer {
|
||||
channel_title: String,
|
||||
#[serde_as(as = "Text")]
|
||||
main_text: String,
|
||||
},
|
||||
|
||||
/// No video list item (e.g. ad) or unimplemented item
|
||||
///
|
||||
/// Unimplemented:
|
||||
|
@ -162,7 +154,7 @@ pub(crate) struct PlaylistVideoRenderer {
|
|||
pub title: String,
|
||||
#[serde(rename = "shortBylineText")]
|
||||
pub channel: TextComponent,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub length_seconds: Option<u32>,
|
||||
/// Regular video: `["29K views", " • ", "13 years ago"]`
|
||||
/// Livestream: `["66K", " watching"]`
|
||||
|
@ -192,7 +184,7 @@ pub(crate) struct PlaylistRenderer {
|
|||
/// The first item of this list contains the playlist thumbnail,
|
||||
/// subsequent items contain very small thumbnails of the next playlist videos
|
||||
pub thumbnails: Option<Vec<Thumbnails>>,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
#[serde_as(as = "Option<JsonString>")]
|
||||
pub video_count: Option<u64>,
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub video_count_short_text: Option<String>,
|
||||
|
@ -248,7 +240,7 @@ pub(crate) struct YouTubeListRenderer {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct UpcomingEventData {
|
||||
/// Unixtime in seconds
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[serde_as(as = "JsonString")]
|
||||
pub start_time: i64,
|
||||
}
|
||||
|
||||
|
@ -712,7 +704,7 @@ impl YouTubeListMapper<YouTubeItem> {
|
|||
self.warnings.append(&mut contents.warnings);
|
||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||
}
|
||||
YouTubeListItem::None | YouTubeListItem::ChannelAgeGateRenderer { .. } => {}
|
||||
YouTubeListItem::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,906 +0,0 @@
|
|||
---
|
||||
source: src/client/video_details.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
VideoDetails(
|
||||
id: "ZeerrnuLi5E",
|
||||
name: "aespa 에스파 \'Black Mamba\' MV",
|
||||
description: RichText([
|
||||
Text(
|
||||
text: "🎧Listen and download aespa\'s debut single \"Black Mamba\": ",
|
||||
),
|
||||
Web(
|
||||
text: "https://smarturl.it/aespa_BlackMamba",
|
||||
url: "https://smarturl.it/aespa_BlackMamba",
|
||||
),
|
||||
Text(
|
||||
text: "\n🐍The Debut Stage ",
|
||||
),
|
||||
YouTube(
|
||||
text: "aespa 에스파 \'Black Mamba\' The Debut Stage",
|
||||
target: Video(
|
||||
id: "Ky5RT5oGg0w",
|
||||
start_time: 0,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
text: "\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: ",
|
||||
),
|
||||
Web(
|
||||
text: "https://www.ticketmaster.com/event/0A...",
|
||||
url: "https://www.ticketmaster.com/event/0A005CCD9E871F6E",
|
||||
),
|
||||
Text(
|
||||
text: "\n\nSubscribe to aespa Official YouTube Channel!\n",
|
||||
),
|
||||
Web(
|
||||
text: "https://www.youtube.com/aespa?sub_con...",
|
||||
url: "https://www.youtube.com/aespa?sub_confirmation=1",
|
||||
),
|
||||
Text(
|
||||
text: "\n\naespa official\n",
|
||||
),
|
||||
Web(
|
||||
text: "aespa",
|
||||
url: "https://www.youtube.com/c/aespa",
|
||||
),
|
||||
Text(
|
||||
text: "\n",
|
||||
),
|
||||
Web(
|
||||
text: "aespa_official",
|
||||
url: "https://www.instagram.com/aespa_official",
|
||||
),
|
||||
Text(
|
||||
text: "\n",
|
||||
),
|
||||
Web(
|
||||
text: "aespa_official",
|
||||
url: "https://www.tiktok.com/@aespa_official",
|
||||
),
|
||||
Text(
|
||||
text: "\n",
|
||||
),
|
||||
Web(
|
||||
text: "aespa_official",
|
||||
url: "https://twitter.com/aespa_Official",
|
||||
),
|
||||
Text(
|
||||
text: "\n",
|
||||
),
|
||||
Web(
|
||||
text: "aespa.official",
|
||||
url: "https://www.facebook.com/aespa.official",
|
||||
),
|
||||
Text(
|
||||
text: "\n",
|
||||
),
|
||||
Web(
|
||||
text: "https://weibo.com/aespa",
|
||||
url: "https://weibo.com/aespa",
|
||||
),
|
||||
Text(
|
||||
text: "\n\n",
|
||||
),
|
||||
Text(
|
||||
text: "#aespa",
|
||||
),
|
||||
Text(
|
||||
text: " ",
|
||||
),
|
||||
Text(
|
||||
text: "#æspa",
|
||||
),
|
||||
Text(
|
||||
text: " ",
|
||||
),
|
||||
Text(
|
||||
text: "#BlackMamba",
|
||||
),
|
||||
Text(
|
||||
text: " ",
|
||||
),
|
||||
Text(
|
||||
text: "#블랙맘바",
|
||||
),
|
||||
Text(
|
||||
text: " ",
|
||||
),
|
||||
Text(
|
||||
text: "#에스파",
|
||||
),
|
||||
Text(
|
||||
text: "\naespa 에스파 \'Black Mamba\' MV ℗ SM Entertainment",
|
||||
),
|
||||
]),
|
||||
channel: ChannelTag(
|
||||
id: "UCEf_Bc-KVd7onSeifS3py9g",
|
||||
name: "SMTOWN",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_1Z4I2qpWaCN9g3BcDd3cVA9MDHOG43lE1YNWDNkKro49haGxkjwuFK-I8faWTKM6Jle9qb4ag=s48-c-k-c0x00ffffff-no-rj",
|
||||
width: 48,
|
||||
height: 48,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_1Z4I2qpWaCN9g3BcDd3cVA9MDHOG43lE1YNWDNkKro49haGxkjwuFK-I8faWTKM6Jle9qb4ag=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 88,
|
||||
height: 88,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_1Z4I2qpWaCN9g3BcDd3cVA9MDHOG43lE1YNWDNkKro49haGxkjwuFK-I8faWTKM6Jle9qb4ag=s176-c-k-c0x00ffffff-no-rj",
|
||||
width: 176,
|
||||
height: 176,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: Some(32100000),
|
||||
),
|
||||
view_count: 255522287,
|
||||
like_count: Some(4209059),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("Nov 17, 2020"),
|
||||
is_live: false,
|
||||
is_ccommons: false,
|
||||
chapters: [],
|
||||
recommended: Paginator(
|
||||
count: None,
|
||||
items: [
|
||||
VideoItem(
|
||||
id: "4TWR90KJl84",
|
||||
name: "aespa 에스파 \'Next Level\' MV",
|
||||
length: Some(236),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/4TWR90KJl84/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBYCGc-AKsDC6UpJgIZw2_VsqjVWA",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/4TWR90KJl84/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDh-eDxZBmrNsHcb6pYX0Gyx6gJ8Q",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCEf_Bc-KVd7onSeifS3py9g",
|
||||
name: "SMTOWN",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_1Z4I2qpWaCN9g3BcDd3cVA9MDHOG43lE1YNWDNkKro49haGxkjwuFK-I8faWTKM6Jle9qb4ag=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 years ago"),
|
||||
view_count: Some(277189882),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "yQUU29NwNF4",
|
||||
name: "aespa(에스파) - Black Mamba @인기가요 inkigayo 20201122",
|
||||
length: Some(213),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/yQUU29NwNF4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLA4pIWwOFmVuVU-jZ-j7S4GvgxjKw",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/yQUU29NwNF4/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLC4B3H-paMDpjdf_V6NsymGNvVicQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCS_hnpJLQTvBkqALgapi_4g",
|
||||
name: "스브스케이팝 X INKIGAYO",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/Uxpz5J0EcsFJRbqh4Ip7i3TTNsxTh5jVUxfZmV1DTrCQM_ihfzBGMmkfSRGWoFK9M0anhIie=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 years ago"),
|
||||
view_count: Some(10870401),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "mTmm0y73ZtM",
|
||||
name: "Secret Missions: 7 Thrilling Spy and Secret Agent Stories",
|
||||
length: Some(6811),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mTmm0y73ZtM/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDz3aKv3IbWrI5GmtWjWl2br6h7jw",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/mTmm0y73ZtM/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCNnJtMt14Dn6iNSSHZtZL5MsYDtQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCSVngVJ_0s0ZX4oiN_4ZkaQ",
|
||||
name: "My Story Shared",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/APkrFKbfZM35CSjtdsk3QIcEvalm3yoAzCkgZptcgSfNHw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("9 days ago"),
|
||||
view_count: Some(888941),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "xBnSq8JKlZw",
|
||||
name: "16 Eylül 2023",
|
||||
length: Some(7971),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/xBnSq8JKlZw/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-BIAC6AKKAgwIABABGGUgUChFMA8=&rs=AOn4CLDBvW0PORHHExpND8qbAa0OCr5MMw",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/xBnSq8JKlZw/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gSAAugCigIMCAAQARhlIFAoRTAP&rs=AOn4CLASNCymIttQ1GwgbWvtxD_KeG5yGw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCcyqrKg5jlvVJy1nc-0igRQ",
|
||||
name: "Buğlem TV",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_kbdhEVJB2pzgy-qLRGQQ5sCPPZnEVeljgVrk0KZxPe8UxT8mm5ZZp7Zn6TGMcBMcpCG-zPi1YI=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 month ago"),
|
||||
view_count: Some(8407),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "fE2h3lGlOsk",
|
||||
name: "ITZY \"WANNABE\" M/V @ITZY",
|
||||
length: Some(219),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/fE2h3lGlOsk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC44Q0lpu5a8rltgTMxi0X2QA6jnQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/fE2h3lGlOsk/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLC6F85UnQjP3_9U0gehdYbbF6NTxw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCaO6TYtlC8U5ttz62hTrZgg",
|
||||
name: "JYP Entertainment",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/kcV7NQkBm-UvvzVTJvrg1Yf1eHSqi-DLXuZPt_ECa3cHEPefujS951Dxj6KUEQ5i9Z7_fyMUjw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 years ago"),
|
||||
view_count: Some(523737389),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "NU611fxGyPU",
|
||||
name: "aespa 에스파 \'Black Mamba\' Dance Practice",
|
||||
length: Some(175),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/NU611fxGyPU/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAgKORzcy6WKosI1_PAVWDgcjJ9jA",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/NU611fxGyPU/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDqWWIfLCdtyqy5aIUA_PGcEW2r2g",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC9GtSLeksfK4yuJ_g1lgQbg",
|
||||
name: "aespa",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/rLxODcRttuDvITkAJruNIDlDkVMEsPVuHJyMQDjeYqoFh80JyGwfXXMOZgZXd6-iKuf9rifqYQ=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Artist,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 years ago"),
|
||||
view_count: Some(40486850),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "dYRITmpFbJ4",
|
||||
name: "aespa 에스파 \'Girls\' MV",
|
||||
length: Some(269),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/dYRITmpFbJ4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBOxN6ukbZNOPwUBhRZYgG9r23lng",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/dYRITmpFbJ4/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBLmRDhzBtNHCuokfKRQufiNKKfZg",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCEf_Bc-KVd7onSeifS3py9g",
|
||||
name: "SMTOWN",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_1Z4I2qpWaCN9g3BcDd3cVA9MDHOG43lE1YNWDNkKro49haGxkjwuFK-I8faWTKM6Jle9qb4ag=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 year ago"),
|
||||
view_count: Some(135870843),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "jiFBY6gk3Lk",
|
||||
name: "BLACKPINK x AESPA – Pink Venom / Black Mamba MASHUP (feat. Next Level)",
|
||||
length: Some(240),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/jiFBY6gk3Lk/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLARhXJ8KOxiWpj430QpyKF2m3LJFQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/jiFBY6gk3Lk/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDGOkNsfHOpy9GLXoHn1rnIOn0CcA",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC5XWNylwy4efFufjMYqcglw",
|
||||
name: "Miggy Smallz",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/APkrFKYR2j9afW3H0lgdKwD8qPiIZvZBfCSLxAZQRiUB=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 year ago"),
|
||||
view_count: Some(4218059),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "CM4CkVFmTds",
|
||||
name: "TWICE \"I CAN\'T STOP ME\" M/V",
|
||||
length: Some(221),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CM4CkVFmTds/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBfd7QADIduQSR2ESLIp1k5gxxNDg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/CM4CkVFmTds/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDRn7hTXV_Ls30E6BQNZQtQjbuEpA",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCaO6TYtlC8U5ttz62hTrZgg",
|
||||
name: "JYP Entertainment",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/kcV7NQkBm-UvvzVTJvrg1Yf1eHSqi-DLXuZPt_ECa3cHEPefujS951Dxj6KUEQ5i9Z7_fyMUjw=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 years ago"),
|
||||
view_count: Some(509192107),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "WPdWvnAAurg",
|
||||
name: "aespa 에스파 \'Savage\' MV",
|
||||
length: Some(259),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/WPdWvnAAurg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDQGxlnDkAdMYRm2cdkDmiDbBDpYw",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/WPdWvnAAurg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAIHFE0eH_r-HP7DRPv1QJJnRDzWw",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCEf_Bc-KVd7onSeifS3py9g",
|
||||
name: "SMTOWN",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/_1Z4I2qpWaCN9g3BcDd3cVA9MDHOG43lE1YNWDNkKro49haGxkjwuFK-I8faWTKM6Jle9qb4ag=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 years ago"),
|
||||
view_count: Some(245301963),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "OgabtEgG_kg",
|
||||
name: "[ FULL ALBUM ] IVE (아이브) — IVE The 1st EP \' I\'VE MINE TRACKLIST",
|
||||
length: Some(1034),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/OgabtEgG_kg/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AH-BIAC6AKKAgwIABABGGkgaShpMA8=&rs=AOn4CLBF1lxbztXMyXmem4owNAWZRqvnBA",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/OgabtEgG_kg/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB_gSAAugCigIMCAAQARhpIGkoaTAP&rs=AOn4CLAqM-WuOQCGTABX_NmzvOUOhx3rLA",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCo7jZg0Q5jBWs0WSq_SdDoA",
|
||||
name: "x i a o s h i z i",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ErguVbkzrIKe0hezWviLvuCCGLaQYlrSWQ7Dw6beAwNJnR1ed7NV3pokG_z_I7GQvxfUybB4tg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("7 days ago"),
|
||||
view_count: Some(1719),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "cSqOY5nktfg",
|
||||
name: "BLACKPINK THE GAME - ‘THE GIRLS’ MV",
|
||||
length: Some(164),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/cSqOY5nktfg/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDkx-bmEWvYbs8ju1cETIRE1AczFQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/cSqOY5nktfg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAYPPC91mec_MPvRxjRBzTpwFDBUQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCOmHUn--16B90oW2L6FRR3A",
|
||||
name: "BLACKPINK",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/hZDUwjoeQqigphL4A1tkg9c6hVp5yXmbboBR7PYFUSFj5PIJSA483NB5v7b0XVoTN9GCku3tqQ=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Artist,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("2 months ago"),
|
||||
view_count: Some(61284005),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "i8fRCkq5tbw",
|
||||
name: "aespa 에스파 ‘ep.2 Next Level’ – SM Culture Universe",
|
||||
length: Some(1040),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/i8fRCkq5tbw/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBGcGKSQOqvI_5ZONNturhZZmkysQ",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/i8fRCkq5tbw/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBQsyuNIuuztFQo2FdweYiROIJY3A",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC9GtSLeksfK4yuJ_g1lgQbg",
|
||||
name: "aespa",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/rLxODcRttuDvITkAJruNIDlDkVMEsPVuHJyMQDjeYqoFh80JyGwfXXMOZgZXd6-iKuf9rifqYQ=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Artist,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 year ago"),
|
||||
view_count: Some(5419659),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "erCzl8x9Zuo",
|
||||
name: "에스파(AESPA) 2023 lotte family concert Full Ver. (Black Mamba +thirsty + Illusion+next level+ Spicy)",
|
||||
length: Some(1192),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/erCzl8x9Zuo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBF18qnuz8guk309k2UUh4xnLuazg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/erCzl8x9Zuo/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB36E2CQnz-l8QtgUEYQfGADaZJdA",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC8yPhlmo-4MY5ZfF-cw3JRg",
|
||||
name: "Rock Music",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/APkrFKYogJDjV-mjqi-JeTWKU7PwH_gKL2x9Thq04YuANQ=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("4 months ago"),
|
||||
view_count: Some(392124),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "7HDeem-JaSY",
|
||||
name: "(여자)아이들((G)I-DLE) - \'퀸카 (Queencard)\' Official Music Video",
|
||||
length: Some(211),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/7HDeem-JaSY/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC_jASE2yooEXAN64rj8-1_AJZl6A",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/7HDeem-JaSY/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDq-8lmFENHZeVvK5bQBvpnqEDFIQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCritGVo7pLJLUS8wEu32vow",
|
||||
name: "(G)I-DLE (여자)아이들 (Official YouTube Channel)",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/APkrFKbGrI182ZniS64zKXUGr2CeJ9tMxoa9w90e6SaZkA=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Artist,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("5 months ago"),
|
||||
view_count: Some(259400824),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "9JFi7MmjtGA",
|
||||
name: "VIVIZ (비비지) - \'MANIAC\' MV",
|
||||
length: Some(197),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/9JFi7MmjtGA/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLALaqiHHm-fnm1TQHpD9PG-zGd-hg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/9JFi7MmjtGA/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLD1uM6lf5nCYUj8ZbzAtNYfwb4Z4Q",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC6YMr57knEIYXOOKMmYAFXQ",
|
||||
name: "BPM Entertainment",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/jltaW4jLrF76MrBe-HKcIQoop67mOF3QLdGzFiYwB-Pt7qKv4X7VECkCzfvkn037abuy6zV0bg=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: None,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 day ago"),
|
||||
view_count: Some(2807502),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "NoYKBAajoyo",
|
||||
name: "EVERGLOW (에버글로우) - DUN DUN MV",
|
||||
length: Some(209),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/NoYKBAajoyo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLC3OhCUbjpIclmjfV8W8T98nVI5pA",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/NoYKBAajoyo/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA-CdJunWg1z_pnrT55qagTHnxkdQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC_pwIXKXNm5KGhdEVzmY60A",
|
||||
name: "Stone Music Entertainment",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/XIxEtKkvUSLHiDBazM8kYyKpyESz5LM--vG0F7aMsiqOC2o_IZaKztsqDMj2mcn4ciQzFbvu=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 years ago"),
|
||||
view_count: Some(286088204),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "pyf8cbqyfPs",
|
||||
name: "LE SSERAFIM (르세라핌) \'ANTIFRAGILE\' OFFICIAL M/V",
|
||||
length: Some(232),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/pyf8cbqyfPs/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAu-V-1EWwbHjZTNTO-vuP_O_WB3Q",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/pyf8cbqyfPs/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCxVrNDDgEYMZWgOukne2kgOV2Vhg",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UC3IZKseVpdzPSBaWxBxundA",
|
||||
name: "HYBE LABELS",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/ytc/APkrFKaWqx5IfcKbi5z8FgPsM_kA6NQ2zTAx8gr27yQcdQ=s68-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Verified,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("1 year ago"),
|
||||
view_count: Some(196956308),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "32si5cfrCNc",
|
||||
name: "BLACKPINK - \'How You Like That\' DANCE PERFORMANCE VIDEO",
|
||||
length: Some(181),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/32si5cfrCNc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBjimPvMxDwTmPBlKX8Buo9EjMeOg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/32si5cfrCNc/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDCsJMBcdZaForwAnhjYy3L1JT1hQ",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCOmHUn--16B90oW2L6FRR3A",
|
||||
name: "BLACKPINK",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/hZDUwjoeQqigphL4A1tkg9c6hVp5yXmbboBR7PYFUSFj5PIJSA483NB5v7b0XVoTN9GCku3tqQ=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Artist,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("3 years ago"),
|
||||
view_count: Some(1522365959),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
VideoItem(
|
||||
id: "bwmSjveL3Lc",
|
||||
name: "BLACKPINK - \'붐바야 (BOOMBAYAH)\' M/V",
|
||||
length: Some(244),
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/bwmSjveL3Lc/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDRzdujtL9QM0RZ8elD00oS2fXMhg",
|
||||
width: 168,
|
||||
height: 94,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/bwmSjveL3Lc/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBOprayVWEKYsgHjpoCw6GFcV3Hng",
|
||||
width: 336,
|
||||
height: 188,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelTag(
|
||||
id: "UCOmHUn--16B90oW2L6FRR3A",
|
||||
name: "BLACKPINK",
|
||||
avatar: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/hZDUwjoeQqigphL4A1tkg9c6hVp5yXmbboBR7PYFUSFj5PIJSA483NB5v7b0XVoTN9GCku3tqQ=s88-c-k-c0x00ffffff-no-rj",
|
||||
width: 68,
|
||||
height: 68,
|
||||
),
|
||||
],
|
||||
verification: Artist,
|
||||
subscriber_count: None,
|
||||
)),
|
||||
publish_date: "[date]",
|
||||
publish_date_txt: Some("7 years ago"),
|
||||
view_count: Some(1646522795),
|
||||
is_live: false,
|
||||
is_short: false,
|
||||
is_upcoming: false,
|
||||
short_description: None,
|
||||
),
|
||||
],
|
||||
ctoken: Some("CBQSExILWmVlcnJudUxpNUXAAQHIAQEYACqjDjJzNkw2d3pUQ2dyUUNnb0Q4ajRBQ2c3Q1Bnc0l6cS1tbFBTLTVKcmhBUW9EOGo0QUNnM0NQZ29JM3I3Z2pfdVl2b2QwQ2c3Q1Bnc0lfZUNBXzQyNl9OV1pBUW9Pd2o0TENQWHE1Y3Z0Z3VlZDVRRUtEc0ktQ3dqazRNencwS19abUpJQkNnM0NQZ29JNHItUm91ZVkxTXBRQ2czQ1Bnb0k4OVRQNGNXMzBJd1JDZzNDUGdvSTRxYmZ6TS11b2ZFRENnN0NQZ3NJX2RIeWs5el9tS0t4QVFvT3dqNExDT24yeE1ycDdKS3JpQUVLRGNJLUNnaVpvcUNKbGFDZ2lVNEtBX0ktQUFvT3dqNExDTjdvd0p1OW04V0N5UUVLQV9JLUFBb093ajRMQ05QTjNmZXkydW1jbVFFS0FfSS1BQW9Pd2o0TENKeXJxcEs4MWZTTXhBRUtBX0ktQUFvTndqNEtDTW4xbEkzbHUtaW1mQW9EOGo0QUNnM0NQZ29JOVpHYjR0LTZyYWMxQ2dQeVBnQUtEY0ktQ2dpZTJaWFM1b21Td25VS0FfSS1BQW9Pd2o0TENMbTVrOEc2ck5DUWpnRUtBX0ktQUFvTndqNEtDTnVibVl1VjBvRG5DQW9EOGo0QUNnM0NQZ29JdVBXQ2dPZlgxZnRZQ2dQeVBnQUtEY0ktQ2dqSV9KdkF4UGFtZ3pvS0FfSS1BQW9Od2o0S0NQanJrcy01ektPVmNRb0Q4ajRBQ2c3Q1Bnc0l2T3ZtMWFTaDlPT0xBUW9EOGo0QUNnM0NQZ29JNnMzMTRfenlyTmg2Q2dQeVBnQUtEc0ktQ3dpbTBxWDhwcy0zdU93QkNnUHlQZ0FLRHNJLUN3amc2STdOek4zWXlQUUJDZ1B5UGdBS0RjSS1DZ2lxeG82MXdNQ0N3ellLQV9JLUFBb093ajRMQ1B2NXlkV2Jqdi1UcHdFS0FfSS1BQW9Pd2o0TENOZVJyTF9jM01pMTN3RUtBX0ktQUFvTndqNEtDTGU1cjd6djBlU0Vid29EOGo0QUNnM0NQZ29JLXNXX3I1ZWI4c01XQ2czQ1Bnb0lpLUdvNjlYMnlfc3JDZzNDUGdvSWlObnc4SmowcHZGbENnM0NQZ29Jczl1ODM4Xzdrc0ZGQ2c3Q1Bnc0k5cnZkNFpqNjlNX2VBUW9Pd2o0TENPQ1VrNG5zbXVMTV9nRUtEc0ktQ3dpMHFySEkzc0R0cGJjQkNnN0NQZ3NJazlxdjZNV2FwY2JhQVFvT3dqNExDTVN6djQyOHhxcUstUUVLRHNJLUN3allfNm5FNXAzVjdMc0JDZzdDUGdzSXNQNjV1ZjdpcEtyV0FRb093ajRMQ05hdWlJWDJwX2VyOFFFS0RzSS1Dd2pvcTlEUG11Nmx6ZEFCQ2c3Q1Bnc0lwNnZFaWNuQ3A1ZUxBUW9Pd2o0TENJQ244cG1vejhuLWlnRUtEY0ktQ2dpSG91emdoS18xdlQwS0RzSS1Dd2pXNE9qcV8tUGFpcHdCQ2c3Q1Bnc0k4ZV9Hbi16bTN2U2pBUW9Od2o0S0NLMjh0WTZJaUtETkd3b093ajRMQ0tfdHlOZXF3T3VCMUFFS0RzSS1Dd2pja3BPWnR0S1AtNDhCQ2c3Q1Bnc0kxSldKNjgyU21lN3dBUW9Od2o0S0NMWE52ZUNUN1pfLVpnb093ajRMQ0pMSHM0eTFrX3FJcXdFU0ZnQUNEUThSRXhVWEdSc2RIeUVqSlNjcEt5MHZNVE1hQkFnQUVBRWFCQWdDRUFNYUJBZ0NFQVFhQkFnQ0VBVWFCQWdDRUFZYUJBZ0NFQWNhQkFnQ0VBZ2FCQWdDRUFrYUJBZ0NFQW9hQkFnQ0VBc2FCQWdDRUF3YUJBZ05FQTRhQkFnUEVCQWFCQWdSRUJJYUJBZ1RFQlFhQkFnVkVCWWFCQWdYRUJnYUJBZ1pFQm9hQkFnYkVCd2FCQWdkRUI0YUJBZ2ZFQ0FhQkFnaEVDSWFCQWdqRUNRYUJBZ2xFQ1lhQkFnbkVDZ2FCQWdwRUNvYUJBZ3JFQ3dhQkFndEVDNGFCQWd2RURBYUJBZ3hFRElhQkFnekVBY2FCQWd6RURRYUJBZ3pFRFVhQkFnekVEWWFCQWd6RURjYUJBZ3pFRGdhQkFnekVEa2FCQWd6RURvYUJBZ3pFRHNhQkFnekVEd2FCQWd6RUQwYUJBZ3pFRDRhQkFnekVBd2FCQWd6RUFVYUJBZ3pFRDhhQkFnekVFQWFCQWd6RUVFYUJBZ3pFRUlhQkFnekVFTWFCQWd6RUVRYUJBZ3pFRVVhQkFnekVBTWFCQWd6RUVZYUJBZ3pFRWNhQkFnekVFZ2FCQWd6RUVrYUJBZ3pFQVlhQkFnekVBUWFCQWd6RUVvYUJBZ3pFRXNxRmdBQ0RROFJFeFVYR1JzZEh5RWpKU2NwS3kwdk1UTWoPd2F0Y2gtbmV4dC1mZWVk"),
|
||||
endpoint: next,
|
||||
),
|
||||
top_comments: Paginator(
|
||||
count: Some(703000),
|
||||
items: [],
|
||||
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyJSIRIgtaZWVycm51TGk1RTAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D"),
|
||||
endpoint: next,
|
||||
),
|
||||
latest_comments: Paginator(
|
||||
count: Some(703000),
|
||||
items: [],
|
||||
ctoken: Some("Eg0SC1plZXJybnVMaTVFGAYyOCIRIgtaZWVycm51TGk1RTABeAIwAUIhZW5nYWdlbWVudC1wYW5lbC1jb21tZW50cy1zZWN0aW9u"),
|
||||
endpoint: next,
|
||||
),
|
||||
visitor_data: None,
|
||||
)
|
|
@ -110,7 +110,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
let video_id = current_video_endpoint.watch_endpoint.video_id;
|
||||
if id != video_id {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong video id {video_id}, expected {id}"
|
||||
"got wrong playlist id {video_id}, expected {id}"
|
||||
)));
|
||||
}
|
||||
|
||||
|
@ -572,10 +572,9 @@ mod tests {
|
|||
#[case::chapters("chapters", "nFDBxBUfE74")]
|
||||
#[case::live("live", "86YLFOog4GM")]
|
||||
#[case::agegate("agegate", "HRKu0cvrr_o")]
|
||||
#[case::ab_newdesc("20220924_newdesc", "ZeerrnuLi5E")]
|
||||
#[case::ab_new_cont("20221011_new_continuation", "ZeerrnuLi5E")]
|
||||
#[case::ab_no_recommends("20221011_rec_isr", "nFDBxBUfE74")]
|
||||
#[case::ab_new_likes("20231103_likes", "ZeerrnuLi5E")]
|
||||
#[case::newdesc("20220924_newdesc", "ZeerrnuLi5E")]
|
||||
#[case::new_cont("20221011_new_continuation", "ZeerrnuLi5E")]
|
||||
#[case::no_recommends("20221011_rec_isr", "nFDBxBUfE74")]
|
||||
fn map_video_details(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "video_details" / format!("video_details_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
15
src/error.rs
15
src/error.rs
|
@ -24,15 +24,14 @@ pub enum Error {
|
|||
/// Error extracting content from YouTube
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ExtractionError {
|
||||
/// Content cannot be extracted with RustyPipe
|
||||
/// Video cannot be extracted with RustyPipe
|
||||
///
|
||||
/// Reasons include:
|
||||
/// - Deletion/Censorship
|
||||
/// - Age restriction
|
||||
/// - Private video
|
||||
/// - Private video that requires a Google account
|
||||
/// - DRM (Movies and TV shows)
|
||||
#[error("content unavailable because it is {reason}. Reason (from YT): {msg}")]
|
||||
Unavailable {
|
||||
#[error("video cant be played because it is {reason}. Reason (from YT): {msg}")]
|
||||
VideoUnavailable {
|
||||
/// Reason why the video could not be extracted
|
||||
reason: UnavailabilityReason,
|
||||
/// The error message as returned from YouTube
|
||||
|
@ -78,9 +77,9 @@ pub enum ExtractionError {
|
|||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum UnavailabilityReason {
|
||||
/// Video/Channel is age restricted.
|
||||
/// Video is age restricted.
|
||||
///
|
||||
/// Video age restriction may be circumvented with the
|
||||
/// Age restriction may be circumvented with the
|
||||
/// [`ClientType::TvHtml5Embed`](crate::client::ClientType::TvHtml5Embed) client.
|
||||
AgeRestricted,
|
||||
/// Video was deleted or censored
|
||||
|
@ -209,7 +208,7 @@ impl ExtractionError {
|
|||
pub(crate) fn switch_client(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ExtractionError::Unavailable {
|
||||
ExtractionError::VideoUnavailable {
|
||||
reason: UnavailabilityReason::AgeRestricted
|
||||
| UnavailabilityReason::UnsupportedClient,
|
||||
..
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_with::{serde_as, DeserializeAs, DisplayFromStr, SerializeAs};
|
||||
use serde_with::{json::JsonString, serde_as, DeserializeAs, SerializeAs};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Range {
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[serde_as(as = "JsonString")]
|
||||
start: u32,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[serde_as(as = "JsonString")]
|
||||
end: u32,
|
||||
}
|
||||
|
||||
|
|
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
|
@ -312,7 +312,7 @@ fn get_player_error(#[case] id: &str, #[case] expect: UnavailabilityReason, rp:
|
|||
let err = tokio_test::block_on(rp.query().player(id)).unwrap_err();
|
||||
|
||||
match err {
|
||||
Error::Extraction(ExtractionError::Unavailable { reason, .. }) => {
|
||||
Error::Extraction(ExtractionError::VideoUnavailable { reason, .. }) => {
|
||||
assert_eq!(reason, expect, "got {err}")
|
||||
}
|
||||
_ => panic!("got {err}"),
|
||||
|
@ -1094,27 +1094,6 @@ fn channel_tab_not_found(#[case] tab: ChannelVideoTab, rp: RustyPipe) {
|
|||
assert!(channel.content.is_empty(), "got: {:?}", channel.content);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn channel_age_restriction(rp: RustyPipe) {
|
||||
let id = "UCbfnHqxXs_K3kvaH-WlNlig";
|
||||
|
||||
let res = tokio_test::block_on(rp.query().channel_videos(&id));
|
||||
if let Err(Error::Extraction(ExtractionError::Unavailable { reason, msg })) = res {
|
||||
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
|
||||
assert!(msg.starts_with("Laphroaig Whisky: "));
|
||||
} else {
|
||||
panic!("invalid res: {res:?}")
|
||||
}
|
||||
|
||||
let res = tokio_test::block_on(rp.query().channel_info(&id));
|
||||
if let Err(Error::Extraction(ExtractionError::Unavailable { reason, msg })) = res {
|
||||
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
|
||||
assert!(msg.starts_with("Laphroaig Whisky: "));
|
||||
} else {
|
||||
panic!("invalid res: {res:?}")
|
||||
}
|
||||
}
|
||||
|
||||
//#CHANNEL_RSS
|
||||
|
||||
#[cfg(feature = "rss")]
|
||||
|
@ -1389,10 +1368,9 @@ fn music_playlist_cont(#[case] id: &str, rp: RustyPipe) {
|
|||
assert_gte(track_count, 100, "tracks");
|
||||
|
||||
assert_eq!(track_count, playlist.tracks.count.unwrap());
|
||||
assert_gte(
|
||||
assert_eq!(
|
||||
usize::try_from(track_count).unwrap(),
|
||||
playlist.tracks.items.len(),
|
||||
"tracks",
|
||||
playlist.tracks.items.len()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2264,12 +2242,7 @@ fn music_new_videos(rp: RustyPipe) {
|
|||
validate::video_id(&video.id).unwrap();
|
||||
assert!(!video.name.is_empty());
|
||||
assert!(!video.cover.is_empty(), "got no cover");
|
||||
if let Some(view_count) = video.view_count {
|
||||
assert_gte(view_count, 1000, "views");
|
||||
} else {
|
||||
// Podcast episode: shows duration instead of view count
|
||||
assert!(video.duration.is_some(), "no view count or duration");
|
||||
}
|
||||
assert_gte(video.view_count.unwrap(), 1000, "views");
|
||||
assert!(video.is_video);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue