Compare commits
5 commits
4d124c6d98
...
53a8ec680a
Author | SHA1 | Date | |
---|---|---|---|
53a8ec680a | |||
a5ec111af4 | |||
596b9c4d4a | |||
1a22dc835a | |||
b145080631 |
19 changed files with 70225 additions and 96 deletions
|
@ -32,7 +32,7 @@ quick-js-dtp = { version = "0.4.1", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
once_cell = "1.12.0"
|
once_cell = "1.12.0"
|
||||||
regex = "1.6.0"
|
regex = "1.6.0"
|
||||||
fancy-regex = "0.11.0"
|
fancy-regex = "0.12.0"
|
||||||
thiserror = "1.0.36"
|
thiserror = "1.0.36"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
reqwest = { version = "0.11.11", default-features = false, features = [
|
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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.82"
|
serde_json = "1.0.82"
|
||||||
serde_with = { version = "3.0.0", default-features = false, features = [
|
serde_with = { version = "3.0.0", default-features = false, features = [
|
||||||
|
"alloc",
|
||||||
"macros",
|
"macros",
|
||||||
"json",
|
|
||||||
] }
|
] }
|
||||||
serde_plain = "1.0.1"
|
serde_plain = "1.0.1"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
@ -59,7 +59,7 @@ ress = "0.11.4"
|
||||||
phf = "0.11.1"
|
phf = "0.11.1"
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
quick-xml = { version = "0.30.0", features = ["serialize"], optional = true }
|
quick-xml = { version = "0.31.0", features = ["serialize"], optional = true }
|
||||||
tracing = { version = "0.1.37", features = ["log"] }
|
tracing = { version = "0.1.37", features = ["log"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -5,6 +5,7 @@ use time::OffsetDateTime;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
client::response::YouTubeListItem,
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{
|
model::{
|
||||||
paginator::{ContinuationEndpoint, Paginator},
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
|
@ -290,7 +291,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
id: &str,
|
||||||
_lang: Language,
|
_lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
_visitor_data: Option<&str>,
|
_visitor_data: Option<&str>,
|
||||||
|
@ -299,11 +300,21 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||||
// and it allows parsing the country name.
|
// and it allows parsing the country name.
|
||||||
let lang = Language::En;
|
let lang = Language::En;
|
||||||
|
|
||||||
let ep = self
|
let ep = match self {
|
||||||
.on_response_received_endpoints
|
response::ChannelAbout::ReceivedEndpoints {
|
||||||
.into_iter()
|
on_response_received_endpoints,
|
||||||
.next()
|
} => on_response_received_endpoints
|
||||||
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?;
|
.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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
let continuations = ep.append_continuation_items_action.continuation_items;
|
let continuations = ep.append_continuation_items_action.continuation_items;
|
||||||
let about = continuations
|
let about = continuations
|
||||||
.c
|
.c
|
||||||
|
@ -483,13 +494,6 @@ fn map_channel_content(
|
||||||
match contents {
|
match contents {
|
||||||
Some(contents) => {
|
Some(contents) => {
|
||||||
let tabs = contents.two_column_browse_results_renderer.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,
|
let cmp_url_suffix = |endpoint: &response::channel::ChannelTabEndpoint,
|
||||||
expect: &str| {
|
expect: &str| {
|
||||||
endpoint
|
endpoint
|
||||||
|
@ -504,24 +508,46 @@ fn map_channel_content(
|
||||||
let mut featured_tab = false;
|
let mut featured_tab = false;
|
||||||
|
|
||||||
for tab in &tabs {
|
for tab in &tabs {
|
||||||
if cmp_url_suffix(&tab.tab_renderer.endpoint, "/featured")
|
if let Some(endpoint) = &tab.tab_renderer.endpoint {
|
||||||
&& (tab.tab_renderer.content.section_list_renderer.is_some()
|
if cmp_url_suffix(endpoint, "/featured")
|
||||||
|| tab.tab_renderer.content.rich_grid_renderer.is_some())
|
&& (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(&tab.tab_renderer.endpoint, "/shorts") {
|
featured_tab = true;
|
||||||
has_shorts = true;
|
} else if cmp_url_suffix(endpoint, "/shorts") {
|
||||||
} else if cmp_url_suffix(&tab.tab_renderer.endpoint, "/streams") {
|
has_shorts = true;
|
||||||
has_live = true;
|
} else if cmp_url_suffix(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().find_map(|tab| {
|
let channel_content = tabs
|
||||||
tab.tab_renderer
|
.into_iter()
|
||||||
.content
|
.filter(|t| t.tab_renderer.endpoint.is_some())
|
||||||
.rich_grid_renderer
|
.find_map(|tab| {
|
||||||
.or(tab.tab_renderer.content.section_list_renderer)
|
tab.tab_renderer
|
||||||
});
|
.content
|
||||||
|
.rich_grid_renderer
|
||||||
|
.or(tab.tab_renderer.content.section_list_renderer)
|
||||||
|
});
|
||||||
|
|
||||||
// YouTube may show the "Featured" tab if the requested tab is empty/does not exist
|
// YouTube may show the "Featured" tab if the requested tab is empty/does not exist
|
||||||
let content = if featured_tab {
|
let content = if featured_tab {
|
||||||
|
@ -530,9 +556,10 @@ fn map_channel_content(
|
||||||
match channel_content {
|
match channel_content {
|
||||||
Some(list) => list.contents,
|
Some(list) => list.contents,
|
||||||
None => {
|
None => {
|
||||||
return Err(ExtractionError::InvalidData(
|
return Err(ExtractionError::NotFound {
|
||||||
"could not extract content".into(),
|
id: id.to_owned(),
|
||||||
))
|
msg: "no tabs".into(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -632,6 +659,7 @@ mod tests {
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapResponse},
|
client::{response, MapResponse},
|
||||||
|
error::{ExtractionError, UnavailabilityReason},
|
||||||
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
||||||
param::{ChannelOrder, ChannelVideoTab, Language},
|
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
|
@ -649,7 +677,7 @@ mod tests {
|
||||||
#[case::upcoming("videos_upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
#[case::upcoming("videos_upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
||||||
#[case::richgrid("videos_20221011_richgrid", "UCh8gHdtzO2tXd593_bjErWg")]
|
#[case::richgrid("videos_20221011_richgrid", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
#[case::richgrid2("videos_20221011_richgrid2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
#[case::richgrid2("videos_20221011_richgrid2", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
#[case::richgrid2("videos_20230415_coachella", "UCHF66aWLOxBW4l6VkSrS3cQ")]
|
#[case::coachella("videos_20230415_coachella", "UCHF66aWLOxBW4l6VkSrS3cQ")]
|
||||||
#[case::shorts("shorts", "UCh8gHdtzO2tXd593_bjErWg")]
|
#[case::shorts("shorts", "UCh8gHdtzO2tXd593_bjErWg")]
|
||||||
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
#[case::livestreams("livestreams", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
||||||
|
@ -678,6 +706,23 @@ 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]
|
#[rstest]
|
||||||
fn map_channel_playlists() {
|
fn map_channel_playlists() {
|
||||||
let json_path = path!(*TESTFILES / "channel" / "channel_playlists.json");
|
let json_path = path!(*TESTFILES / "channel" / "channel_playlists.json");
|
||||||
|
|
|
@ -112,13 +112,14 @@ mod tests {
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::default("default")]
|
#[case::default("default")]
|
||||||
|
#[case::default("w_podcasts")]
|
||||||
fn map_music_new_videos(#[case] name: &str) {
|
fn map_music_new_videos(#[case] name: &str) {
|
||||||
let json_path = path!(*TESTFILES / "music_new" / format!("videos_{name}.json"));
|
let json_path = path!(*TESTFILES / "music_new" / format!("videos_{name}.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
let new_albums: response::MusicNew =
|
let new_videos: response::MusicNew =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Vec<TrackItem>> = new_albums
|
let map_res: MapResult<Vec<TrackItem>> = new_videos
|
||||||
.map_response("", Language::En, None, None)
|
.map_response("", Language::En, None, None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl RustyPipeQuery {
|
||||||
|
|
||||||
match tv_res {
|
match tv_res {
|
||||||
// Output desktop client error if the tv client is unsupported
|
// Output desktop client error if the tv client is unsupported
|
||||||
Err(Error::Extraction(ExtractionError::VideoUnavailable {
|
Err(Error::Extraction(ExtractionError::Unavailable {
|
||||||
reason: UnavailabilityReason::UnsupportedClient,
|
reason: UnavailabilityReason::UnsupportedClient,
|
||||||
..
|
..
|
||||||
})) => Err(Error::Extraction(e)),
|
})) => Err(Error::Extraction(e)),
|
||||||
|
@ -183,7 +183,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
return Err(ExtractionError::VideoUnavailable { reason, msg });
|
return Err(ExtractionError::Unavailable { reason, msg });
|
||||||
}
|
}
|
||||||
response::player::PlayabilityStatus::LoginRequired { reason, messages } => {
|
response::player::PlayabilityStatus::LoginRequired { reason, messages } => {
|
||||||
let mut msg = reason;
|
let mut msg = reason;
|
||||||
|
@ -205,10 +205,10 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
return Err(ExtractionError::VideoUnavailable { reason, msg });
|
return Err(ExtractionError::Unavailable { reason, msg });
|
||||||
}
|
}
|
||||||
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
|
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
|
||||||
return Err(ExtractionError::VideoUnavailable {
|
return Err(ExtractionError::Unavailable {
|
||||||
reason: UnavailabilityReason::OfflineLivestream,
|
reason: UnavailabilityReason::OfflineLivestream,
|
||||||
msg: reason,
|
msg: reason,
|
||||||
});
|
});
|
||||||
|
@ -216,7 +216,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
response::player::PlayabilityStatus::Error { reason } => {
|
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 (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"
|
// reason: "This video is unavailable"
|
||||||
return Err(ExtractionError::VideoUnavailable {
|
return Err(ExtractionError::Unavailable {
|
||||||
reason: UnavailabilityReason::Deleted,
|
reason: UnavailabilityReason::Deleted,
|
||||||
msg: reason,
|
msg: reason,
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub(crate) struct TabRendererWrap {
|
||||||
pub(crate) struct TabRenderer {
|
pub(crate) struct TabRenderer {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub content: TabContent,
|
pub content: TabContent,
|
||||||
pub endpoint: ChannelTabEndpoint,
|
pub endpoint: Option<ChannelTabEndpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -148,10 +148,16 @@ pub(crate) struct MicroformatDataRenderer {
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(untagged)]
|
||||||
pub(crate) struct ChannelAbout {
|
pub(crate) enum ChannelAbout {
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub on_response_received_endpoints: Vec<ContinuationActionWrap<AboutChannelRendererWrap>>,
|
ReceivedEndpoints {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
on_response_received_endpoints: Vec<ContinuationActionWrap<AboutChannelRendererWrap>>,
|
||||||
|
},
|
||||||
|
Content {
|
||||||
|
contents: Option<Contents>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
@ -54,7 +54,7 @@ use serde::{
|
||||||
de::{IgnoredAny, Visitor},
|
de::{IgnoredAny, Visitor},
|
||||||
Deserialize,
|
Deserialize,
|
||||||
};
|
};
|
||||||
use serde_with::{json::JsonString, serde_as, VecSkipError};
|
use serde_with::{serde_as, DisplayFromStr, VecSkipError};
|
||||||
|
|
||||||
use crate::error::ExtractionError;
|
use crate::error::ExtractionError;
|
||||||
use crate::serializer::{text::Text, MapResult, VecSkipErrorWrap};
|
use crate::serializer::{text::Text, MapResult, VecSkipErrorWrap};
|
||||||
|
@ -202,7 +202,7 @@ pub(crate) struct ResponseContext {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct Continuation {
|
pub(crate) struct Continuation {
|
||||||
/// Number of search results
|
/// Number of search results
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub estimated_results: Option<u64>,
|
pub estimated_results: Option<u64>,
|
||||||
#[serde(
|
#[serde(
|
||||||
alias = "onResponseReceivedCommands",
|
alias = "onResponseReceivedCommands",
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
text::{Text, TextComponent, TextComponents},
|
text::{Text, TextComponent, TextComponents},
|
||||||
MapResult,
|
MapResult,
|
||||||
},
|
},
|
||||||
util::{self, dictionary},
|
util::{self, dictionary, timeago},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
@ -780,10 +780,17 @@ impl MusicListMapper {
|
||||||
.map(|st| map_album_type(st.first_str(), self.lang))
|
.map(|st| map_album_type(st.first_str(), self.lang))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let (artists, by_va) = map_artists(subtitle_p2);
|
let (mut artists, by_va) = map_artists(subtitle_p2);
|
||||||
|
|
||||||
let artist_id = map_artist_id_fallback(item.menu, artists.first());
|
let artist_id = map_artist_id_fallback(item.menu, artists.first());
|
||||||
|
|
||||||
|
// 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 year =
|
let year =
|
||||||
subtitle_p3.and_then(|st| util::parse_numeric(st.first_str()).ok());
|
subtitle_p3.and_then(|st| util::parse_numeric(st.first_str()).ok());
|
||||||
|
|
||||||
|
@ -855,23 +862,38 @@ impl MusicListMapper {
|
||||||
match item.navigation_endpoint.music_page() {
|
match item.navigation_endpoint.music_page() {
|
||||||
Some(music_page) => match music_page.typ {
|
Some(music_page) => match music_page.typ {
|
||||||
MusicPageType::Track { vtype } => {
|
MusicPageType::Track { vtype } => {
|
||||||
let (artists, by_va) = map_artists(subtitle_p1);
|
let (artists, by_va, view_count, duration) = if vtype == MusicVideoType::Episode
|
||||||
|
{
|
||||||
self.items.push(MusicItem::Track(TrackItem {
|
let (artists, by_va) = map_artists(subtitle_p2);
|
||||||
id: music_page.id,
|
let duration = subtitle_p1.and_then(|s| {
|
||||||
name: item.title,
|
timeago::parse_video_duration_or_warn(
|
||||||
duration: None,
|
self.lang,
|
||||||
cover: item.thumbnail_renderer.into(),
|
s.first_str(),
|
||||||
artist_id: artists.first().and_then(|a| a.id.clone()),
|
&mut self.warnings,
|
||||||
artists,
|
)
|
||||||
album: None,
|
});
|
||||||
view_count: subtitle_p2.and_then(|c| {
|
(artists, by_va, None, duration)
|
||||||
|
} else {
|
||||||
|
let (artists, by_va) = map_artists(subtitle_p1);
|
||||||
|
let view_count = subtitle_p2.and_then(|c| {
|
||||||
util::parse_large_numstr_or_warn(
|
util::parse_large_numstr_or_warn(
|
||||||
c.first_str(),
|
c.first_str(),
|
||||||
self.lang,
|
self.lang,
|
||||||
&mut self.warnings,
|
&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(),
|
is_video: vtype.is_video(),
|
||||||
track_nr: None,
|
track_nr: None,
|
||||||
by_va,
|
by_va,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::ops::Range;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{json::JsonString, DefaultOnError};
|
use serde_with::{DefaultOnError, DisplayFromStr};
|
||||||
|
|
||||||
use super::{ResponseContext, Thumbnails};
|
use super::{ResponseContext, Thumbnails};
|
||||||
use crate::serializer::{text::Text, MapResult};
|
use crate::serializer::{text::Text, MapResult};
|
||||||
|
@ -78,7 +78,7 @@ pub(crate) struct ErrorMessage {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct StreamingData {
|
pub(crate) struct StreamingData {
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub expires_in_seconds: u32,
|
pub expires_in_seconds: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub formats: MapResult<Vec<Format>>,
|
pub formats: MapResult<Vec<Format>>,
|
||||||
|
@ -106,7 +106,7 @@ pub(crate) struct Format {
|
||||||
|
|
||||||
pub width: Option<u32>,
|
pub width: Option<u32>,
|
||||||
pub height: Option<u32>,
|
pub height: Option<u32>,
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub approx_duration_ms: Option<u32>,
|
pub approx_duration_ms: Option<u32>,
|
||||||
|
|
||||||
#[serde_as(as = "Option<crate::serializer::Range>")]
|
#[serde_as(as = "Option<crate::serializer::Range>")]
|
||||||
|
@ -114,7 +114,7 @@ pub(crate) struct Format {
|
||||||
#[serde_as(as = "Option<crate::serializer::Range>")]
|
#[serde_as(as = "Option<crate::serializer::Range>")]
|
||||||
pub init_range: Option<Range<u32>>,
|
pub init_range: Option<Range<u32>>,
|
||||||
|
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub content_length: Option<u64>,
|
pub content_length: Option<u64>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -129,7 +129,7 @@ pub(crate) struct Format {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
pub audio_quality: Option<AudioQuality>,
|
pub audio_quality: Option<AudioQuality>,
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub audio_sample_rate: Option<u32>,
|
pub audio_sample_rate: Option<u32>,
|
||||||
pub audio_channels: Option<u8>,
|
pub audio_channels: Option<u8>,
|
||||||
pub loudness_db: Option<f32>,
|
pub loudness_db: Option<f32>,
|
||||||
|
@ -237,7 +237,7 @@ pub(crate) struct CaptionTrack {
|
||||||
pub(crate) struct VideoDetails {
|
pub(crate) struct VideoDetails {
|
||||||
pub video_id: String,
|
pub video_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub length_seconds: u32,
|
pub length_seconds: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub keywords: Vec<String>,
|
pub keywords: Vec<String>,
|
||||||
|
@ -245,7 +245,7 @@ pub(crate) struct VideoDetails {
|
||||||
pub short_description: Option<String>,
|
pub short_description: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub thumbnail: Thumbnails,
|
pub thumbnail: Thumbnails,
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub view_count: u64,
|
pub view_count: u64,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub is_live_content: bool,
|
pub is_live_content: bool,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use serde::{
|
||||||
de::{IgnoredAny, Visitor},
|
de::{IgnoredAny, Visitor},
|
||||||
Deserialize,
|
Deserialize,
|
||||||
};
|
};
|
||||||
use serde_with::{json::JsonString, serde_as};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
|
|
||||||
use super::{video_item::YouTubeListRendererWrap, ResponseContext};
|
use super::{video_item::YouTubeListRendererWrap, ResponseContext};
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use super::{video_item::YouTubeListRendererWrap, ResponseContext};
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct Search {
|
pub(crate) struct Search {
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub estimated_results: Option<u64>,
|
pub estimated_results: Option<u64>,
|
||||||
pub contents: Contents,
|
pub contents: Contents,
|
||||||
pub response_context: ResponseContext,
|
pub response_context: ResponseContext,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{
|
use serde_with::{
|
||||||
json::JsonString, rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError,
|
rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError,
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
@ -69,6 +69,14 @@ pub(crate) enum YouTubeListItem {
|
||||||
contents: MapResult<Vec<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
|
/// No video list item (e.g. ad) or unimplemented item
|
||||||
///
|
///
|
||||||
/// Unimplemented:
|
/// Unimplemented:
|
||||||
|
@ -154,7 +162,7 @@ pub(crate) struct PlaylistVideoRenderer {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(rename = "shortBylineText")]
|
#[serde(rename = "shortBylineText")]
|
||||||
pub channel: TextComponent,
|
pub channel: TextComponent,
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub length_seconds: Option<u32>,
|
pub length_seconds: Option<u32>,
|
||||||
/// Regular video: `["29K views", " • ", "13 years ago"]`
|
/// Regular video: `["29K views", " • ", "13 years ago"]`
|
||||||
/// Livestream: `["66K", " watching"]`
|
/// Livestream: `["66K", " watching"]`
|
||||||
|
@ -184,7 +192,7 @@ pub(crate) struct PlaylistRenderer {
|
||||||
/// The first item of this list contains the playlist thumbnail,
|
/// The first item of this list contains the playlist thumbnail,
|
||||||
/// subsequent items contain very small thumbnails of the next playlist videos
|
/// subsequent items contain very small thumbnails of the next playlist videos
|
||||||
pub thumbnails: Option<Vec<Thumbnails>>,
|
pub thumbnails: Option<Vec<Thumbnails>>,
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub video_count: Option<u64>,
|
pub video_count: Option<u64>,
|
||||||
#[serde_as(as = "Option<Text>")]
|
#[serde_as(as = "Option<Text>")]
|
||||||
pub video_count_short_text: Option<String>,
|
pub video_count_short_text: Option<String>,
|
||||||
|
@ -240,7 +248,7 @@ pub(crate) struct YouTubeListRenderer {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct UpcomingEventData {
|
pub(crate) struct UpcomingEventData {
|
||||||
/// Unixtime in seconds
|
/// Unixtime in seconds
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub start_time: i64,
|
pub start_time: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -704,7 +712,7 @@ impl YouTubeListMapper<YouTubeItem> {
|
||||||
self.warnings.append(&mut contents.warnings);
|
self.warnings.append(&mut contents.warnings);
|
||||||
contents.c.into_iter().for_each(|it| self.map_item(it));
|
contents.c.into_iter().for_each(|it| self.map_item(it));
|
||||||
}
|
}
|
||||||
YouTubeListItem::None => {}
|
YouTubeListItem::None | YouTubeListItem::ChannelAgeGateRenderer { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,906 @@
|
||||||
|
---
|
||||||
|
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;
|
let video_id = current_video_endpoint.watch_endpoint.video_id;
|
||||||
if id != video_id {
|
if id != video_id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
"got wrong playlist id {video_id}, expected {id}"
|
"got wrong video id {video_id}, expected {id}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,9 +572,10 @@ mod tests {
|
||||||
#[case::chapters("chapters", "nFDBxBUfE74")]
|
#[case::chapters("chapters", "nFDBxBUfE74")]
|
||||||
#[case::live("live", "86YLFOog4GM")]
|
#[case::live("live", "86YLFOog4GM")]
|
||||||
#[case::agegate("agegate", "HRKu0cvrr_o")]
|
#[case::agegate("agegate", "HRKu0cvrr_o")]
|
||||||
#[case::newdesc("20220924_newdesc", "ZeerrnuLi5E")]
|
#[case::ab_newdesc("20220924_newdesc", "ZeerrnuLi5E")]
|
||||||
#[case::new_cont("20221011_new_continuation", "ZeerrnuLi5E")]
|
#[case::ab_new_cont("20221011_new_continuation", "ZeerrnuLi5E")]
|
||||||
#[case::no_recommends("20221011_rec_isr", "nFDBxBUfE74")]
|
#[case::ab_no_recommends("20221011_rec_isr", "nFDBxBUfE74")]
|
||||||
|
#[case::ab_new_likes("20231103_likes", "ZeerrnuLi5E")]
|
||||||
fn map_video_details(#[case] name: &str, #[case] id: &str) {
|
fn map_video_details(#[case] name: &str, #[case] id: &str) {
|
||||||
let json_path = path!(*TESTFILES / "video_details" / format!("video_details_{name}.json"));
|
let json_path = path!(*TESTFILES / "video_details" / format!("video_details_{name}.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
15
src/error.rs
15
src/error.rs
|
@ -24,14 +24,15 @@ pub enum Error {
|
||||||
/// Error extracting content from YouTube
|
/// Error extracting content from YouTube
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum ExtractionError {
|
pub enum ExtractionError {
|
||||||
/// Video cannot be extracted with RustyPipe
|
/// Content cannot be extracted with RustyPipe
|
||||||
///
|
///
|
||||||
/// Reasons include:
|
/// Reasons include:
|
||||||
/// - Deletion/Censorship
|
/// - Deletion/Censorship
|
||||||
/// - Private video that requires a Google account
|
/// - Age restriction
|
||||||
|
/// - Private video
|
||||||
/// - DRM (Movies and TV shows)
|
/// - DRM (Movies and TV shows)
|
||||||
#[error("video cant be played because it is {reason}. Reason (from YT): {msg}")]
|
#[error("content unavailable because it is {reason}. Reason (from YT): {msg}")]
|
||||||
VideoUnavailable {
|
Unavailable {
|
||||||
/// Reason why the video could not be extracted
|
/// Reason why the video could not be extracted
|
||||||
reason: UnavailabilityReason,
|
reason: UnavailabilityReason,
|
||||||
/// The error message as returned from YouTube
|
/// The error message as returned from YouTube
|
||||||
|
@ -77,9 +78,9 @@ pub enum ExtractionError {
|
||||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum UnavailabilityReason {
|
pub enum UnavailabilityReason {
|
||||||
/// Video is age restricted.
|
/// Video/Channel is age restricted.
|
||||||
///
|
///
|
||||||
/// Age restriction may be circumvented with the
|
/// Video age restriction may be circumvented with the
|
||||||
/// [`ClientType::TvHtml5Embed`](crate::client::ClientType::TvHtml5Embed) client.
|
/// [`ClientType::TvHtml5Embed`](crate::client::ClientType::TvHtml5Embed) client.
|
||||||
AgeRestricted,
|
AgeRestricted,
|
||||||
/// Video was deleted or censored
|
/// Video was deleted or censored
|
||||||
|
@ -208,7 +209,7 @@ impl ExtractionError {
|
||||||
pub(crate) fn switch_client(&self) -> bool {
|
pub(crate) fn switch_client(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
ExtractionError::VideoUnavailable {
|
ExtractionError::Unavailable {
|
||||||
reason: UnavailabilityReason::AgeRestricted
|
reason: UnavailabilityReason::AgeRestricted
|
||||||
| UnavailabilityReason::UnsupportedClient,
|
| UnavailabilityReason::UnsupportedClient,
|
||||||
..
|
..
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serde_with::{json::JsonString, serde_as, DeserializeAs, SerializeAs};
|
use serde_with::{serde_as, DeserializeAs, DisplayFromStr, SerializeAs};
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Range {
|
pub struct Range {
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
start: u32,
|
start: u32,
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
end: u32,
|
end: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1068
testfiles/channel/channel_agegate.json
Normal file
1068
testfiles/channel/channel_agegate.json
Normal file
File diff suppressed because it is too large
Load diff
44183
testfiles/music_new/videos_w_podcasts.json
Normal file
44183
testfiles/music_new/videos_w_podcasts.json
Normal file
File diff suppressed because it is too large
Load diff
20913
testfiles/video_details/video_details_20231103_likes.json
Normal file
20913
testfiles/video_details/video_details_20231103_likes.json
Normal file
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();
|
let err = tokio_test::block_on(rp.query().player(id)).unwrap_err();
|
||||||
|
|
||||||
match err {
|
match err {
|
||||||
Error::Extraction(ExtractionError::VideoUnavailable { reason, .. }) => {
|
Error::Extraction(ExtractionError::Unavailable { reason, .. }) => {
|
||||||
assert_eq!(reason, expect, "got {err}")
|
assert_eq!(reason, expect, "got {err}")
|
||||||
}
|
}
|
||||||
_ => panic!("got {err}"),
|
_ => panic!("got {err}"),
|
||||||
|
@ -1094,6 +1094,27 @@ fn channel_tab_not_found(#[case] tab: ChannelVideoTab, rp: RustyPipe) {
|
||||||
assert!(channel.content.is_empty(), "got: {:?}", channel.content);
|
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
|
//#CHANNEL_RSS
|
||||||
|
|
||||||
#[cfg(feature = "rss")]
|
#[cfg(feature = "rss")]
|
||||||
|
@ -1368,9 +1389,10 @@ fn music_playlist_cont(#[case] id: &str, rp: RustyPipe) {
|
||||||
assert_gte(track_count, 100, "tracks");
|
assert_gte(track_count, 100, "tracks");
|
||||||
|
|
||||||
assert_eq!(track_count, playlist.tracks.count.unwrap());
|
assert_eq!(track_count, playlist.tracks.count.unwrap());
|
||||||
assert_eq!(
|
assert_gte(
|
||||||
usize::try_from(track_count).unwrap(),
|
usize::try_from(track_count).unwrap(),
|
||||||
playlist.tracks.items.len()
|
playlist.tracks.items.len(),
|
||||||
|
"tracks",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2242,7 +2264,12 @@ fn music_new_videos(rp: RustyPipe) {
|
||||||
validate::video_id(&video.id).unwrap();
|
validate::video_id(&video.id).unwrap();
|
||||||
assert!(!video.name.is_empty());
|
assert!(!video.name.is_empty());
|
||||||
assert!(!video.cover.is_empty(), "got no cover");
|
assert!(!video.cover.is_empty(), "got no cover");
|
||||||
assert_gte(video.view_count.unwrap(), 1000, "views");
|
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!(video.is_video);
|
assert!(video.is_video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue