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,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> 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 grid = match content.c {
|
||||
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
||||
|
|
@ -191,7 +191,7 @@ impl MapResponse<Channel<Paginator<ChannelPlaylist>>> for response::Channel {
|
|||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> 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 grid = match content.c {
|
||||
response::channel::ChannelContent::GridRenderer { items } => Some(items),
|
||||
|
|
@ -222,7 +222,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
|||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> 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 meta = match content.c {
|
||||
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>(
|
||||
header: response::channel::Header,
|
||||
metadata: response::channel::Metadata,
|
||||
microformat: response::channel::Microformat,
|
||||
header: Option<response::channel::Header>,
|
||||
metadata: Option<response::channel::Metadata>,
|
||||
microformat: Option<response::channel::Microformat>,
|
||||
content: T,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
) -> 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 {
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
|
|
@ -494,39 +498,54 @@ fn map_channel<T>(
|
|||
}
|
||||
|
||||
fn map_channel_content(
|
||||
contents: response::channel::Contents,
|
||||
contents: Option<response::channel::Contents>,
|
||||
id: &str,
|
||||
) -> MapResult<response::channel::ChannelContent> {
|
||||
let mut tabs = contents.two_column_browse_results_renderer.tabs;
|
||||
let mut sectionlist = some_or_bail!(
|
||||
tabs.try_swap_remove(0),
|
||||
MapResult::error("no tab".to_owned())
|
||||
)
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer;
|
||||
alerts: Option<Vec<response::Alert>>,
|
||||
) -> Result<MapResult<response::channel::ChannelContent>, ExtractionError> {
|
||||
match contents {
|
||||
Some(contents) => {
|
||||
let mut tabs = contents.two_column_browse_results_renderer.tabs;
|
||||
let mut sectionlist = some_or_bail!(
|
||||
tabs.try_swap_remove(0),
|
||||
Ok(MapResult::error("no tab".to_owned()))
|
||||
)
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer;
|
||||
|
||||
if let Some(target_id) = sectionlist.target_id {
|
||||
// YouTube falls back to the featured page if the channel does not have a "videos" tab.
|
||||
// This is the case for YouTube Music channels.
|
||||
if target_id.starts_with(&format!("browse-feed{}featured", id)) {
|
||||
return MapResult::ok(response::channel::ChannelContent::None);
|
||||
if let Some(target_id) = sectionlist.target_id {
|
||||
// YouTube falls back to the featured page if the channel does not have a "videos" tab.
|
||||
// This is the case for YouTube Music channels.
|
||||
if target_id.starts_with(&format!("browse-feed{}featured", id)) {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,13 @@ impl RustyPipeQuery {
|
|||
Ok(mapres.c)
|
||||
}
|
||||
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())
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use serde_with::VecSkipError;
|
||||
use serde_with::{DefaultOnError, VecSkipError};
|
||||
|
||||
use super::ChannelBadge;
|
||||
use super::Thumbnails;
|
||||
use super::{Alert, ChannelBadge};
|
||||
use super::{ContentRenderer, ContentsRenderer, VideoListItem};
|
||||
use crate::serializer::ignore_any;
|
||||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||
|
|
@ -12,10 +12,13 @@ use crate::serializer::{text::Text, MapResult, VecLogError};
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Channel {
|
||||
pub header: Header,
|
||||
pub contents: Contents,
|
||||
pub metadata: Metadata,
|
||||
pub microformat: Microformat,
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub header: Option<Header>,
|
||||
pub contents: Option<Contents>,
|
||||
pub metadata: Option<Metadata>,
|
||||
pub microformat: Option<Microformat>,
|
||||
#[serde_as(as = "Option<DefaultOnError>")]
|
||||
pub alerts: Option<Vec<Alert>>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
|
|||
|
|
@ -313,6 +313,20 @@ pub enum VideoBadgeStyle {
|
|||
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
|
||||
|
||||
#[serde_as]
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ pub enum ExtractionError {
|
|||
VideoUnavailable(&'static str, String),
|
||||
#[error("Video is age restricted")]
|
||||
VideoAgeRestricted,
|
||||
#[error("Content is not available. Reason (from YT): {0}")]
|
||||
ContentUnavailable(String),
|
||||
#[error("Got no data from YouTube")]
|
||||
NoData,
|
||||
#[error("deserialization error: {0}")]
|
||||
Deserialization(#[from] serde_json::Error),
|
||||
#[error("got invalid data from YT: {0}")]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use chrono::{Datelike, Timelike};
|
|||
use rstest::rstest;
|
||||
|
||||
use rustypipe::client::{ClientType, RustyPipe};
|
||||
use rustypipe::error::{Error, ExtractionError};
|
||||
use rustypipe::model::richtext::ToPlaintext;
|
||||
use rustypipe::model::{
|
||||
AudioCodec, AudioFormat, Channel, SearchItem, Verification, VideoCodec, VideoFormat,
|
||||
|
|
@ -857,6 +858,24 @@ async fn channel_more(
|
|||
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
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
Reference in a new issue