222 lines
7.5 KiB
Rust
222 lines
7.5 KiB
Rust
//! RustyPipe error types
|
|
|
|
use std::{borrow::Cow, fmt::Display};
|
|
|
|
use reqwest::StatusCode;
|
|
|
|
/// Error type for the RustyPipe library
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum Error {
|
|
/// Error extracting content from YouTube
|
|
#[error("extraction error: {0}")]
|
|
Extraction(#[from] ExtractionError),
|
|
/// Error from the HTTP client
|
|
#[error("http error: {0}")]
|
|
Http(Cow<'static, str>),
|
|
/// Erroneous HTTP status code received
|
|
#[error("http status code: {0} message: {1}")]
|
|
HttpStatus(u16, Cow<'static, str>),
|
|
/// Unspecified error
|
|
#[error("error: {0}")]
|
|
Other(Cow<'static, str>),
|
|
}
|
|
|
|
/// Error extracting content from YouTube
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum ExtractionError {
|
|
/// Content cannot be extracted with RustyPipe
|
|
///
|
|
/// Reasons include:
|
|
/// - Deletion/Censorship
|
|
/// - Age restriction
|
|
/// - Private video
|
|
/// - DRM (Movies and TV shows)
|
|
#[error("content unavailable ({reason}). Reason (from YT): {msg}")]
|
|
Unavailable {
|
|
/// Reason why the video could not be extracted
|
|
reason: UnavailabilityReason,
|
|
/// The error message as returned from YouTube
|
|
msg: String,
|
|
},
|
|
/// Content with the given ID does not exist
|
|
#[error("content `{id}` was not found ({msg})")]
|
|
NotFound {
|
|
/// ID of the requested content
|
|
id: String,
|
|
/// Error message
|
|
msg: Cow<'static, str>,
|
|
},
|
|
/// Bad request (Error 400 from YouTube), probably invalid input parameters
|
|
#[error("bad request ({0})")]
|
|
BadRequest(Cow<'static, str>),
|
|
/// YouTube returned data that could not be deserialized or parsed
|
|
#[error("invalid data from YT: {0}")]
|
|
InvalidData(Cow<'static, str>),
|
|
/// Error deobfuscating YouTube's URL signatures
|
|
#[error("deobfuscation error: {0}")]
|
|
Deobfuscation(Cow<'static, str>),
|
|
/// YouTube returned data that does not match the queried ID
|
|
///
|
|
/// Specifically YouTube may return this video <https://www.youtube.com/watch?v=aQvGIIdgFDM>,
|
|
/// which is a 5 minute error message, instead of the requested video when using an outdated
|
|
/// Android client.
|
|
#[error("wrong result from YT: {0}")]
|
|
WrongResult(String),
|
|
/// YouTube redirects you to another content ID
|
|
///
|
|
/// This is used internally for YouTube Music channels that link to a main channel.
|
|
#[error("redirecting to: {0}")]
|
|
Redirect(String),
|
|
/// Warnings occurred during deserialization/mapping
|
|
///
|
|
/// This error is only returned in strict mode.
|
|
#[error("warnings during deserialization/mapping")]
|
|
DeserializationWarnings,
|
|
}
|
|
|
|
/// Reason why a video cannot be extracted
|
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
|
#[non_exhaustive]
|
|
pub enum UnavailabilityReason {
|
|
/// Video/Channel is age restricted.
|
|
///
|
|
/// Video age restriction may be circumvented with the
|
|
/// [`ClientType::TvHtml5Embed`](crate::client::ClientType::TvHtml5Embed) client.
|
|
AgeRestricted,
|
|
/// Video was deleted or censored
|
|
Deleted,
|
|
/// Video is not available in your country
|
|
Geoblocked,
|
|
/// Video cannot be extracted with the specified client
|
|
UnsupportedClient,
|
|
/// Video is private
|
|
Private,
|
|
/// Video needs to be purchased and is protected by digital restrictions management
|
|
/// (e.g. movies and TV shows)
|
|
Paid,
|
|
/// Video is only available to YouTube Premium users
|
|
Premium,
|
|
/// Video is only available to channel members
|
|
MembersOnly,
|
|
/// Livestream has gone offline
|
|
OfflineLivestream,
|
|
/// YouTube banned your IP address from accessing the platform without an account
|
|
IpBan,
|
|
/// Video cant be played for other reasons
|
|
#[default]
|
|
Unplayable,
|
|
}
|
|
|
|
impl Display for UnavailabilityReason {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
UnavailabilityReason::AgeRestricted => f.write_str("age-restricted"),
|
|
UnavailabilityReason::Deleted => f.write_str("deleted"),
|
|
UnavailabilityReason::Geoblocked => f.write_str("geoblocked"),
|
|
UnavailabilityReason::UnsupportedClient => f.write_str("unsupported by client"),
|
|
UnavailabilityReason::Private => f.write_str("private"),
|
|
UnavailabilityReason::Paid => f.write_str("paid"),
|
|
UnavailabilityReason::Premium => f.write_str("premium-only"),
|
|
UnavailabilityReason::MembersOnly => f.write_str("members-only"),
|
|
UnavailabilityReason::OfflineLivestream => f.write_str("offline stream"),
|
|
UnavailabilityReason::IpBan => f.write_str("ip-ban"),
|
|
UnavailabilityReason::Unplayable => f.write_str("unplayable"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) mod internal {
|
|
use super::{Error, ExtractionError};
|
|
|
|
/// Error that occurred during the initialization
|
|
/// or use of the YouTube URL signature deobfuscator.
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum DeobfError {
|
|
/// Error during JavaScript execution
|
|
#[error("js execution error: {0}")]
|
|
JavaScript(#[from] quick_js::ExecutionError),
|
|
/// Error during JavaScript parsing
|
|
#[error("js parsing: {0}")]
|
|
JsParser(#[from] ress::error::Error),
|
|
/// Could not extract certain data
|
|
#[error("could not extract {0}")]
|
|
Extraction(&'static str),
|
|
/// Unspecified error
|
|
#[error("error: {0}")]
|
|
Other(&'static str),
|
|
}
|
|
|
|
impl From<DeobfError> for Error {
|
|
fn from(value: DeobfError) -> Self {
|
|
Self::Extraction(value.into())
|
|
}
|
|
}
|
|
|
|
impl From<DeobfError> for ExtractionError {
|
|
fn from(value: DeobfError) -> Self {
|
|
Self::Deobfuscation(value.to_string().into())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for ExtractionError {
|
|
fn from(value: serde_json::Error) -> Self {
|
|
Self::InvalidData(value.to_string().into())
|
|
}
|
|
}
|
|
|
|
impl From<reqwest::Error> for Error {
|
|
fn from(value: reqwest::Error) -> Self {
|
|
if value.is_status() {
|
|
if let Some(status) = value.status() {
|
|
return Self::HttpStatus(status.as_u16(), Cow::default());
|
|
}
|
|
}
|
|
Self::Http(value.to_string().into())
|
|
}
|
|
}
|
|
|
|
impl From<serde_plain::Error> for Error {
|
|
fn from(value: serde_plain::Error) -> Self {
|
|
Self::Other(value.to_string().into())
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
/// Return true if a report should be generated
|
|
pub(crate) fn should_report(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::HttpStatus(_, _)
|
|
| Self::Extraction(
|
|
ExtractionError::InvalidData(_) | ExtractionError::WrongResult(_)
|
|
)
|
|
)
|
|
}
|
|
|
|
/// Return true if the request should be retried
|
|
pub(crate) fn should_retry(&self) -> bool {
|
|
match self {
|
|
Self::HttpStatus(code, _) => match StatusCode::try_from(*code) {
|
|
Ok(status) => status.is_server_error() || status == StatusCode::TOO_MANY_REQUESTS,
|
|
Err(_) => false,
|
|
},
|
|
Self::Extraction(ExtractionError::InvalidData(_)) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ExtractionError {
|
|
/// Return true if the video should be fetched with a different client
|
|
pub(crate) fn switch_client(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
ExtractionError::Unavailable {
|
|
reason: UnavailabilityReason::AgeRestricted
|
|
| UnavailabilityReason::UnsupportedClient,
|
|
..
|
|
} | ExtractionError::WrongResult(_)
|
|
)
|
|
}
|
|
}
|