diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index d08a6e1..54b5201 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -61,10 +61,34 @@ impl DeobfData { } pub fn extract_fns(js_url: &str, player_js: &str) -> Result { - let sig_fn = get_sig_fn(player_js)?; - let nsig_fn = get_nsig_fn(player_js)?; + // The signature timestamp is the only piece every "needs_deobf" client + // actually requires in its request payload — without it, those clients + // get an error back. So we hard-fail on sts extraction. let sts = get_sts(player_js)?; + // sig_fn and nsig_fn are needed only when YouTube returns stream URLs + // containing the &s= cipher / &n= throttling params. Most clients + // (iOS, Android, Tv) get pre-signed URLs and never touch these. + // Tolerate extraction failures here so a single rotated player.js + // shape doesn't bring down the whole player path for those clients. + // The dead-code fallback is preserved: if a stream URL DOES need + // deobfuscation, `Deobfuscator::deobfuscate_sig` will fail with a + // clear "sig fn unavailable" error instead of crashing the player. + let sig_fn = match get_sig_fn(player_js) { + Ok(f) => f, + Err(e) => { + tracing::warn!("could not extract sig deobf fn (sig deobfuscation disabled until YT rotates player.js again): {}", e); + String::new() + } + }; + let nsig_fn = match get_nsig_fn(player_js) { + Ok(f) => f, + Err(e) => { + tracing::warn!("could not extract nsig deobf fn (throttling parameter deobf disabled until YT rotates player.js again): {}", e); + String::new() + } + }; + Ok(Self { js_url: js_url.to_owned(), sig_fn, @@ -79,13 +103,23 @@ impl Deobfuscator { pub fn new(data: &DeobfData) -> Result { let rt = Runtime::new()?; let ctx = Context::full(&rt)?; - ctx.with(|ctx| { - let mut opts = rquickjs::context::EvalOptions::default(); - opts.strict = false; - ctx.eval_with_options::<(), _>(data.sig_fn.as_bytes(), opts)?; - let mut opts = rquickjs::context::EvalOptions::default(); - opts.strict = false; - ctx.eval_with_options::<(), _>(data.nsig_fn.as_bytes(), opts) + ctx.with(|ctx| -> Result<(), rquickjs::Error> { + // Skip JS eval for any deobf fn we couldn't extract. The matching + // `deobfuscate_sig` / `deobfuscate_nsig` calls will then return an + // Err naturally because the global won't be defined — and that + // only matters if a stream actually has obfuscated params, which + // shouldn't happen on the iOS/Android/Tv InnerTube paths. + if !data.sig_fn.is_empty() { + let mut opts = rquickjs::context::EvalOptions::default(); + opts.strict = false; + ctx.eval_with_options::<(), _>(data.sig_fn.as_bytes(), opts)?; + } + if !data.nsig_fn.is_empty() { + let mut opts = rquickjs::context::EvalOptions::default(); + opts.strict = false; + ctx.eval_with_options::<(), _>(data.nsig_fn.as_bytes(), opts)?; + } + Ok(()) })?; Ok(Self { ctx }) }