fix: channel not found
This commit is contained in:
parent
012cde8b51
commit
ef35c48890
6 changed files with 108 additions and 43 deletions
|
|
@ -160,7 +160,7 @@ impl MapResponse<Channel<Paginator<ChannelVideo>>> for response::Channel {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
) -> Result<MapResult<Channel<Paginator<ChannelVideo>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<ChannelVideo>>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, id);
|
let content = map_channel_content(self.contents, id, self.alerts)?;
|
||||||
let mut warnings = content.warnings;
|
let mut warnings = content.warnings;
|
||||||
let grid = match content.c {
|
let grid = match content.c {
|
||||||
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
||||||
|
|
@ -191,7 +191,7 @@ impl MapResponse<Channel<Paginator<ChannelPlaylist>>> for response::Channel {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
) -> Result<MapResult<Channel<Paginator<ChannelPlaylist>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<ChannelPlaylist>>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, id);
|
let content = map_channel_content(self.contents, id, self.alerts)?;
|
||||||
let mut warnings = content.warnings;
|
let mut warnings = content.warnings;
|
||||||
let grid = match content.c {
|
let grid = match content.c {
|
||||||
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
||||||
|
|
@ -222,7 +222,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, id);
|
let content = map_channel_content(self.contents, id, self.alerts)?;
|
||||||
let mut warnings = content.warnings;
|
let mut warnings = content.warnings;
|
||||||
let meta = match content.c {
|
let meta = match content.c {
|
||||||
response::channel::ChannelContent::ChannelAboutFullMetadataRenderer(meta) => Some(meta),
|
response::channel::ChannelContent::ChannelAboutFullMetadataRenderer(meta) => Some(meta),
|
||||||
|
|
@ -419,14 +419,18 @@ fn map_vanity_url(url: &str, id: &str) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_channel<T>(
|
fn map_channel<T>(
|
||||||
header: response::channel::Header,
|
header: Option<response::channel::Header>,
|
||||||
metadata: response::channel::Metadata,
|
metadata: Option<response::channel::Metadata>,
|
||||||
microformat: response::channel::Microformat,
|
microformat: Option<response::channel::Microformat>,
|
||||||
content: T,
|
content: T,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
) -> Result<Channel<T>, ExtractionError> {
|
) -> Result<Channel<T>, ExtractionError> {
|
||||||
let metadata = metadata.channel_metadata_renderer;
|
let header = header.ok_or(ExtractionError::NoData)?;
|
||||||
|
let metadata = metadata
|
||||||
|
.ok_or(ExtractionError::NoData)?
|
||||||
|
.channel_metadata_renderer;
|
||||||
|
let microformat = microformat.ok_or(ExtractionError::NoData)?;
|
||||||
|
|
||||||
if metadata.external_id != id {
|
if metadata.external_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
|
|
@ -494,39 +498,54 @@ fn map_channel<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_channel_content(
|
fn map_channel_content(
|
||||||
contents: response::channel::Contents,
|
contents: Option<response::channel::Contents>,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> MapResult<response::channel::ChannelContent> {
|
alerts: Option<Vec<response::Alert>>,
|
||||||
let mut tabs = contents.two_column_browse_results_renderer.tabs;
|
) -> Result<MapResult<response::channel::ChannelContent>, ExtractionError> {
|
||||||
let mut sectionlist = some_or_bail!(
|
match contents {
|
||||||
tabs.try_swap_remove(0),
|
Some(contents) => {
|
||||||
MapResult::error("no tab".to_owned())
|
let mut tabs = contents.two_column_browse_results_renderer.tabs;
|
||||||
)
|
let mut sectionlist = some_or_bail!(
|
||||||
.tab_renderer
|
tabs.try_swap_remove(0),
|
||||||
.content
|
Ok(MapResult::error("no tab".to_owned()))
|
||||||
.section_list_renderer;
|
)
|
||||||
|
.tab_renderer
|
||||||
|
.content
|
||||||
|
.section_list_renderer;
|
||||||
|
|
||||||
if let Some(target_id) = sectionlist.target_id {
|
if let Some(target_id) = sectionlist.target_id {
|
||||||
// YouTube falls back to the featured page if the channel does not have a "videos" tab.
|
// YouTube falls back to the featured page if the channel does not have a "videos" tab.
|
||||||
// This is the case for YouTube Music channels.
|
// This is the case for YouTube Music channels.
|
||||||
if target_id.starts_with(&format!("browse-feed{}featured", id)) {
|
if target_id.starts_with(&format!("browse-feed{}featured", id)) {
|
||||||
return MapResult::ok(response::channel::ChannelContent::None);
|
return Ok(MapResult::ok(response::channel::ChannelContent::None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut itemsection = some_or_bail!(
|
||||||
|
sectionlist.contents.try_swap_remove(0),
|
||||||
|
Ok(MapResult::error("no sectionlist".to_owned()))
|
||||||
|
)
|
||||||
|
.item_section_renderer
|
||||||
|
.contents;
|
||||||
|
|
||||||
|
let content = some_or_bail!(
|
||||||
|
itemsection.try_swap_remove(0),
|
||||||
|
Ok(MapResult::error("no channel content".to_owned()))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(MapResult::ok(content))
|
||||||
}
|
}
|
||||||
|
None => match alerts {
|
||||||
|
Some(alerts) => Err(ExtractionError::ContentUnavailable(
|
||||||
|
alerts
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.alert_renderer.text)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" "),
|
||||||
|
)),
|
||||||
|
None => Err(ExtractionError::InvalidData("no contents".into())),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut itemsection = some_or_bail!(
|
|
||||||
sectionlist.contents.try_swap_remove(0),
|
|
||||||
MapResult::error("no sectionlist".to_owned())
|
|
||||||
)
|
|
||||||
.item_section_renderer
|
|
||||||
.contents;
|
|
||||||
|
|
||||||
let content = some_or_bail!(
|
|
||||||
itemsection.try_swap_remove(0),
|
|
||||||
MapResult::error("no channel content".to_owned())
|
|
||||||
);
|
|
||||||
|
|
||||||
MapResult::ok(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -1009,7 +1009,13 @@ impl RustyPipeQuery {
|
||||||
Ok(mapres.c)
|
Ok(mapres.c)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
create_report(Level::ERR, Some(e.to_string()), Vec::new());
|
match e {
|
||||||
|
ExtractionError::VideoUnavailable(_, _)
|
||||||
|
| ExtractionError::VideoAgeRestricted
|
||||||
|
| ExtractionError::ContentUnavailable(_)
|
||||||
|
| ExtractionError::NoData => (),
|
||||||
|
_ => create_report(Level::ERR, Some(e.to_string()), Vec::new()),
|
||||||
|
}
|
||||||
Err(e.into())
|
Err(e.into())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::VecSkipError;
|
use serde_with::{DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use super::ChannelBadge;
|
|
||||||
use super::Thumbnails;
|
use super::Thumbnails;
|
||||||
|
use super::{Alert, ChannelBadge};
|
||||||
use super::{ContentRenderer, ContentsRenderer, VideoListItem};
|
use super::{ContentRenderer, ContentsRenderer, VideoListItem};
|
||||||
use crate::serializer::ignore_any;
|
use crate::serializer::ignore_any;
|
||||||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||||
|
|
@ -12,10 +12,13 @@ use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub header: Header,
|
#[serde_as(as = "DefaultOnError")]
|
||||||
pub contents: Contents,
|
pub header: Option<Header>,
|
||||||
pub metadata: Metadata,
|
pub contents: Option<Contents>,
|
||||||
pub microformat: Microformat,
|
pub metadata: Option<Metadata>,
|
||||||
|
pub microformat: Option<Microformat>,
|
||||||
|
#[serde_as(as = "Option<DefaultOnError>")]
|
||||||
|
pub alerts: Option<Vec<Alert>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,20 @@ pub enum VideoBadgeStyle {
|
||||||
BadgeStyleTypeLiveNow,
|
BadgeStyleTypeLiveNow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Alert {
|
||||||
|
pub alert_renderer: AlertRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AlertRenderer {
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
// YouTube Music
|
// YouTube Music
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,10 @@ pub enum ExtractionError {
|
||||||
VideoUnavailable(&'static str, String),
|
VideoUnavailable(&'static str, String),
|
||||||
#[error("Video is age restricted")]
|
#[error("Video is age restricted")]
|
||||||
VideoAgeRestricted,
|
VideoAgeRestricted,
|
||||||
|
#[error("Content is not available. Reason (from YT): {0}")]
|
||||||
|
ContentUnavailable(String),
|
||||||
|
#[error("Got no data from YouTube")]
|
||||||
|
NoData,
|
||||||
#[error("deserialization error: {0}")]
|
#[error("deserialization error: {0}")]
|
||||||
Deserialization(#[from] serde_json::Error),
|
Deserialization(#[from] serde_json::Error),
|
||||||
#[error("got invalid data from YT: {0}")]
|
#[error("got invalid data from YT: {0}")]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use chrono::{Datelike, Timelike};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use rustypipe::client::{ClientType, RustyPipe};
|
use rustypipe::client::{ClientType, RustyPipe};
|
||||||
|
use rustypipe::error::{Error, ExtractionError};
|
||||||
use rustypipe::model::richtext::ToPlaintext;
|
use rustypipe::model::richtext::ToPlaintext;
|
||||||
use rustypipe::model::{
|
use rustypipe::model::{
|
||||||
AudioCodec, AudioFormat, Channel, SearchItem, Verification, VideoCodec, VideoFormat,
|
AudioCodec, AudioFormat, Channel, SearchItem, Verification, VideoCodec, VideoFormat,
|
||||||
|
|
@ -857,6 +858,24 @@ async fn channel_more(
|
||||||
assert_channel(&channel_info, id, name);
|
assert_channel(&channel_info, id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::gaming("UCOpNcN46UbXVtpKMrmU4Abg", false)]
|
||||||
|
#[case::not_found("UCOpNcN46UbXVtpKMrmU4Abx", true)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_error(#[case] id: &str, #[case] not_found: bool) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let err = rp.query().channel_videos(&id).await.unwrap_err();
|
||||||
|
|
||||||
|
if not_found {
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Extraction(ExtractionError::ContentUnavailable(_))
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
assert!(matches!(err, Error::Extraction(ExtractionError::NoData)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//#CHANNEL_RSS
|
//#CHANNEL_RSS
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
Reference in a new issue