fullporner fix

This commit is contained in:
Simon
2026-10-09 20:52:33 +00:00
parent e204202611
commit 7becc3305d
3 changed files with 125 additions and 30 deletions

View File

@@ -172,7 +172,7 @@ impl FullpornerorgProvider {
let card_sel = Self::selector("article.loop-video[data-video-id]")?;
let link_sel = Self::selector("a[href]")?;
let thumb_attr_sel = Self::selector("[data-main-thumb]")?;
let title_sel = Self::selector("header.entry-header h2, header.entry-header .entry-title a")?;
let title_sel = Self::selector("header.entry-header span")?;
let duration_sel = Self::selector(".duration")?;
let actor_regex = Regex::new(r"\bactors-([a-z0-9-]+)")
.map_err(|error| Error::from(format!("regex failed: {error}")))?;
@@ -259,27 +259,44 @@ impl FullpornerorgProvider {
Ok(stubs)
}
/// Extracts the luluvdo.com embed URL from a fullporner.org detail page.
/// Extracts the embed URL (luluvdo.com, luluvid.com, lulustream.com, lulust.com, or myvidplay.com) from a fullporner.org detail page.
fn extract_embed_url(html: &str) -> Option<String> {
// The embed is in an iframe: <iframe src="https://luluvdo.com/e/VIDEO_ID" ...>
let needle = "luluvdo.com/e/";
let needle_idx = html.find(needle)?;
// Walk back to the start of the URL (find `https://` or `http://`).
let head = &html[..=needle_idx];
let scheme_idx = head
.rfind("https://")
.or_else(|| head.rfind("http://"))?;
let after_scheme = &html[scheme_idx..];
// End of the URL: the next quote, space, or `>`.
let end = after_scheme
.find(|c: char| c == '"' || c == '\'' || c == ' ' || c == '>')
.unwrap_or(after_scheme.len());
Some(after_scheme[..end].to_string())
// Also check for myvidplay.com and lulust.com
let needles = [
"luluvdo.com/e/",
"luluvid.com/e/",
"lulustream.com/e/",
"lulust.com/e/",
"myvidplay.com/e/",
];
for needle in needles {
if let Some(needle_idx) = html.find(needle) {
// Walk back to the start of the URL (find `https://` or `http://`).
let head = &html[..=needle_idx];
let scheme_idx = head
.rfind("https://")
.or_else(|| head.rfind("http://"))?;
let after_scheme = &html[scheme_idx..];
// End of the URL: the next quote, space, or `>`.
let end = after_scheme
.find(|c: char| c == '"' || c == '\'' || c == ' ' || c == '>')
.unwrap_or(after_scheme.len());
return Some(after_scheme[..end].to_string());
}
}
None
}
/// Extracts the embed ID from a `https://luluvdo.com/e/{id}` or `https://luluvid.com/e/{id}` URL.
/// Extracts the embed ID from a supported embed URL.
fn extract_embed_id(embed_url: &str) -> Option<String> {
let prefixes = ["luluvdo.com/e/", "luluvid.com/e/", "lulustream.com/e/"];
let prefixes = [
"luluvdo.com/e/",
"luluvid.com/e/",
"lulustream.com/e/",
"lulust.com/e/",
"myvidplay.com/e/",
];
for prefix in prefixes {
if let Some(idx) = embed_url.find(prefix) {
let after = &embed_url[idx + prefix.len()..];

View File

@@ -32,6 +32,10 @@ impl LulustreamProxy {
(format!("https://{endpoint}"), Some("luluvid.com"))
} else if endpoint.starts_with("luluvdo.com/") {
(format!("https://{endpoint}"), Some("luluvdo.com"))
} else if endpoint.starts_with("lulust.com/") {
(format!("https://{endpoint}"), Some("lulust.com"))
} else if endpoint.starts_with("myvidplay.com/") {
(format!("https://{endpoint}"), Some("myvidplay.com"))
} else {
(format!("https://lulustream.com/{endpoint}"), Some("lulustream.com"))
};
@@ -62,7 +66,12 @@ impl LulustreamProxy {
let Some(host) = parsed.host_str() else {
return false;
};
(host == "lulustream.com" || host == "www.lulustream.com" || host == "luluvid.com" || host == "luluvdo.com")
(host == "lulustream.com"
|| host == "www.lulustream.com"
|| host == "luluvid.com"
|| host == "luluvdo.com"
|| host == "lulust.com"
|| host == "myvidplay.com")
&& !parsed.path().is_empty()
&& parsed.path() != "/"
}
@@ -95,19 +104,25 @@ impl LulustreamProxy {
// 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())?;
// Search ALL script tags for a packed script (not just the second one).
for script_content in html.split("<script type='text/javascript'>").skip(1) {
let packed = script_content.split("</script>").next()?;
if packed.contains("eval(function(p,a,c,k,e,d)") {
if let Ok(unpacked) = dean_edwards::unpack(packed) {
if let Some(url) = unpacked
.split("[{file:\"")
.nth(1)
.and_then(|s| s.split('"').next())
.map(|s| s.to_string())
.filter(|s| !s.is_empty() && s.contains(".m3u8"))
{
return Some(url);
}
}
}
}
let unpacked = dean_edwards::unpack(packed).ok()?;
unpacked
.split("[{file:\"")
.nth(1)
.and_then(|s| s.split('"').next())
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
None
}
async fn try_chrome_extraction(embed_url: &str) -> Option<String> {
@@ -203,4 +218,33 @@ mod tests {
let url = LulustreamProxy::extract_media_url(&html);
assert!(url.is_some(), "should extract a URL from packed script");
}
#[test]
fn extracts_media_url_from_packed_script_standard_format() {
// Standard Dean Edwards format (base 10, simple example)
// Original: jwplayer("vplayer").setup({sources:[{file:"https://cdn.example.com/video.m3u8"}]})
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 standard packed script");
let url = url.unwrap();
assert!(url.contains(".m3u8"), "URL should be an m3u8: {}", url);
assert!(url.contains("cdn.example.com"), "URL should be from example CDN: {}", url);
}
#[test]
fn extracts_media_url_from_multiple_script_tags() {
// Test that extract_media_url finds the URL in any script tag, not just the second
let html = r#"
<html>
<script type='text/javascript'>var x = 1;</script>
<script type='text/javascript'>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://cdn123.cdn-tnmr.org/hls2/test/master.m3u8?t=abc\"}]")',10,2,'sources|file'.split('|'),0,{}))</script>
<script type='text/javascript'>var y = 2;</script>
</html>
"#;
let url = LulustreamProxy::extract_media_url(html);
assert!(url.is_some(), "should extract URL from any script tag");
let url = url.unwrap();
assert_eq!(url, "https://cdn123.cdn-tnmr.org/hls2/test/master.m3u8?t=abc");
}
}

View File

@@ -156,6 +156,8 @@ fn pad_to(mut v: Vec<String>, n: usize) -> Vec<String> {
/// Parse a single-quoted or double-quoted JS string literal at the start of `s`.
/// Returns (unescaped content, remainder after closing quote).
/// Handles standard JS escape sequences and allows the opposite quote type to appear
/// unescaped inside the string (e.g., 'hello "world"' or "hello 'world'").
fn parse_js_string(s: &str) -> Option<(String, &str)> {
let mut chars = s.char_indices();
let (_, quote) = chars.next()?;
@@ -181,6 +183,7 @@ fn parse_js_string(s: &str) -> Option<(String, &str)> {
} else if ch == quote {
return Some((result, &s[i + ch.len_utf8()..]));
} else {
// In JS, the opposite quote type can appear unescaped inside a string
result.push(ch);
}
}
@@ -331,4 +334,35 @@ mod tests {
let result = unpack(packed).unwrap();
assert_eq!(result, "hello world rust");
}
#[test]
fn test_parse_js_string_with_escapes() {
// Test escaped single quote
let input = r#"'hello\'world'"#;
let result = parse_js_string(input);
println!("Result: {:?}", result);
assert_eq!(result, Some(("hello'world".to_string(), "")));
// Test multiple escapes
let input2 = r#"'a\'b\'c'"#;
let result2 = parse_js_string(input2);
println!("Result2: {:?}", result2);
assert_eq!(result2, Some(("a'b'c".to_string(), "")));
}
#[test]
fn test_unpack_with_escaped_payload() {
// Payload with escaped quotes (base 10 for simplicity)
// Original payload: hello 'world' rust
// Packed with base 10, words: ["hello", "world", "rust"]
// Payload after packing: "0 '1' 2"
let 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\' 2',10,3,'hello|world|rust'.split('|'),0,{}))"#;
let result = unpack(packed);
println!("Unpack result: {:?}", result.as_ref().map(|s| s.len()));
if let Ok(ref u) = result {
println!("Unpacked: {}", u);
}
assert!(result.is_ok(), "should unpack with escaped payload");
assert_eq!(result.unwrap(), "hello 'world' rust");
}
}