feat: add custom error types, remove anyhow
This commit is contained in:
parent
1297bcb641
commit
a3e3269fb3
16 changed files with 385 additions and 184 deletions
|
|
@ -1,9 +1,9 @@
|
|||
use anyhow::{anyhow, bail, Result};
|
||||
use chrono::TimeZone;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
Channel, ChannelInfo, ChannelOrder, ChannelPlaylist, ChannelVideo, Language, Paginator,
|
||||
},
|
||||
|
|
@ -43,7 +43,7 @@ impl RustyPipeQuery {
|
|||
pub async fn channel_videos(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
) -> Result<Channel<Paginator<ChannelVideo>>> {
|
||||
) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
|
||||
self.channel_videos_ordered(channel_id, ChannelOrder::default())
|
||||
.await
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ impl RustyPipeQuery {
|
|||
&self,
|
||||
channel_id: &str,
|
||||
order: ChannelOrder,
|
||||
) -> Result<Channel<Paginator<ChannelVideo>>> {
|
||||
) -> Result<Channel<Paginator<ChannelVideo>>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
|
|
@ -77,7 +77,7 @@ impl RustyPipeQuery {
|
|||
pub async fn channel_videos_continuation(
|
||||
&self,
|
||||
ctoken: &str,
|
||||
) -> Result<Paginator<ChannelVideo>> {
|
||||
) -> Result<Paginator<ChannelVideo>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
|
|
@ -97,7 +97,7 @@ impl RustyPipeQuery {
|
|||
pub async fn channel_playlists(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
) -> Result<Channel<Paginator<ChannelPlaylist>>> {
|
||||
) -> Result<Channel<Paginator<ChannelPlaylist>>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
|
|
@ -118,7 +118,7 @@ impl RustyPipeQuery {
|
|||
pub async fn channel_playlists_continuation(
|
||||
&self,
|
||||
ctoken: &str,
|
||||
) -> Result<Paginator<ChannelPlaylist>> {
|
||||
) -> Result<Paginator<ChannelPlaylist>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
|
|
@ -135,7 +135,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn channel_info(&self, channel_id: &str) -> Result<Channel<ChannelInfo>> {
|
||||
pub async fn channel_info(&self, channel_id: &str) -> Result<Channel<ChannelInfo>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QChannel {
|
||||
context,
|
||||
|
|
@ -160,7 +160,7 @@ impl MapResponse<Channel<Paginator<ChannelVideo>>> for response::Channel {
|
|||
id: &str,
|
||||
lang: crate::model::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Channel<Paginator<ChannelVideo>>>> {
|
||||
) -> Result<MapResult<Channel<Paginator<ChannelVideo>>>, ExtractionError> {
|
||||
let content = map_channel_content(self.contents, id);
|
||||
let mut warnings = content.warnings;
|
||||
let grid = match content.c {
|
||||
|
|
@ -191,7 +191,7 @@ impl MapResponse<Channel<Paginator<ChannelPlaylist>>> for response::Channel {
|
|||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Channel<Paginator<ChannelPlaylist>>>> {
|
||||
) -> Result<MapResult<Channel<Paginator<ChannelPlaylist>>>, ExtractionError> {
|
||||
let content = map_channel_content(self.contents, id);
|
||||
let mut warnings = content.warnings;
|
||||
let grid = match content.c {
|
||||
|
|
@ -222,7 +222,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
|||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Channel<ChannelInfo>>> {
|
||||
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
||||
let content = map_channel_content(self.contents, id);
|
||||
let mut warnings = content.warnings;
|
||||
let meta = match content.c {
|
||||
|
|
@ -278,11 +278,11 @@ impl MapResponse<Paginator<ChannelVideo>> for response::ChannelCont {
|
|||
_id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<ChannelVideo>>> {
|
||||
) -> Result<MapResult<Paginator<ChannelVideo>>, ExtractionError> {
|
||||
let mut actions = self.on_response_received_actions;
|
||||
let res = some_or_bail!(
|
||||
actions.try_swap_remove(0),
|
||||
Err(anyhow!("no received action"))
|
||||
Err(ExtractionError::InvalidData("no received action".into()))
|
||||
)
|
||||
.append_continuation_items_action
|
||||
.continuation_items;
|
||||
|
|
@ -297,11 +297,11 @@ impl MapResponse<Paginator<ChannelPlaylist>> for response::ChannelCont {
|
|||
_id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<ChannelPlaylist>>> {
|
||||
) -> Result<MapResult<Paginator<ChannelPlaylist>>, ExtractionError> {
|
||||
let mut actions = self.on_response_received_actions;
|
||||
let res = some_or_bail!(
|
||||
actions.try_swap_remove(0),
|
||||
Err(anyhow!("no received action"))
|
||||
Err(ExtractionError::InvalidData("no received action".into()))
|
||||
)
|
||||
.append_continuation_items_action
|
||||
.continuation_items;
|
||||
|
|
@ -423,15 +423,14 @@ fn map_channel<T>(
|
|||
content: T,
|
||||
id: &str,
|
||||
lang: Language,
|
||||
) -> Result<Channel<T>> {
|
||||
) -> Result<Channel<T>, ExtractionError> {
|
||||
let header = header.c4_tabbed_header_renderer;
|
||||
|
||||
if header.channel_id != id {
|
||||
bail!(
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong channel id {}, expected {}",
|
||||
header.channel_id,
|
||||
id
|
||||
);
|
||||
header.channel_id, id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Channel {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{model::ChannelRss, report::Report};
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::ChannelRss,
|
||||
report::Report,
|
||||
};
|
||||
|
||||
use super::{response, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
pub async fn channel_rss(&self, channel_id: &str) -> Result<ChannelRss> {
|
||||
pub async fn channel_rss(&self, channel_id: &str) -> Result<ChannelRss, Error> {
|
||||
let url = format!(
|
||||
"https://www.youtube.com/feeds/videos.xml?channel_id={}",
|
||||
channel_id
|
||||
|
|
@ -41,7 +43,10 @@ impl RustyPipeQuery {
|
|||
reporter.report(&report);
|
||||
}
|
||||
|
||||
Err(e.into())
|
||||
Err(ExtractionError::InvalidData(
|
||||
format!("could not deserialize xml: {}", e).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ mod channel_rss;
|
|||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use fancy_regex::Regex;
|
||||
use log::{debug, error, warn};
|
||||
|
|
@ -27,6 +26,7 @@ use tokio::sync::RwLock;
|
|||
use crate::{
|
||||
cache::{CacheStorage, FileStorage},
|
||||
deobfuscate::{DeobfData, Deobfuscator},
|
||||
error::{Error, ExtractionError, Result},
|
||||
model::{Country, Language},
|
||||
report::{FileReporter, Level, Report, Reporter},
|
||||
serializer::MapResult,
|
||||
|
|
@ -166,8 +166,8 @@ pub struct RustyPipe {
|
|||
|
||||
struct RustyPipeRef {
|
||||
http: Client,
|
||||
storage: Option<Box<dyn CacheStorage + Sync + Send>>,
|
||||
reporter: Option<Box<dyn Reporter + Sync + Send>>,
|
||||
storage: Option<Box<dyn CacheStorage>>,
|
||||
reporter: Option<Box<dyn Reporter>>,
|
||||
n_retries: u32,
|
||||
consent_cookie: String,
|
||||
cache: CacheHolder,
|
||||
|
|
@ -183,8 +183,8 @@ struct RustyPipeOpts {
|
|||
}
|
||||
|
||||
pub struct RustyPipeBuilder {
|
||||
storage: Option<Box<dyn CacheStorage + Sync + Send>>,
|
||||
reporter: Option<Box<dyn Reporter + Sync + Send>>,
|
||||
storage: Option<Box<dyn CacheStorage>>,
|
||||
reporter: Option<Box<dyn Reporter>>,
|
||||
n_retries: u32,
|
||||
user_agent: String,
|
||||
default_opts: RustyPipeOpts,
|
||||
|
|
@ -452,8 +452,11 @@ impl RustyPipe {
|
|||
}
|
||||
|
||||
/// Execute the given http request.
|
||||
async fn http_request(&self, request: Request) -> Result<Response, reqwest::Error> {
|
||||
let mut last_res: Option<Result<Response, reqwest::Error>> = None;
|
||||
async fn http_request(
|
||||
&self,
|
||||
request: Request,
|
||||
) -> core::result::Result<Response, reqwest::Error> {
|
||||
let mut last_res = None;
|
||||
for n in 0..self.inner.n_retries {
|
||||
let res = self.inner.http.execute(request.try_clone().unwrap()).await;
|
||||
let emsg = match &res {
|
||||
|
|
@ -509,11 +512,13 @@ impl RustyPipe {
|
|||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to download sw.js")?;
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1)
|
||||
.ok_or_else(|| anyhow!("Could not find desktop client version in sw.js"))
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData(
|
||||
"Could not find desktop client version in sw.js".into(),
|
||||
),
|
||||
))
|
||||
};
|
||||
|
||||
let from_html = async {
|
||||
|
|
@ -525,11 +530,13 @@ impl RustyPipe {
|
|||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to get YT Desktop page")?;
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1)
|
||||
.ok_or_else(|| anyhow!("Could not find desktop client version on html page"))
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData(
|
||||
"Could not find desktop client version in sw.js".into(),
|
||||
),
|
||||
))
|
||||
};
|
||||
|
||||
match from_swjs.await {
|
||||
|
|
@ -552,11 +559,11 @@ impl RustyPipe {
|
|||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to download sw.js")?;
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1)
|
||||
.ok_or_else(|| anyhow!("Could not find desktop client version in sw.js"))
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData("Could not find music client version in sw.js".into()),
|
||||
))
|
||||
};
|
||||
|
||||
let from_html = async {
|
||||
|
|
@ -568,11 +575,13 @@ impl RustyPipe {
|
|||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to get YT Desktop page")?;
|
||||
.await?;
|
||||
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1)
|
||||
.ok_or_else(|| anyhow!("Could not find desktop client version on html page"))
|
||||
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(Error::from(
|
||||
ExtractionError::InvalidData(
|
||||
"Could not find music client version on html page".into(),
|
||||
),
|
||||
))
|
||||
};
|
||||
|
||||
match from_swjs.await {
|
||||
|
|
@ -971,7 +980,7 @@ impl RustyPipeQuery {
|
|||
};
|
||||
|
||||
if status.is_client_error() || status.is_server_error() {
|
||||
let e = anyhow!("Server responded with error code {}", status);
|
||||
let e = Error::HttpStatus(status.into());
|
||||
create_report(Level::ERR, Some(e.to_string()), vec![]);
|
||||
return Err(e);
|
||||
}
|
||||
|
|
@ -982,12 +991,12 @@ impl RustyPipeQuery {
|
|||
if !mapres.warnings.is_empty() {
|
||||
create_report(
|
||||
Level::WRN,
|
||||
Some("Warnings during deserialization/mapping".to_owned()),
|
||||
Some(ExtractionError::Warnings.to_string()),
|
||||
mapres.warnings,
|
||||
);
|
||||
|
||||
if self.opts.strict {
|
||||
bail!("Warnings during deserialization/mapping");
|
||||
return Err(Error::Extraction(ExtractionError::Warnings));
|
||||
}
|
||||
} else if self.opts.report {
|
||||
create_report(Level::DBG, None, vec![]);
|
||||
|
|
@ -995,15 +1004,13 @@ impl RustyPipeQuery {
|
|||
Ok(mapres.c)
|
||||
}
|
||||
Err(e) => {
|
||||
let emsg = "Could not map reponse";
|
||||
create_report(Level::ERR, Some(emsg.to_owned()), vec![e.to_string()]);
|
||||
Err(e).context(emsg)
|
||||
create_report(Level::ERR, Some(e.to_string()), Vec::new());
|
||||
Err(e.into())
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let emsg = "Could not deserialize response";
|
||||
create_report(Level::ERR, Some(emsg.to_owned()), vec![e.to_string()]);
|
||||
Err(e).context(emsg)
|
||||
create_report(Level::ERR, Some(e.to_string()), Vec::new());
|
||||
Err(Error::from(ExtractionError::from(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1058,7 +1065,7 @@ trait MapResponse<T> {
|
|||
id: &str,
|
||||
lang: Language,
|
||||
deobf: Option<&Deobfuscator>,
|
||||
) -> Result<MapResult<T>>;
|
||||
) -> core::result::Result<MapResult<T>, crate::error::ExtractionError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use anyhow::Result;
|
||||
use crate::error::Result;
|
||||
|
||||
use crate::model::{
|
||||
ChannelPlaylist, ChannelVideo, Comment, Paginator, PlaylistVideo, RecommendedVideo,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use std::{
|
|||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
|
||||
use fancy_regex::Regex;
|
||||
use once_cell::sync::Lazy;
|
||||
|
|
@ -12,6 +11,7 @@ use url::Url;
|
|||
|
||||
use crate::{
|
||||
deobfuscate::Deobfuscator,
|
||||
error::{DeobfError, Error, ExtractionError},
|
||||
model::{
|
||||
AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Language, Subtitle,
|
||||
VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
|
|
@ -58,7 +58,11 @@ struct QContentPlaybackContext {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
pub async fn player(self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
|
||||
pub async fn player(
|
||||
self,
|
||||
video_id: &str,
|
||||
client_type: ClientType,
|
||||
) -> Result<VideoPlayer, Error> {
|
||||
let q1 = self.clone();
|
||||
let t_context = tokio::spawn(async move { q1.get_context(client_type, false).await });
|
||||
let q2 = self.client.clone();
|
||||
|
|
@ -111,7 +115,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
id: &str,
|
||||
_lang: Language,
|
||||
deobf: Option<&Deobfuscator>,
|
||||
) -> Result<super::MapResult<VideoPlayer>> {
|
||||
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
|
||||
let deobf = deobf.unwrap();
|
||||
let mut warnings = vec![];
|
||||
|
||||
|
|
@ -121,26 +125,36 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
live_streamability.is_some()
|
||||
}
|
||||
response::player::PlayabilityStatus::Unplayable { reason } => {
|
||||
bail!("Video is unplayable. Reason: {}", reason)
|
||||
return Err(ExtractionError::VideoUnavailable("DRM/Geoblock", reason))
|
||||
}
|
||||
response::player::PlayabilityStatus::LoginRequired { reason } => {
|
||||
bail!("Playback requires login. Reason: {}", reason)
|
||||
// reason: "Sign in to confirm your age"
|
||||
if reason.split_whitespace().any(|word| word == "age") {
|
||||
return Err(ExtractionError::VideoAgeRestricted);
|
||||
}
|
||||
return Err(ExtractionError::VideoUnavailable("private video", reason));
|
||||
}
|
||||
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
|
||||
bail!("Livestream is offline. Reason: {}", reason)
|
||||
return Err(ExtractionError::VideoUnavailable(
|
||||
"offline livestream",
|
||||
reason,
|
||||
))
|
||||
}
|
||||
response::player::PlayabilityStatus::Error { reason } => {
|
||||
bail!("Video was deleted. Reason: {}", reason)
|
||||
return Err(ExtractionError::VideoUnavailable(
|
||||
"deletion/censorship",
|
||||
reason,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let mut streaming_data = some_or_bail!(
|
||||
self.streaming_data,
|
||||
Err(anyhow!("No streaming data was returned"))
|
||||
Err(ExtractionError::InvalidData("no streaming data".into()))
|
||||
);
|
||||
let video_details = some_or_bail!(
|
||||
self.video_details,
|
||||
Err(anyhow!("No video details were returned"))
|
||||
Err(ExtractionError::InvalidData("no video details".into()))
|
||||
);
|
||||
let microformat = self.microformat.map(|m| m.player_microformat_renderer);
|
||||
let (publish_date, category, tags, is_family_safe) =
|
||||
|
|
@ -159,11 +173,10 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
});
|
||||
|
||||
if video_details.video_id != id {
|
||||
bail!(
|
||||
"got wrong video id {}, expected {}",
|
||||
video_details.video_id,
|
||||
id
|
||||
);
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"video id {}, expected {}",
|
||||
video_details.video_id, id
|
||||
)));
|
||||
}
|
||||
|
||||
let video_info = VideoPlayerDetails {
|
||||
|
|
@ -272,7 +285,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
fn cipher_to_url_params(
|
||||
signature_cipher: &str,
|
||||
deobf: &Deobfuscator,
|
||||
) -> Result<(String, BTreeMap<String, String>)> {
|
||||
) -> Result<(String, BTreeMap<String, String>), DeobfError> {
|
||||
let params: HashMap<Cow<str>, Cow<str>> =
|
||||
url::form_urlencoded::parse(signature_cipher.as_bytes()).collect();
|
||||
|
||||
|
|
@ -281,12 +294,15 @@ fn cipher_to_url_params(
|
|||
// `sp`: Signature parameter
|
||||
// `url`: URL that is missing the signature parameter
|
||||
|
||||
let sig = some_or_bail!(params.get("s"), Err(anyhow!("no s param")));
|
||||
let sp = some_or_bail!(params.get("sp"), Err(anyhow!("no sp param")));
|
||||
let raw_url = some_or_bail!(params.get("url"), Err(anyhow!("no url param")));
|
||||
let (url_base, mut url_params) = util::url_to_params(raw_url)?;
|
||||
let sig = some_or_bail!(params.get("s"), Err(DeobfError::Extraction("s param")));
|
||||
let sp = some_or_bail!(params.get("sp"), Err(DeobfError::Extraction("sp param")));
|
||||
let raw_url = some_or_bail!(
|
||||
params.get("url"),
|
||||
Err(DeobfError::Extraction("no url param"))
|
||||
);
|
||||
let (url_base, mut url_params) =
|
||||
util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?;
|
||||
|
||||
// println!("sig: {}", sig);
|
||||
let deobf_sig = deobf.deobfuscate_sig(sig)?;
|
||||
url_params.insert(sp.to_string(), deobf_sig);
|
||||
|
||||
|
|
@ -297,7 +313,7 @@ fn deobf_nsig(
|
|||
url_params: &mut BTreeMap<String, String>,
|
||||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> Result<()> {
|
||||
) -> Result<(), DeobfError> {
|
||||
let nsig: String;
|
||||
if let Some(n) = url_params.get("n") {
|
||||
nsig = if n == &last_nsig[0] {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
deobfuscate::Deobfuscator,
|
||||
error::{Error, ExtractionError},
|
||||
model::{ChannelId, Language, Paginator, Playlist, PlaylistVideo},
|
||||
timeago,
|
||||
util::{self, TryRemove},
|
||||
|
|
@ -22,7 +22,7 @@ struct QPlaylist {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
pub async fn playlist(self, playlist_id: &str) -> Result<Playlist> {
|
||||
pub async fn playlist(self, playlist_id: &str) -> Result<Playlist, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QPlaylist {
|
||||
context,
|
||||
|
|
@ -39,7 +39,10 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn playlist_continuation(self, ctoken: &str) -> Result<Paginator<PlaylistVideo>> {
|
||||
pub async fn playlist_continuation(
|
||||
self,
|
||||
ctoken: &str,
|
||||
) -> Result<Paginator<PlaylistVideo>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
|
|
@ -63,26 +66,32 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
id: &str,
|
||||
lang: Language,
|
||||
_deobf: Option<&Deobfuscator>,
|
||||
) -> Result<MapResult<Playlist>> {
|
||||
) -> Result<MapResult<Playlist>, ExtractionError> {
|
||||
// TODO: think about a deserializer that deserializes only first list item
|
||||
let mut tcbr_contents = self.contents.two_column_browse_results_renderer.contents;
|
||||
let video_items = some_or_bail!(
|
||||
some_or_bail!(
|
||||
some_or_bail!(
|
||||
tcbr_contents.try_swap_remove(0),
|
||||
Err(anyhow!("twoColumnBrowseResultsRenderer empty"))
|
||||
Err(ExtractionError::InvalidData(
|
||||
"twoColumnBrowseResultsRenderer empty".into()
|
||||
))
|
||||
)
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents
|
||||
.try_swap_remove(0),
|
||||
Err(anyhow!("sectionListRenderer empty"))
|
||||
Err(ExtractionError::InvalidData(
|
||||
"sectionListRenderer empty".into()
|
||||
))
|
||||
)
|
||||
.item_section_renderer
|
||||
.contents
|
||||
.try_swap_remove(0),
|
||||
Err(anyhow!("itemSectionRenderer empty"))
|
||||
Err(ExtractionError::InvalidData(
|
||||
"itemSectionRenderer empty".into()
|
||||
))
|
||||
)
|
||||
.playlist_video_list_renderer
|
||||
.contents;
|
||||
|
|
@ -94,7 +103,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
let mut sidebar_items = sidebar.playlist_sidebar_renderer.items;
|
||||
let mut primary = some_or_bail!(
|
||||
sidebar_items.try_swap_remove(0),
|
||||
Err(anyhow!("no primary sidebar"))
|
||||
Err(ExtractionError::InvalidData("no primary sidebar".into()))
|
||||
);
|
||||
|
||||
(
|
||||
|
|
@ -112,7 +121,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
None => {
|
||||
let header_banner = some_or_bail!(
|
||||
self.header.playlist_header_renderer.playlist_header_banner,
|
||||
Err(anyhow!("no thumbnail found"))
|
||||
Err(ExtractionError::InvalidData("no thumbnail found".into()))
|
||||
);
|
||||
|
||||
let mut byline = self.header.playlist_header_renderer.byline;
|
||||
|
|
@ -131,7 +140,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
Some(_) => {
|
||||
ok_or_bail!(
|
||||
util::parse_numeric(&self.header.playlist_header_renderer.num_videos_text),
|
||||
Err(anyhow!("no video count"))
|
||||
Err(ExtractionError::InvalidData("no video count".into()))
|
||||
)
|
||||
}
|
||||
None => videos.len() as u32,
|
||||
|
|
@ -139,7 +148,10 @@ impl MapResponse<Playlist> for response::Playlist {
|
|||
|
||||
let playlist_id = self.header.playlist_header_renderer.playlist_id;
|
||||
if playlist_id != id {
|
||||
bail!("got wrong playlist id {}, expected {}", playlist_id, id);
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong playlist id {}, expected {}",
|
||||
playlist_id, id
|
||||
)));
|
||||
}
|
||||
|
||||
let name = self.header.playlist_header_renderer.title;
|
||||
|
|
@ -178,11 +190,13 @@ impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
|
|||
_id: &str,
|
||||
_lang: Language,
|
||||
_deobf: Option<&Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<PlaylistVideo>>> {
|
||||
) -> Result<MapResult<Paginator<PlaylistVideo>>, ExtractionError> {
|
||||
let mut actions = self.on_response_received_actions;
|
||||
let action = some_or_bail!(
|
||||
actions.try_swap_remove(0),
|
||||
Err(anyhow!("no continuation action"))
|
||||
Err(ExtractionError::InvalidData(
|
||||
"no continuation action".into()
|
||||
))
|
||||
);
|
||||
|
||||
let (items, ctoken) =
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
ChannelId, ChannelTag, Chapter, Comment, Language, Paginator, RecommendedVideo,
|
||||
VideoDetails,
|
||||
|
|
@ -30,7 +30,7 @@ struct QVideo {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
pub async fn video_details(self, video_id: &str) -> Result<VideoDetails> {
|
||||
pub async fn video_details(self, video_id: &str) -> Result<VideoDetails, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QVideo {
|
||||
context,
|
||||
|
|
@ -49,7 +49,10 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn video_recommendations(self, ctoken: &str) -> Result<Paginator<RecommendedVideo>> {
|
||||
pub async fn video_recommendations(
|
||||
self,
|
||||
ctoken: &str,
|
||||
) -> Result<Paginator<RecommendedVideo>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
|
|
@ -66,7 +69,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn video_comments(self, ctoken: &str) -> Result<Paginator<Comment>> {
|
||||
pub async fn video_comments(self, ctoken: &str) -> Result<Paginator<Comment>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true).await;
|
||||
let request_body = QContinuation {
|
||||
context,
|
||||
|
|
@ -90,12 +93,15 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
id: &str,
|
||||
lang: crate::model::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<VideoDetails>> {
|
||||
) -> Result<MapResult<VideoDetails>, ExtractionError> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
let video_id = self.current_video_endpoint.watch_endpoint.video_id;
|
||||
if id != video_id {
|
||||
bail!("got wrong playlist id {}, expected {}", video_id, id);
|
||||
return Err(ExtractionError::WrongResult(format!(
|
||||
"got wrong playlist id {}, expected {}",
|
||||
video_id, id
|
||||
)));
|
||||
}
|
||||
|
||||
let mut primary_results = self
|
||||
|
|
@ -167,7 +173,11 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
view_count.video_view_count_renderer.is_live,
|
||||
)
|
||||
}
|
||||
_ => bail!("could not find primary_info"),
|
||||
_ => {
|
||||
return Err(ExtractionError::InvalidData(
|
||||
"could not find primary_info".into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let comment_count = comment_count_section.and_then(|s| {
|
||||
|
|
@ -214,7 +224,11 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
|
||||
(owner.video_owner_renderer, desc, is_ccommons)
|
||||
}
|
||||
_ => bail!("could not find secondary_info"),
|
||||
_ => {
|
||||
return Err(ExtractionError::InvalidData(
|
||||
"could not find secondary_info".into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let (channel_id, channel_name) = match owner.title {
|
||||
|
|
@ -224,9 +238,13 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
browse_id,
|
||||
} => match page_type {
|
||||
crate::serializer::text::PageType::Channel => (browse_id, text),
|
||||
_ => bail!("invalid channel link type"),
|
||||
_ => {
|
||||
return Err(ExtractionError::InvalidData(
|
||||
"invalid channel link type".into(),
|
||||
))
|
||||
}
|
||||
},
|
||||
_ => bail!("invalid channel link"),
|
||||
_ => return Err(ExtractionError::InvalidData("invalid channel link".into())),
|
||||
};
|
||||
|
||||
let recommended = self
|
||||
|
|
@ -324,11 +342,13 @@ impl MapResponse<Paginator<RecommendedVideo>> for response::VideoRecommendations
|
|||
_id: &str,
|
||||
lang: crate::model::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<RecommendedVideo>>> {
|
||||
) -> Result<MapResult<Paginator<RecommendedVideo>>, ExtractionError> {
|
||||
let mut endpoints = self.on_response_received_endpoints;
|
||||
let cont = some_or_bail!(
|
||||
endpoints.try_swap_remove(0),
|
||||
Err(anyhow!("no continuation endpoint"))
|
||||
Err(ExtractionError::InvalidData(
|
||||
"no continuation endpoint".into()
|
||||
))
|
||||
);
|
||||
|
||||
Ok(map_recommendations(
|
||||
|
|
@ -344,7 +364,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
_id: &str,
|
||||
lang: crate::model::Language,
|
||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<Comment>>> {
|
||||
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
||||
let mut warnings = self.on_response_received_endpoints.warnings;
|
||||
|
||||
let mut comments = Vec::new();
|
||||
|
|
|
|||
Reference in a new issue