rustypipe/src/error.rs

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(_)
)
}
}