videos fixing protocol
This commit is contained in:
@@ -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<Requester>,
|
||||
) -> 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("<script type='text/javascript'>").nth(1).and_then(|t| t.split("</script>").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<wreq::Client> {
|
||||
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<String> {
|
||||
// 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("<script type='text/javascript'>")
|
||||
.nth(1)
|
||||
.and_then(|t| t.split("</script>").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<String> {
|
||||
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<Requester>) -> 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!("<script type='text/javascript'>{fake_packed}</script>");
|
||||
let url = LulustreamProxy::extract_media_url(&html);
|
||||
assert!(url.is_some(), "should extract a URL from packed script");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user