diff --git a/src/client/player.rs b/src/client/player.rs index 8ea30cd..c085b0a 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -10,7 +10,7 @@ use url::Url; use crate::{ deobfuscate::Deobfuscator, - error::{DeobfError, Error, ExtractionError}, + error::{internal::DeobfError, Error, ExtractionError}, model::{ traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, @@ -143,8 +143,7 @@ impl MapResponse for response::Player { _lang: Language, deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { - let deobf = Deobfuscator::new(deobf.unwrap()) - .map_err(|e| ExtractionError::InvalidData(e.to_string().into()))?; + let deobf = Deobfuscator::new(deobf.unwrap())?; let mut warnings = vec![]; // Check playability status diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index e4b2705..4ce8bbd 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -4,9 +4,10 @@ use regex::Regex; use reqwest::Client; use serde::{Deserialize, Serialize}; -use crate::{error::DeobfError, util}; - -type Result = core::result::Result; +use crate::{ + error::{internal::DeobfError, Error}, + util, +}; pub struct Deobfuscator { ctx: quick_js::Context, @@ -21,7 +22,7 @@ pub struct DeobfData { } impl DeobfData { - pub async fn download(http: Client) -> Result { + pub async fn download(http: Client) -> Result { let js_url = get_player_js_url(&http).await?; let player_js = get_response(&http, &js_url).await?; @@ -41,7 +42,7 @@ impl DeobfData { } impl Deobfuscator { - pub fn new(data: &DeobfData) -> Result { + pub fn new(data: &DeobfData) -> Result { let ctx = quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?; ctx.eval(&data.sig_fn)?; @@ -50,7 +51,7 @@ impl Deobfuscator { Ok(Self { ctx }) } - pub fn deobfuscate_sig(&self, sig: &str) -> Result { + pub fn deobfuscate_sig(&self, sig: &str) -> Result { let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?; res.as_str().map_or( @@ -62,7 +63,7 @@ impl Deobfuscator { ) } - pub fn deobfuscate_nsig(&self, nsig: &str) -> Result { + pub fn deobfuscate_nsig(&self, nsig: &str) -> Result { let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?; res.as_str().map_or( @@ -78,7 +79,7 @@ impl Deobfuscator { const DEOBF_SIG_FUNC_NAME: &str = "deobf_sig"; const DEOBF_NSIG_FUNC_NAME: &str = "deobf_nsig"; -fn get_sig_fn_name(player_js: &str) -> Result { +fn get_sig_fn_name(player_js: &str) -> Result { static FUNCTION_REGEXES: Lazy<[FancyRegex; 6]> = Lazy::new(|| { [ FancyRegex::new("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)").unwrap(), @@ -98,7 +99,7 @@ fn caller_function(mapped_name: &str, fn_name: &str) -> String { format!("var {mapped_name}={fn_name};") } -fn get_sig_fn(player_js: &str) -> Result { +fn get_sig_fn(player_js: &str) -> Result { let dfunc_name = get_sig_fn_name(player_js)?; let function_pattern_str = @@ -141,7 +142,7 @@ fn get_sig_fn(player_js: &str) -> Result { + &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name)) } -fn get_nsig_fn_name(player_js: &str) -> Result { +fn get_nsig_fn_name(player_js: &str) -> Result { static FUNCTION_NAME_REGEX: Lazy = Lazy::new(|| { Regex::new("\\.get\\(\"n\"\\)\\)&&\\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9$_]\\)") .unwrap() @@ -183,7 +184,7 @@ fn get_nsig_fn_name(player_js: &str) -> Result { Ok(name.to_owned()) } -fn extract_js_fn(js: &str, name: &str) -> Result { +fn extract_js_fn(js: &str, name: &str) -> Result { let scan = ress::Scanner::new(js); let mut state = 0; let mut level = 0; @@ -235,7 +236,7 @@ fn extract_js_fn(js: &str, name: &str) -> Result { Ok(js[start..end].to_owned()) } -fn get_nsig_fn(player_js: &str) -> Result { +fn get_nsig_fn(player_js: &str) -> Result { let function_name = get_nsig_fn_name(player_js)?; let function_base = function_name.to_owned() + "=function"; let offset = player_js.find(&function_base).unwrap_or_default(); @@ -244,7 +245,7 @@ fn get_nsig_fn(player_js: &str) -> Result { .map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_name)) } -async fn get_player_js_url(http: &Client) -> Result { +async fn get_player_js_url(http: &Client) -> Result { let resp = http .get("https://www.youtube.com/iframe_api") .send() @@ -267,12 +268,12 @@ async fn get_player_js_url(http: &Client) -> Result { )) } -async fn get_response(http: &Client, url: &str) -> Result { +async fn get_response(http: &Client, url: &str) -> Result { let resp = http.get(url).send().await?.error_for_status()?; Ok(resp.text().await?) } -fn get_sts(player_js: &str) -> Result { +fn get_sts(player_js: &str) -> Result { static STS_PATTERN: Lazy = Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap()); diff --git a/src/error.rs b/src/error.rs index 101bbfc..9d5abea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,16 +2,13 @@ use std::borrow::Cow; -/// Custom error type for the RustyPipe library +/// Error type for the RustyPipe library #[derive(thiserror::Error, Debug)] #[non_exhaustive] pub enum Error { /// Error extracting content from YouTube #[error("extraction error: {0}")] Extraction(#[from] ExtractionError), - /// Error from the deobfuscater - #[error("deobfuscator error: {0}")] - Deobfuscation(#[from] DeobfError), /// File IO error #[error(transparent)] Io(#[from] std::io::Error), @@ -26,28 +23,6 @@ pub enum Error { Other(Cow<'static, str>), } -/// Error that occurred during the initialization -/// or use of the YouTube URL signature deobfuscator. -#[derive(thiserror::Error, Debug)] -#[non_exhaustive] -pub enum DeobfError { - /// Error from the HTTP client - #[error("http error: {0}")] - Http(#[from] reqwest::Error), - /// 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), -} - /// Error extracting content from YouTube #[derive(thiserror::Error, Debug)] #[non_exhaustive] @@ -80,6 +55,9 @@ pub enum ExtractionError { /// Error deserializing YouTube's response JSON #[error("deserialization error: {0}")] Deserialization(#[from] serde_json::Error), + /// Error deobfuscating YouTube's URL signatures + #[error("deobfuscation error: {0}")] + Deobfuscation(Cow<'static, str>), /// YouTube returned invalid data #[error("got invalid data from YT: {0}")] InvalidData(Cow<'static, str>), @@ -102,6 +80,40 @@ pub enum ExtractionError { DeserializationWarnings, } +pub(crate) mod internal { + use super::*; + + /// 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 for Error { + fn from(value: DeobfError) -> Self { + Self::Extraction(value.into()) + } + } + + impl From for ExtractionError { + fn from(value: DeobfError) -> Self { + Self::Deobfuscation(value.to_string().into()) + } + } +} + impl ExtractionError { pub(crate) fn should_report(&self) -> bool { matches!(