fullporner fix
This commit is contained in:
@@ -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()..];
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user