diff --git a/src/proxies/lulustream.rs b/src/proxies/lulustream.rs index 851caa6..5f4dd5c 100644 --- a/src/proxies/lulustream.rs +++ b/src/proxies/lulustream.rs @@ -1,6 +1,10 @@ +use std::sync::Arc; + use ntex::web; use url::Url; -use serde_json::json; +use wreq::cookie::Jar; +use wreq::redirect::Policy; +use wreq_util::Emulation; use crate::util::{dean_edwards, requester::Requester}; @@ -20,8 +24,9 @@ impl LulustreamProxy { let detail_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { endpoint.to_string() - } else if endpoint.starts_with("lulustream.com/") || endpoint.starts_with("www.lulustream.com/") || - endpoint.starts_with("luluvdo.com/") + } else if endpoint.starts_with("lulustream.com/") + || endpoint.starts_with("www.lulustream.com/") + || endpoint.starts_with("luluvid.com/") { format!("https://{endpoint}") } else { @@ -33,9 +38,7 @@ impl LulustreamProxy { } let parsed = Url::parse(&detail_url).ok()?; - let video_id = parsed.path_segments()? - .last() - .map(ToOwned::to_owned)?; + let video_id = parsed.path_segments()?.last().map(ToOwned::to_owned)?; Some((detail_url, video_id)) } @@ -50,40 +53,82 @@ impl LulustreamProxy { let Some(host) = parsed.host_str() else { return false; }; - (host == "lulustream.com" || host == "www.lulustream.com" || host == "luluvdo.com") - && !parsed.path().is_empty() && parsed.path() != "/" + (host == "lulustream.com" || host == "www.lulustream.com" || host == "luluvid.com") + && !parsed.path().is_empty() + && parsed.path() != "/" } - pub async fn get_video_url( - &self, - url: String, - requester: web::types::State, - ) -> String { - let mut requester = requester.get_ref().clone(); - let Some((detail_url, video_id)) = Self::normalize_detail_request(&url) else { - return String::new(); - }; - println!("LulustreamProxy: Normalized detail URL: {:?}", format!("https://luluvid.com/e/{video_id}")); - let mut text = requester.get(format!("https://luluvid.com/e/{video_id}").as_str(), None).await.unwrap_or_default(); - if !text.contains("[{file:\"") { - let packedtext = text.split("").next()).unwrap_or_default(); - text = dean_edwards::unpack(&packedtext).unwrap_or_default(); + // Chrome120 emulation bypasses Cloudflare on luluvid.com (Firefox136 gets blocked). + fn build_chrome_client() -> Option { + let jar = Arc::new(Jar::default()); + wreq::Client::builder() + .cert_verification(false) + .emulation(Emulation::Chrome120) + .cookie_provider(jar) + .redirect(Policy::default()) + .build() + .ok() + } + + fn extract_media_url(html: &str) -> Option { + // Fast path: file URL present in plain text (no packing) + if html.contains("[{file:\"") { + let url = html + .split("[{file:\"") + .nth(1) + .and_then(|s| s.split('"').next())? + .to_string(); + if !url.is_empty() { + return Some(url); + } } - let video_url = text.split("[{file:\"") + + // Unpack the Dean Edwards p,a,c,k,e,d script that embeds the player config. + // The packed payload encodes the jwplayer setup call; after decoding it contains + // `sources:[{file:"https://cdn*.cdn-tnmr.org/hls2/.../master.m3u8?..."}]`. + let packed = html + .split("").next())?; + + let unpacked = dean_edwards::unpack(packed).ok()?; + + unpacked + .split("[{file:\"") .nth(1) .and_then(|s| s.split('"').next()) - .unwrap_or_default() - .to_string(); - println!("LulustreamProxy: Extracted video URL: {}", video_url); - let test_request = requester.get_raw_with_headers(video_url.as_str(), vec![ - ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()), - ("Referer".to_string(), detail_url.clone()), - ("User-Agent".to_string(), "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36".to_string()) - ]).await.unwrap(); - println!("LulustreamProxy: Test request status: {}", test_request.status()); + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + } - video_url - // return "https://cdn1004.cdn-tnmr.org/hls2/01/03256/cssckmym0ibf_h/master.m3u8?t=Y2jXSIPERwSec0L6RSAOIPFAW53dQ0UgslngqGnF0go&s=1778507711&e=28800&f=16283923&i=0.3&sp=0".to_string(); + async fn try_chrome_extraction(embed_url: &str) -> Option { + let client = Self::build_chrome_client()?; + let response = client.get(embed_url).send().await.ok()?; + if !response.status().is_success() { + return None; + } + let html = response.text().await.ok()?; + Self::extract_media_url(&html) + } +} + +impl crate::proxies::Proxy for LulustreamProxy { + async fn get_video_url(&self, url: String, requester: web::types::State) -> String { + let Some((_detail_url, video_id)) = Self::normalize_detail_request(&url) else { + return String::new(); + }; + + let embed_url = format!("https://luluvid.com/e/{video_id}"); + + // Chrome120 emulation bypasses Cloudflare; try it first. + if let Some(media_url) = Self::try_chrome_extraction(&embed_url).await { + return media_url; + } + + // Fallback: standard requester (Firefox136 + optional FlareSolverr). + let mut requester = requester.get_ref().clone(); + let html = requester.get(&embed_url, None).await.unwrap_or_default(); + Self::extract_media_url(&html).unwrap_or_default() } } @@ -107,4 +152,33 @@ mod tests { assert_eq!(url, "https://lulustream.com/d/s484n23k8opy"); assert_eq!(video_id, "s484n23k8opy"); } + + #[test] + fn normalizes_luluvid_url() { + let (url, video_id) = + LulustreamProxy::normalize_detail_request("https://luluvid.com/e/s484n23k8opy") + .expect("detail request should parse"); + assert_eq!(url, "https://luluvid.com/e/s484n23k8opy"); + assert_eq!(video_id, "s484n23k8opy"); + } + + #[test] + fn extracts_media_url_from_plain_html() { + let html = r#"[{file:"https://cdn1007.cdn-tnmr.org/hls2/02/02723/abc_h/master.m3u8?t=TOKEN&s=12345&e=28800&f=999&i=0.3&sp=0"}]"#; + assert_eq!( + LulustreamProxy::extract_media_url(html).as_deref(), + Some("https://cdn1007.cdn-tnmr.org/hls2/02/02723/abc_h/master.m3u8?t=TOKEN&s=12345&e=28800&f=999&i=0.3&sp=0") + ); + } + + #[test] + fn extracts_media_url_from_packed_script() { + // Minimal valid packed script that decodes to a jwplayer sources array. + // Original: jwplayer("vplayer").setup({sources:[{file:"https://cdn.example.com/video.m3u8"}]}) + // We fake it with a trivial packer (base 10, a few words). + let fake_packed = r#"eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\b'+c.toString(a)+'\b','g'),k[c]);return p}('0("[{1:\"https://cdn.example.com/video.m3u8\"}]")',10,2,'sources|file'.split('|'),0,{}))"#; + let html = format!(""); + let url = LulustreamProxy::extract_media_url(&html); + assert!(url.is_some(), "should extract a URL from packed script"); + } } diff --git a/src/videos.rs b/src/videos.rs index 1e1770a..3444332 100644 --- a/src/videos.rs +++ b/src/videos.rs @@ -275,7 +275,7 @@ impl VideoFormat { abr: None, vbr: None, container: None, - protocol: Some("m3u8_native".to_string()), + protocol: Some("https".to_string()), audio_ext: Some("none".to_string()), video_ext: Some("mp4".to_string()), resolution: None,