diff --git a/src/account.rs b/src/account.rs index 7e10d91..fb0d19e 100644 --- a/src/account.rs +++ b/src/account.rs @@ -152,14 +152,14 @@ impl Account { if !profile_update.is_empty() { self.executor .patch(profile_endpoint) - .json(&serde_json::to_value(profile_update)?) + .json(&Value::Object(profile_update)) .request::() .await?; } if !notification_update.is_empty() { self.executor .patch(notification_endpoint) - .json(&serde_json::to_value(notification_update)?) + .json(&Value::Object(notification_update)) .request::() .await?; } @@ -218,7 +218,7 @@ impl Crunchyroll { /// Return information about the current account. [`Account`] can be used to modify account /// settings like the email or web interface language. pub async fn account(&self) -> Result { - let mut result: HashMap = HashMap::new(); + let mut result: serde_json::Map = serde_json::Map::new(); let me_endpoint = "https://www.crunchyroll.com/accounts/v1/me"; result.extend( @@ -236,7 +236,7 @@ impl Crunchyroll { .await?, ); - let mut account: Account = serde_json::from_value(serde_json::to_value(result)?)?; + let mut account: Account = serde_json::from_value(Value::Object(result))?; account.executor = self.executor.clone(); Ok(account) diff --git a/src/crunchyroll.rs b/src/crunchyroll.rs index acdda4c..0f99f58 100644 --- a/src/crunchyroll.rs +++ b/src/crunchyroll.rs @@ -687,9 +687,10 @@ mod auth { policy: index.cms_web.policy, key_pair_id: index.cms_web.key_pair_id, account_id: login_response.account_id.ok_or_else(|| { - Error::Authentication( - "Login with a user account to use this function".into(), - ) + Error::Authentication { + message: "Login with a user account to use this function" + .to_string(), + } }), }, fixes: self.fixes, @@ -721,17 +722,10 @@ mod auth { let value = serde_json::Value::deserialize(serde::de::value::MapDeserializer::new( cleaned.into_iter(), ))?; - serde_json::from_value(value.clone()).map_err(|e| { - Error::Decode( - crate::error::ErrorContext::new(format!( - "{} at {}:{}", - e, - e.line(), - e.column() - )) - .with_url(url) - .with_value(value.to_string().as_bytes()), - ) + serde_json::from_value(value.clone()).map_err(|e| Error::Decode { + message: format!("{} at {}:{}", e, e.line(), e.column()), + content: value.to_string().into_bytes(), + url, }) } } diff --git a/src/error.rs b/src/error.rs index 2c76e6e..71bfb9a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,39 +9,82 @@ use std::fmt::{Debug, Display, Formatter}; pub(crate) type Result = core::result::Result; -/// Crate specfic error types. +/// Crate specific error types. #[derive(Clone, Debug)] pub enum Error { /// Error was caused by something library internal. This only happens if something was /// implemented incorrectly (which hopefully should never be the case) or if Crunchyroll /// surprisingly changed specific parts of their api which broke a part of this crate. - Internal(ErrorContext<()>), + Internal { message: String }, /// Some sort of error occurred while requesting the Crunchyroll api. - Request(ErrorContext), + Request { + message: String, + status: Option, + /// The url which caused the error. + url: String, + }, /// While decoding the api response body something went wrong. - Decode(ErrorContext<()>), + Decode { + message: String, + /// The content which failed to get decoded. Might be empty if the error got triggered by + /// the [`From`] implementation for this enum. + content: Vec, + /// The url which caused the error. Might be empty if the error got triggered by the + /// [`From`] implementation for this enum. + url: String, + }, /// Something went wrong while logging in. - Authentication(ErrorContext<()>), + Authentication { message: String }, /// Generally malformed or invalid user input. - Input(ErrorContext<()>), + Input { message: String }, /// When the request got blocked. Currently this only triggers when the cloudflare bot /// protection is detected. - Block(ErrorContext<()>), + Block { + message: String, + /// HTML/text body of the block response. + body: String, + /// The url which caused the error. + url: String, + }, } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Error::Internal(context) => write!(f, "{context}"), - Error::Request(context) => write!(f, "{context}"), - Error::Decode(context) => write!(f, "{context}"), - Error::Authentication(context) => write!(f, "{context}"), - Error::Input(context) => write!(f, "{context}"), - Error::Block(context) => write!(f, "{context}"), + Error::Internal { message } => write!(f, "{message}"), + Error::Request { message, url, .. } => { + // the url can be 'n/a' when the error got triggered by the [`From`] + // implementation for this error struct + if url != "n/a" { + write!(f, "{message} ({url})") + } else { + write!(f, "{message}") + } + } + Error::Decode { + message, + content, + url, + } => { + let mut msg = message.clone(); + // the url is 'n/a' when the error got triggered by the [`From`] + // implementation for this error struct or [`VariantSegment::decrypt`] + if url != "n/a" { + msg.push_str(&format!(" ({url})")) + } + if content.is_empty() { + write!(f, "{}", msg) + } else { + write!(f, "{}: {}", msg, String::from_utf8_lossy(content.as_ref())) + } + } + Error::Authentication { message } => write!(f, "{message}"), + Error::Input { message } => write!(f, "{message}"), + Error::Block { message, body, url } => write!(f, "{message} ({url}): {body}"), } } } @@ -50,17 +93,16 @@ impl std::error::Error for Error {} impl From for Error { fn from(err: serde_json::Error) -> Self { - Self::Decode(ErrorContext::new(err.to_string())) + Self::Decode { + message: err.to_string(), + content: vec![], + url: "n/a".to_string(), + } } } impl From for Error { fn from(err: reqwest::Error) -> Self { - let mut context: ErrorContext<()> = ErrorContext::new(err.to_string()); - if let Some(url) = err.url() { - context = context.with_url(url.clone()); - } - if err.is_request() || err.is_redirect() || err.is_timeout() @@ -68,102 +110,30 @@ impl From for Error { || err.is_body() || err.is_status() { - let mut request_context = context.into_other_context(); - if let Some(status) = err.status() { - request_context = request_context.with_extra(status) + Error::Request { + message: err.to_string(), + status: err.status(), + url: err.url().map_or("n/a".to_string(), |url| url.to_string()), } - Error::Request(request_context) } else if err.is_decode() { - Error::Decode(context) + Error::Decode { + message: err.to_string(), + content: vec![], + url: err.url().map_or("n/a".to_string(), |url| url.to_string()), + } } else if err.is_builder() { - Error::Internal(context) + Error::Internal { + message: err.to_string(), + } } else { - Error::Internal(ErrorContext::new(format!( - "Could not determine request error type - {err}" - ))) - } - } -} - -/// Information about a [`Error`]. -#[derive(Clone, Debug)] -pub struct ErrorContext { - pub message: String, - pub url: Option, - pub value: Option, - - pub extra: Option, -} - -impl Display for ErrorContext { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut res = self.message.clone(); - - if let Some(url) = &self.url { - res.push_str(&format!(" ({url})")); - } - if let Some(value) = &self.value { - res.push_str(&format!(": {value}")); - } - - write!(f, "{res}") - } -} - -impl From for ErrorContext { - fn from(string: String) -> Self { - ErrorContext::new(string) - } -} - -impl From<&str> for ErrorContext { - fn from(str: &str) -> Self { - ErrorContext::new(str) - } -} - -impl ErrorContext { - pub(crate) fn new(message: S) -> Self { - Self { - message: message.to_string(), - url: None, - value: None, - extra: None, - } - } - - pub(crate) fn with_url>(mut self, url: S) -> Self { - self.url = Some(url.as_ref().to_string()); - - self - } - - pub(crate) fn with_value(mut self, value: &[u8]) -> Self { - self.value = Some(format!( - ": {}", - std::str::from_utf8(value).unwrap_or("-- not displayable --") - )); - - self - } - - pub(crate) fn with_extra(mut self, extra: T) -> Self { - self.extra = Some(extra); - - self - } - - pub(crate) fn into_other_context(self) -> ErrorContext { - ErrorContext { - message: self.message, - url: self.url, - value: self.value, - extra: None, + Error::Internal { + message: "Could not determine request error type - {err}".to_string(), + } } } } -pub(crate) fn is_request_error(value: Value) -> Result<()> { +pub(crate) fn is_request_error(value: Value, url: &String, status: &StatusCode) -> Result<()> { #[derive(Debug, Deserialize)] struct CodeFieldContext { code: String, @@ -195,9 +165,11 @@ pub(crate) fn is_request_error(value: Value) -> Result<()> { } if let Ok(err) = serde_json::from_value::(value.clone()) { - return Err(Error::Request( - format!("{} - {}", err.error_type, err.message).into(), - )); + return Err(Error::Request { + message: format!("{} - {}", err.error_type, err.message), + status: Some(*status), + url: url.to_string(), + }); } else if let Ok(err) = serde_json::from_value::(value.clone()) { let mut details: Vec = vec![]; @@ -206,13 +178,17 @@ pub(crate) fn is_request_error(value: Value) -> Result<()> { } return if let Some(message) = err.message { - Err(Error::Request( - format!("{} ({}) - {}", message, err.code, details.join(", ")).into(), - )) + Err(Error::Request { + message: format!("{} ({}) - {}", message, err.code, details.join(", ")), + status: Some(*status), + url: url.to_string(), + }) } else { - Err(Error::Request( - format!("({}) - {}", err.code, details.join(", ")).into(), - )) + Err(Error::Request { + message: format!("({}) - {}", err.code, details.join(", ")), + status: Some(*status), + url: url.to_string(), + }) }; } else if let Ok(err) = serde_json::from_value::(value) { let details = err @@ -231,9 +207,11 @@ pub(crate) fn is_request_error(value: Value) -> Result<()> { }) .collect::>(); - return Err(Error::Request( - format!("{}: {}", err.code, details.join(", ")).into(), - )); + return Err(Error::Request { + message: format!("{}: {}", err.code, details.join(", ")), + status: Some(*status), + url: url.to_string(), + }); } Ok(()) } @@ -249,18 +227,20 @@ pub(crate) async fn check_request(url: String, resp: Respon .windows(31) .any(|w| w == b"Just a moment...") { - return Err(Error::Block( - ErrorContext::new("Triggered Cloudflare bot protection").with_url(url), - )); + return Err(Error::Block { + message: "Triggered Cloudflare bot protection".to_string(), + body: String::from_utf8_lossy(raw.as_ref()).to_string(), + url, + }); } raw } 404 => { - return Err(Error::Request( - ErrorContext::new("The requested resource is not present (404)") - .with_url(url) - .with_extra(status), - )) + return Err(Error::Request { + message: "The requested resource is not present".to_string(), + status: Some(resp.status()), + url, + }) } 429 => { let retry_secs = @@ -272,16 +252,16 @@ pub(crate) async fn check_request(url: String, resp: Respon None }; - return Err(Error::Request( - ErrorContext::new(format!( + return Err(Error::Request { + message: format!( "Rate limit detected. {}", retry_secs.map_or("Try again later".to_string(), |secs| format!( "Try again in {secs} seconds" )) - )) - .with_url(url) - .with_extra(status), - )); + ), + status: Some(resp.status()), + url, + }); } _ => resp.bytes().await?, }; @@ -292,25 +272,15 @@ pub(crate) async fn check_request(url: String, resp: Respon raw = "{}".as_bytes(); } - let value: Value = serde_json::from_slice(raw).map_err(|e| { - Error::Decode( - ErrorContext::new(format!("{} at {}:{}", e, e.line(), e.column())) - .with_url(&url) - .with_value(raw), - ) - })?; - is_request_error(value.clone()).map_err(|e| { - if let Error::Request(context) = e { - Error::Request(context.with_url(&url).with_extra(status)) - } else { - e - } + let value: Value = serde_json::from_slice(raw).map_err(|e| Error::Decode { + message: format!("{} at {}:{}", e, e.line(), e.column()), + content: raw.to_vec(), + url: url.clone(), })?; - serde_json::from_value::(value).map_err(|e| { - Error::Decode( - ErrorContext::new(format!("{} at {}:{}", e, e.line(), e.column())) - .with_url(&url) - .with_value(raw), - ) + is_request_error(value.clone(), &url, &status)?; + serde_json::from_value::(value).map_err(|e| Error::Decode { + message: format!("{} at {}:{}", e, e.line(), e.column()), + content: raw.to_vec(), + url, }) } diff --git a/src/internal/serde.rs b/src/internal/serde.rs index 6211409..faed3e8 100644 --- a/src/internal/serde.rs +++ b/src/internal/serde.rs @@ -1,5 +1,5 @@ use crate::common::Image; -use crate::error::{Error, ErrorContext}; +use crate::error::Error; use crate::{Request, Result}; use chrono::Duration; use serde::de::DeserializeOwned; @@ -45,10 +45,9 @@ pub(crate) fn query_to_urlencoded( Value::String(string) => string, Value::Null => continue, _ => { - return Err(Error::Internal( - ErrorContext::new("value is not supported to urlencode") - .with_value(key.to_string().as_bytes()), - )) + return Err(Error::Internal { + message: format!("key is not supported to be urlencoded ({})", key), + }) } }; let value_as_string = match value { @@ -60,21 +59,17 @@ pub(crate) fn query_to_urlencoded( .map(|vv| match vv { Value::Number(number) => Ok(number.to_string()), Value::String(string) => Ok(string), - _ => { - return Err(Error::Internal( - ErrorContext::new("value is not supported to urlencode") - .with_value(vv.to_string().as_bytes()), - )) - } + _ => Err(Error::Internal { + message: format!("value is not supported to be urlencoded ({})", vv), + }), }) .collect::>>()? .join(","), Value::Null => continue, _ => { - return Err(Error::Internal( - ErrorContext::new("value is not supported to urlencode") - .with_value(value.to_string().as_bytes()), - )) + return Err(Error::Internal { + message: format!("value is not supported to be urlencoded ({})", value), + }) } }; q.push((key_as_string, value_as_string)); diff --git a/src/list/crunchylist.rs b/src/list/crunchylist.rs index 48df60c..15e4379 100644 --- a/src/list/crunchylist.rs +++ b/src/list/crunchylist.rs @@ -136,7 +136,11 @@ impl Crunchylist { MediaCollection::Episode(episode) => episode.series_id, MediaCollection::MovieListing(movie_listing) => movie_listing.id, MediaCollection::Movie(movie) => movie.movie_listing_id, - _ => return Err(Error::Input("music related media isn't supported".into())), + _ => { + return Err(Error::Input { + message: "music related media isn't supported".to_string(), + }) + } }; self.executor .post(endpoint) diff --git a/src/list/watchlist.rs b/src/list/watchlist.rs index c54b2db..63287e7 100644 --- a/src/list/watchlist.rs +++ b/src/list/watchlist.rs @@ -51,9 +51,9 @@ impl WatchlistEntry { match self.panel.clone() { MediaCollection::Series(series) => Ok(series.id), MediaCollection::MovieListing(movie_listing) => Ok(movie_listing.id), - _ => Err(Error::Internal( - "panel is not series nor movie listing".into(), - )), + _ => Err(Error::Internal { + message: "panel is not series nor movie listing".to_string(), + }), } } } diff --git a/src/media/media_collection.rs b/src/media/media_collection.rs index 27cca91..2b47076 100644 --- a/src/media/media_collection.rs +++ b/src/media/media_collection.rs @@ -43,9 +43,9 @@ impl MediaCollection { } else if let Ok(music_video) = MusicVideo::from_id(crunchyroll, id.as_ref()).await { Ok(MediaCollection::MusicVideo(music_video)) } else { - Err(Error::Input( - format!("failed to find valid media with id '{}'", id.as_ref()).into(), - )) + Err(Error::Input { + message: format!("failed to find valid media with id '{}'", id.as_ref()), + }) } } } diff --git a/src/media/stream.rs b/src/media/stream.rs index 473601e..110a578 100644 --- a/src/media/stream.rs +++ b/src/media/stream.rs @@ -124,7 +124,7 @@ impl Stream { let mut map = data.meta.clone(); map.insert("variants".to_string(), data.data.remove(0).into()); - let mut stream: Stream = serde_json::from_value(serde_json::to_value(map)?)?; + let mut stream: Stream = serde_json::from_value(Value::Object(map))?; stream.executor = executor; stream.version_request_url = Some(base.as_ref().to_string()); @@ -160,7 +160,7 @@ impl Stream { .map_or(serde_json::Map::new().into(), |s| s); data.insert("variants".to_string(), variants); - let mut stream: Stream = serde_json::from_value(serde_json::to_value(data)?)?; + let mut stream: Stream = serde_json::from_value(Value::Object(data))?; stream.executor = executor; Ok(stream) @@ -238,8 +238,9 @@ pub struct Subtitle { impl Subtitle { pub async fn write_to(self, w: &mut impl Write) -> Result<()> { let resp = self.executor.get(self.url).request_raw().await?; - w.write_all(resp.as_ref()) - .map_err(|e| Error::Input(e.to_string().into()))?; + w.write_all(resp.as_ref()).map_err(|e| Error::Input { + message: e.to_string(), + })?; Ok(()) } } diff --git a/src/media/streaming.rs b/src/media/streaming.rs index e56cdf6..16999ee 100644 --- a/src/media/streaming.rs +++ b/src/media/streaming.rs @@ -41,9 +41,9 @@ impl Stream { ) .await } else { - Err(Error::Input( - format!("could not find any stream with hardsub locale '{}'", locale).into(), - )) + Err(Error::Input { + message: format!("could not find any stream with hardsub locale '{}'", locale), + }) } } else if let Some(raw_streams) = self.variants.get(&Locale::Custom("".into())) { VariantData::from_hls_master( @@ -58,7 +58,9 @@ impl Stream { ) .await } else { - Err(Error::Internal("could not find supported stream".into())) + Err(Error::Internal { + message: "could not find supported stream".to_string(), + }) } } @@ -87,26 +89,32 @@ impl Stream { if let Some(raw_streams) = self.variants.get(&locale) { raw_streams.adaptive_dash.as_ref().unwrap().url.clone() } else { - return Err(Error::Input( - format!("could not find any stream with hardsub locale '{}'", locale).into(), - )); + return Err(Error::Input { + message: format!("could not find any stream with hardsub locale '{}'", locale), + }); } } else if let Some(raw_streams) = self.variants.get(&Locale::Custom("".into())) { raw_streams.adaptive_dash.as_ref().unwrap().url.clone() } else { - return Err(Error::Internal("could not find supported stream".into())); + return Err(Error::Internal { + message: "could not find supported stream".to_string(), + }); }; let mut video = vec![]; let mut audio = vec![]; - let raw_mpd = self.executor.get(url).request_raw().await?; + let raw_mpd = self.executor.get(&url).request_raw().await?; let period = dash_mpd::parse( String::from_utf8_lossy(raw_mpd.as_slice()) .to_string() .as_str(), ) - .map_err(|e| Error::Decode(e.to_string().into()))? + .map_err(|e| Error::Decode { + message: e.to_string(), + content: raw_mpd, + url, + })? .periods[0] .clone(); let adaptions = period.adaptations; @@ -202,10 +210,14 @@ pub struct VariantData { impl VariantData { #[cfg(feature = "hls-stream")] async fn from_hls_master(executor: Arc, url: String) -> Result> { - let raw_master_playlist = executor.get(url).request_raw().await?; + let raw_master_playlist = executor.get(&url).request_raw().await?; let master_playlist = m3u8_rs::parse_master_playlist_res(raw_master_playlist.as_slice()) - .map_err(|e| Error::Decode(e.to_string().into()))?; + .map_err(|e| Error::Decode { + message: e.to_string(), + content: raw_master_playlist.clone(), + url, + })?; let mut stream_data: Vec = vec![]; @@ -367,12 +379,16 @@ impl VariantData { #[allow(irrefutable_let_patterns)] let VariantDataUrl::Hls { url } = &self.url else { - return Err(Error::Internal("variant url should be hls".into())) + return Err(Error::Internal { message: "variant url should be hls".to_string() }) }; let raw_media_playlist = self.executor.get(url).request_raw().await?; let media_playlist = m3u8_rs::parse_media_playlist_res(raw_media_playlist.as_slice()) - .map_err(|e| Error::Decode(e.to_string().into()))?; + .map_err(|e| Error::Decode { + message: e.to_string(), + content: raw_media_playlist.clone(), + url: url.clone(), + })?; let mut segments: Vec = vec![]; let mut key: Option = None; @@ -424,7 +440,7 @@ impl VariantData { async fn dash_segments(&self) -> Result> { #[allow(irrefutable_let_patterns)] let VariantDataUrl::MpegDash { id, base, init, fragments, start, lengths } = self.url.clone() else { - return Err(Error::Internal("variant url should be dash".into())) + return Err(Error::Internal{ message: "variant url should be dash".to_string() }) }; let mut segments = vec![VariantSegment { @@ -475,9 +491,16 @@ impl VariantSegment { pub fn decrypt(segment_bytes: &mut [u8], key: Option) -> Result<&[u8]> { use aes::cipher::BlockDecryptMut; if let Some(key) = key { + // yes, the input bytes are copied into a new vec just for a better error output. + // probably not worth it but better safe than sorry + let error_segment_content = segment_bytes.to_vec(); let decrypted = key .decrypt_padded_mut::(segment_bytes) - .map_err(|e| Error::Decode(e.to_string().into()))?; + .map_err(|e| Error::Decode { + message: e.to_string(), + content: error_segment_content, + url: "n/a".to_string(), + })?; Ok(decrypted) } else { Ok(segment_bytes) @@ -492,7 +515,9 @@ impl VariantSegment { segment.borrow_mut(), self.key.clone(), )?) - .map_err(|e| Error::Input(e.to_string().into()))?; + .map_err(|e| Error::Input { + message: e.to_string(), + })?; Ok(()) }