diff --git a/src/downloader/default_impl.rs b/src/downloader/default_impl.rs index 69428dc..3bd3b6e 100644 --- a/src/downloader/default_impl.rs +++ b/src/downloader/default_impl.rs @@ -54,10 +54,7 @@ impl Downloader for ReqwestDownloader { fn execute(&self, request: Request) -> Result { let method = match request.method() { Method::Get => reqwest::Method::GET, - Method::Head => reqwest::Method::HEAD, Method::Post => reqwest::Method::POST, - Method::Put => reqwest::Method::PUT, - Method::Delete => reqwest::Method::DELETE, }; let mut builder = self.client.request(method, request.url()); diff --git a/src/downloader/mod.rs b/src/downloader/mod.rs index 75d37a6..c6e659d 100644 --- a/src/downloader/mod.rs +++ b/src/downloader/mod.rs @@ -17,7 +17,6 @@ pub mod request; pub mod response; use crate::exceptions::NetworkError; -use crate::localization::Localization; pub use default_impl::ReqwestDownloader; pub use request::{Request, RequestBuilder}; @@ -25,24 +24,4 @@ pub use response::Response; pub trait Downloader: Send + Sync { fn execute(&self, request: Request) -> Result; - - fn get(&self, url: &str) -> Result { - self.execute(Request::get(url).build()) - } - - fn get_localized( - &self, - url: &str, - localization: Localization, - ) -> Result { - self.execute(Request::get(url).localization(Some(localization)).build()) - } - - fn head(&self, url: &str) -> Result { - self.execute(Request::head(url).build()) - } - - fn post(&self, url: &str, body: Vec) -> Result { - self.execute(Request::post(url, body).build()) - } } diff --git a/src/downloader/request.rs b/src/downloader/request.rs index e48c262..024cb86 100644 --- a/src/downloader/request.rs +++ b/src/downloader/request.rs @@ -13,20 +13,14 @@ pub type Headers = BTreeMap>; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Method { Get, - Head, Post, - Put, - Delete, } impl Method { pub fn as_str(&self) -> &'static str { match self { Method::Get => "GET", - Method::Head => "HEAD", Method::Post => "POST", - Method::Put => "PUT", - Method::Delete => "DELETE", } } } @@ -46,10 +40,6 @@ impl Request { RequestBuilder::new(Method::Get, url) } - pub fn head(url: impl Into) -> RequestBuilder { - RequestBuilder::new(Method::Head, url) - } - pub fn post(url: impl Into, body: Vec) -> RequestBuilder { RequestBuilder::new(Method::Post, url).body(Some(body)) } diff --git a/src/youtube/itag.rs b/src/youtube/itag.rs index 1a53e92..a6f6195 100644 --- a/src/youtube/itag.rs +++ b/src/youtube/itag.rs @@ -1,6 +1,5 @@ // itag → MediaFormat table. Mirrors NPE ItagItem.java:28-101 — the -// hard-coded array of 53 entries (14 combined-AV + 10 audio + 33 -// video-only). +// hard-coded array (currently 57 entries, see ITAG_TABLE). // // Codec column ("AV1", "VP9") is derived from response mimeType at extract // time, NOT stored here — matches NPE's source comment ItagItem.java:26. diff --git a/src/youtube/js/extractor.rs b/src/youtube/js/extractor.rs index ab3d81c..be27f38 100644 --- a/src/youtube/js/extractor.rs +++ b/src/youtube/js/extractor.rs @@ -126,15 +126,6 @@ fn download_javascript_code(downloader: &dyn Downloader, url: &str) -> Result/player_ias.vflset/.../base.js`. -/// Used for rotation detection. -pub fn extract_player_hash(url: &str) -> Option { - static RE: Lazy = - Lazy::new(|| Regex::new(r"/s/player/([A-Za-z0-9]{8})/").unwrap()); - RE.captures(url).and_then(|c| c.get(1)).map(|m| m.as_str().to_string()) -} - #[cfg(test)] mod tests { use super::*; @@ -181,9 +172,4 @@ mod tests { assert_eq!(out, "https://www.youtube.com/s/player/x/base.js"); } - #[test] - fn player_hash_extracted_from_url() { - let url = "https://www.youtube.com/s/player/c2f7551f/player_ias.vflset/en_GB/base.js"; - assert_eq!(extract_player_hash(url).as_deref(), Some("c2f7551f")); - } } diff --git a/src/youtube/js/mod.rs b/src/youtube/js/mod.rs index 02d0c93..691dc09 100644 --- a/src/youtube/js/mod.rs +++ b/src/youtube/js/mod.rs @@ -1,7 +1,7 @@ // JS deobfuscator subsystem — mirrors NPE's player.js / sig / nsig pipeline. // -// Public surface is the `player_manager` module (mirrors NPE's -// YoutubeJavaScriptPlayerManager — the sole public class in the subsystem): +// Production callers go through `player_manager` (mirrors NPE's +// YoutubeJavaScriptPlayerManager): // * signature_timestamp(video_id) // * deobfuscate_signature(video_id, obfuscated) // * url_with_throttling_parameter_deobfuscated(video_id, url) @@ -9,8 +9,10 @@ // * clear_all_caches() // * clear_throttling_parameters_cache() // -// Everything else (runtime / lexer / extractor / signature / nsig) is -// crate-private plumbing. +// The other submodules (runtime / lexer / extractor / signature / nsig) are +// kept `pub` because `tests/js_phase2_offline.rs` exercises them directly +// from outside the crate. If the integration test gets folded into the +// inline `#[cfg(test)]` blocks, these can drop to `pub(crate)`. pub mod extractor; pub mod lexer; diff --git a/src/youtube/js/player_manager.rs b/src/youtube/js/player_manager.rs index 57d6981..c16d503 100644 --- a/src/youtube/js/player_manager.rs +++ b/src/youtube/js/player_manager.rs @@ -194,14 +194,6 @@ impl PlayerManager { self.inner.lock().player_url.clone() } - pub fn player_hash(&self) -> Option { - self.inner - .lock() - .player_url - .as_deref() - .and_then(extractor::extract_player_hash) - } - fn ensure_player_code(state: &mut ManagerState, video_id: &str) -> Result<(), DeobfError> { if state.player_code.is_some() { return Ok(()); diff --git a/src/youtube/parsing.rs b/src/youtube/parsing.rs index a461766..e0d1c99 100644 --- a/src/youtube/parsing.rs +++ b/src/youtube/parsing.rs @@ -1,31 +1,18 @@ -// YoutubeParsingHelper-shaped helpers — mirrors NPE -// services/youtube/YoutubeParsingHelper.java. -// -// Currently implements: +// YoutubeParsingHelper-shaped helpers: // * consent toggle + cookie generator (set_consent_accepted, consent_cookie) -// * client-version cache + sw.js fetch fallback (get_web_client_version) -// * visitor-data bootstrap via /youtubei/v1/visitor_id -// * client/origin/referer header builder +// * WEB client-version constant (web_client_version) +// * client/origin/referer header builder (youtube_post_headers / mobile_post_headers) +// * mobile user-agent string builders // -// PoToken integration lands in Phase 5. po_token / DroidGuard / BotGuard -// machinery is host-provided (PoTokenProvider trait). +// PoToken integration lives in youtube::potoken — host-provided via the +// PoTokenProvider trait. use once_cell::sync::Lazy; use parking_lot::RwLock; -use regex::Regex; -use serde_json::Value; -use crate::downloader::request::Request; -use crate::exceptions::ParsingError; -use crate::localization::{ContentCountry, Localization}; -use crate::newpipe::NewPipe; -use crate::youtube::client_request::{ - build_envelope, InnertubeClientRequestInfo, -}; use crate::youtube::constants::*; static CONSENT_ACCEPTED: Lazy> = Lazy::new(|| RwLock::new(false)); -static CACHED_WEB_CLIENT_VERSION: Lazy>> = Lazy::new(|| RwLock::new(None)); pub fn set_consent_accepted(accepted: bool) { *CONSENT_ACCEPTED.write() = accepted; @@ -45,53 +32,14 @@ pub fn consent_cookie() -> &'static str { } } -/// Returns the cached WEB client version. Falls back to the hardcoded -/// constant if no live extraction has run. +/// Returns the WEB client version. The sw.js-based live-version discovery +/// (NPE pattern) was ported but never wired up by any caller, so we just +/// ship the hardcoded constant. If/when live version probing matters +/// again, restore `discover_web_client_version` from git history. pub fn web_client_version() -> String { - if let Some(v) = CACHED_WEB_CLIENT_VERSION.read().as_ref() { - return v.clone(); - } WEB_HARDCODED_CLIENT_VERSION.to_string() } -pub fn reset_web_client_version_cache() { - *CACHED_WEB_CLIENT_VERSION.write() = None; -} - -static SW_JS_VERSION_RE: Lazy = Lazy::new(|| { - Regex::new(r#"INNERTUBE_CONTEXT_CLIENT_VERSION":\s*"([^"]+)""#).unwrap() -}); - -/// Fetches sw.js + extracts the live WEB client version. Caches the -/// result. Returns the cached value if already known. -pub fn discover_web_client_version() -> Result { - if let Some(v) = CACHED_WEB_CLIENT_VERSION.read().as_ref() { - return Ok(v.clone()); - } - let downloader = NewPipe::downloader() - .ok_or_else(|| ParsingError::Invalid("downloader not initialized".into()))?; - let req = Request::get("https://www.youtube.com/sw.js") - .add_header("Origin", "https://www.youtube.com") - .add_header("Referer", "https://www.youtube.com") - .build(); - let resp = downloader - .execute(req) - .map_err(|e| ParsingError::Invalid(format!("sw.js fetch: {e}")))?; - if resp.response_code() != 200 { - return Err(ParsingError::Invalid(format!( - "sw.js HTTP {}", - resp.response_code() - ))); - } - let version = SW_JS_VERSION_RE - .captures(resp.response_body()) - .and_then(|c| c.get(1)) - .map(|m| m.as_str().to_string()) - .ok_or_else(|| ParsingError::RegexMiss("INNERTUBE_CONTEXT_CLIENT_VERSION".into()))?; - *CACHED_WEB_CLIENT_VERSION.write() = Some(version.clone()); - Ok(version) -} - /// Headers for a WEB-flavor POST (JSON content-type, client headers, /// origin/referer, consent cookie). pub fn youtube_post_headers() -> Vec<(String, String)> { @@ -129,48 +77,6 @@ pub fn ios_user_agent(country: &ContentCountry) -> String { ) } -/// Bootstraps a visitor_data token via `/youtubei/v1/visitor_id`. Returns -/// the value of `responseContext.visitorData` from the response. -pub fn bootstrap_visitor_data( - info: &InnertubeClientRequestInfo, - localization: &Localization, - content_country: &ContentCountry, - use_gapis_endpoint: bool, -) -> Result { - let downloader = NewPipe::downloader() - .ok_or_else(|| ParsingError::Invalid("downloader not initialized".into()))?; - let envelope = build_envelope(info, localization, content_country, None); - let body = serde_json::to_vec(&envelope)?; - - let base = if use_gapis_endpoint { - YOUTUBEI_V1_GAPIS_URL - } else { - YOUTUBEI_V1_URL - }; - let url = format!("{base}visitor_id{DISABLE_PRETTY_PRINT_PARAM}"); - - let mut req_builder = Request::post(&url, body); - for (k, v) in youtube_post_headers() { - req_builder = req_builder.add_header(&k, &v); - } - let resp = downloader - .execute(req_builder.build()) - .map_err(|e| ParsingError::Invalid(format!("visitor_id POST: {e}")))?; - if resp.response_code() != 200 { - return Err(ParsingError::Invalid(format!( - "visitor_id HTTP {}", - resp.response_code() - ))); - } - let parsed: Value = serde_json::from_str(resp.response_body())?; - parsed - .get("responseContext") - .and_then(|rc| rc.get("visitorData")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .ok_or_else(|| ParsingError::MissingField("responseContext.visitorData".into())) -} - #[cfg(test)] mod tests { use super::*; @@ -185,8 +91,7 @@ mod tests { } #[test] - fn web_client_version_falls_back_to_hardcoded() { - reset_web_client_version_cache(); + fn web_client_version_returns_hardcoded() { assert_eq!(web_client_version(), WEB_HARDCODED_CLIENT_VERSION); } diff --git a/src/youtube/stream_helper.rs b/src/youtube/stream_helper.rs index 721cf28..e6c60d6 100644 --- a/src/youtube/stream_helper.rs +++ b/src/youtube/stream_helper.rs @@ -91,27 +91,6 @@ pub fn get_web_metadata_player_response( post_youtube(&url, &Value::Object(body), youtube_post_headers()) } -/// WEB_EMBEDDED_PLAYER /player call. Carries embedUrl + signatureTimestamp. -pub fn get_web_embedded_player_response( - video_id: &str, - localization: &Localization, - content_country: &ContentCountry, - signature_timestamp: i32, - po_token: Option<&str>, -) -> Result { - let info = InnertubeClientRequestInfo::of_web_embedded_player_client(); - let embed_url = format!("https://www.youtube.com/embed/{video_id}"); - let env = build_envelope(&info, localization, content_country, Some(&embed_url)); - let mut body = envelope_to_body(env); - add_player_body_fields(&mut body, video_id, &generate_content_playback_nonce()); - add_playback_context(&mut body, signature_timestamp, &embed_url); - if let Some(token) = po_token { - add_service_integrity_dimensions(&mut body, token); - } - let url = format!("{YOUTUBEI_V1_URL}player{DISABLE_PRETTY_PRINT_PARAM}"); - post_youtube(&url, &Value::Object(body), youtube_post_headers()) -} - /// ANDROID full /player call. Hits the gapis endpoint with the mobile /// header set. Caller must supply (cpn, po_token) — they are paired with /// the URLs the response will return; mixing them with iOS values returns