Fix six dead providers (shooshtime, hdporn92, camsoda, sextb, supjav, hentaimama)
- shooshtime: update parse_list_videos selectors for the site's redesigned
markup (div.card / a.title / a.image img / div.duration / div.list div.box
/ div.name a.text)
- hdporn92: mirror the site's own links.hls4||hls3||hls2 fallback and scan
every P.A.C.K.E.R. block instead of assuming the first one is the player
block; prefer a genuine .m3u8 candidate over a decoy .txt one
- camsoda: replace naive first-`{`/last-`}` JSON extraction with a proper
balanced-brace scan so trailing wrapper markup can't corrupt the parse
("key must be a string")
- sextb: fix mirror_url() double-prefixing the scheme when building Jina
mirror URLs (produced a malformed https://r.jina.ai/http://https://... URL)
- supjav: switch curl_cffi impersonation from chrome to safari18_0 - Cloudflare
blocks the chrome/firefox profiles outright but passes Safari
- hentaimama: resolve every card on a listing page (not just the first
perPage) and truncate after filtering, so a bulk "Coming soon" placeholder
batch at the front of the feed no longer empties the whole page; report a
provider error if literally every candidate fails
Refreshes graphify's graph and docs/provider-catalog.md with the new
technical details for all six providers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTqf6orbHZ9rFFpVpcgzcR
This commit is contained in:
@@ -265,53 +265,99 @@ impl Hdporn92Provider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the P.A.C.K.E.R. `eval(function(p,a,c,k,e,d){...}(...))` block in
|
||||
/// the morencius embed page and returns the deobfuscated JavaScript source.
|
||||
fn decode_packered(source: &str) -> Option<String> {
|
||||
/// Decodes every P.A.C.K.E.R. `eval(function(p,a,c,k,e,d){...}(...))` block
|
||||
/// found in the morencius embed page and returns the deobfuscated JS
|
||||
/// source for each. The embed page carries more than one such block (an
|
||||
/// ad/VAST setup block plus the actual jwplayer setup block), so callers
|
||||
/// must scan all of them for the data they need rather than assume the
|
||||
/// first one is the relevant one.
|
||||
fn decode_packered_blocks(source: &str) -> Vec<String> {
|
||||
// Match: eval(function(p,a,c,k,e,d){...}('P',A,C,'K'.split('|')
|
||||
// We capture P, A, C, and the dictionary K.
|
||||
let re = Regex::new(
|
||||
let Ok(re) = Regex::new(
|
||||
r#"eval\(function\(p,a,c,k,e,d\)\{.*?\}\('([\s\S]*?)',(\d+),(\d+),'([\s\S]*?)'\.split\('\|'\)\)\)"#,
|
||||
)
|
||||
.ok()?;
|
||||
let caps = re.captures(source)?;
|
||||
let p = caps.get(1)?.as_str();
|
||||
let a: u32 = caps.get(2)?.as_str().parse().ok()?;
|
||||
let c: usize = caps.get(3)?.as_str().parse().ok()?;
|
||||
let k: Vec<&str> = caps.get(4)?.as_str().split('|').collect();
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
// P.A.C.K.E.R. deobfuscation: iterate c-1..=0; replace \b<token>\b with
|
||||
// dictionary[i] (the original JS uses a `\w+` lookalike via the `e`
|
||||
// parameter, but in practice every token is a base-A digit string).
|
||||
let mut result = p.to_string();
|
||||
for i in (0..c).rev() {
|
||||
let value = k.get(i).copied().unwrap_or("");
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let token = to_base(i, a);
|
||||
// Build a word-boundary regex for the token. Base-A tokens are
|
||||
// always made of [0-9a-z], so \b is a real word boundary.
|
||||
let token_re = Regex::new(&format!(r"\b{}\b", regex::escape(&token))).ok()?;
|
||||
result = token_re.replace_all(&result, value).into_owned();
|
||||
}
|
||||
Some(result)
|
||||
re.captures_iter(source)
|
||||
.filter_map(|caps| {
|
||||
let p = caps.get(1)?.as_str();
|
||||
let a: u32 = caps.get(2)?.as_str().parse().ok()?;
|
||||
let c: usize = caps.get(3)?.as_str().parse().ok()?;
|
||||
let k: Vec<&str> = caps.get(4)?.as_str().split('|').collect();
|
||||
|
||||
// P.A.C.K.E.R. deobfuscation: iterate c-1..=0; replace
|
||||
// \b<token>\b with dictionary[i] (the original JS uses a
|
||||
// `\w+` lookalike via the `e` parameter, but in practice
|
||||
// every token is a base-A digit string).
|
||||
let mut result = p.to_string();
|
||||
for i in (0..c).rev() {
|
||||
let value = k.get(i).copied().unwrap_or("");
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let token = to_base(i, a);
|
||||
// Build a word-boundary regex for the token. Base-A
|
||||
// tokens are always made of [0-9a-z], so \b is a real
|
||||
// word boundary.
|
||||
let token_re =
|
||||
Regex::new(&format!(r"\b{}\b", regex::escape(&token))).ok()?;
|
||||
result = token_re.replace_all(&result, value).into_owned();
|
||||
}
|
||||
Some(result)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Pulls the morencius `/stream/.../master.m3u8` URL out of the decoded JS.
|
||||
fn extract_hls4_url(decoded_js: &str) -> Option<String> {
|
||||
// The decoded JS has: "hls4":"/stream/<token>/<secret>/<expiry>/<file_id>/master.m3u8"
|
||||
// The value is a path (no scheme), so we prefix it.
|
||||
let re = Regex::new(r#""hls4"\s*:\s*"([^"]+)""#).ok()?;
|
||||
let caps = re.captures(decoded_js)?;
|
||||
let path = caps.get(1)?.as_str();
|
||||
if path.starts_with("http") {
|
||||
Some(path.to_string())
|
||||
} else if path.starts_with('/') {
|
||||
Some(format!("https://morencius.com{path}"))
|
||||
} else {
|
||||
Some(format!("https://morencius.com/{path}"))
|
||||
}
|
||||
/// Decodes the first P.A.C.K.E.R. block found in `source`. Kept for
|
||||
/// callers/tests that only care about a single block; production code
|
||||
/// that needs to find the block with a specific key (e.g. `hls4`) should
|
||||
/// use [`Self::decode_packered_blocks`] and scan all of them instead.
|
||||
fn decode_packered(source: &str) -> Option<String> {
|
||||
Self::decode_packered_blocks(source).into_iter().next()
|
||||
}
|
||||
|
||||
/// Pulls the morencius HLS URL out of the decoded JS. The player itself
|
||||
/// picks `links.hls4 || links.hls3 || links.hls2` (not every tier is
|
||||
/// always populated, and hls4 is frequently absent), so mirror that
|
||||
/// priority order. At least one tier has been observed serving a `.txt`
|
||||
/// decoy instead of a real `.m3u8` playlist, so within that priority
|
||||
/// order prefer a candidate that actually looks like a stream URL.
|
||||
fn extract_hls_url(decoded_js: &str) -> Option<String> {
|
||||
let re = Regex::new(r#""(hls[234])"\s*:\s*"([^"]+)""#).ok()?;
|
||||
let candidates: Vec<(String, String)> = re
|
||||
.captures_iter(decoded_js)
|
||||
.filter_map(|caps| {
|
||||
let tier = caps.get(1)?.as_str().to_string();
|
||||
let raw = caps.get(2)?.as_str().replace("\\/", "/");
|
||||
Some((tier, raw))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let normalize = |path: &str| {
|
||||
if path.starts_with("http") {
|
||||
path.to_string()
|
||||
} else if path.starts_with('/') {
|
||||
format!("https://morencius.com{path}")
|
||||
} else {
|
||||
format!("https://morencius.com/{path}")
|
||||
}
|
||||
};
|
||||
|
||||
const TIERS: [&str; 3] = ["hls4", "hls3", "hls2"];
|
||||
TIERS
|
||||
.iter()
|
||||
.filter_map(|tier| candidates.iter().find(|(t, _)| t == tier))
|
||||
.map(|(_, raw)| normalize(raw))
|
||||
.find(|url| url.to_lowercase().ends_with(".m3u8"))
|
||||
.or_else(|| {
|
||||
TIERS
|
||||
.iter()
|
||||
.filter_map(|tier| candidates.iter().find(|(t, _)| t == tier))
|
||||
.map(|(_, raw)| normalize(raw))
|
||||
.next()
|
||||
})
|
||||
}
|
||||
|
||||
/// Pulls the integer duration (in seconds, may be decimal) from the decoded
|
||||
@@ -336,11 +382,21 @@ impl Hdporn92Provider {
|
||||
|
||||
// Step 2: fetch the morencius embed page (referer = the hdporn92 detail).
|
||||
let player_html = Self::fetch_html(options, &embed_url, &stub.url).await?;
|
||||
let decoded = Self::decode_packered(&player_html)
|
||||
.ok_or_else(|| Error::from(format!("no eval block in {embed_url}")))?;
|
||||
let stream_path = Self::extract_hls4_url(&decoded)
|
||||
.ok_or_else(|| Error::from(format!("no hls4 url in decoded JS of {embed_url}")))?;
|
||||
let duration = Self::extract_duration(&decoded).unwrap_or(0);
|
||||
let decoded_blocks = Self::decode_packered_blocks(&player_html);
|
||||
if decoded_blocks.is_empty() {
|
||||
return Err(Error::from(format!("no eval block in {embed_url}")));
|
||||
}
|
||||
// A page may carry multiple P.A.C.K.E.R. blocks (e.g. an ad/VAST
|
||||
// setup block); only one decodes to JS containing the player's
|
||||
// hls2/3/4 stream keys, so scan all of them rather than assuming
|
||||
// the first.
|
||||
let decoded = decoded_blocks
|
||||
.iter()
|
||||
.find(|block| Self::extract_hls_url(block).is_some())
|
||||
.ok_or_else(|| Error::from(format!("no hls url in decoded JS of {embed_url}")))?;
|
||||
let stream_path = Self::extract_hls_url(decoded)
|
||||
.ok_or_else(|| Error::from(format!("no hls url in decoded JS of {embed_url}")))?;
|
||||
let duration = Self::extract_duration(decoded).unwrap_or(0);
|
||||
|
||||
// Build the format. morencius serves the m3u8 only when Referer is the
|
||||
// embed URL — without it, the response is 403.
|
||||
@@ -568,7 +624,7 @@ mod tests {
|
||||
.expect("deobfuscator should return Some");
|
||||
// Sanity: the decoded text contains the hls4 key.
|
||||
assert!(decoded.contains("\"hls4\""), "decoded: {decoded}");
|
||||
let url = Hdporn92Provider::extract_hls4_url(&decoded)
|
||||
let url = Hdporn92Provider::extract_hls_url(&decoded)
|
||||
.expect("hls4 should be present after decode");
|
||||
// The path was /stream/abc/secret/123/456/master.m3u8.
|
||||
assert_eq!(url, "https://morencius.com/stream/abc/secret/123/456/master.m3u8");
|
||||
@@ -603,18 +659,19 @@ mod tests {
|
||||
"src/providers/testdata/hdporn92_morencius.html",
|
||||
)
|
||||
.expect("test fixture should exist");
|
||||
let decoded = Hdporn92Provider::decode_packered(&html)
|
||||
.expect("real morencius HTML should be deobfuscable");
|
||||
// The decoded JS contains the hls4 value.
|
||||
let url = Hdporn92Provider::extract_hls4_url(&decoded)
|
||||
.expect("hls4 should be present after decode");
|
||||
assert!(
|
||||
url.starts_with("https://morencius.com/stream/"),
|
||||
"got {url}"
|
||||
);
|
||||
assert!(url.ends_with("/master.m3u8"), "got {url}");
|
||||
// The page carries several P.A.C.K.E.R. blocks (ad setup, player
|
||||
// setup, ...); only the player block decodes to JS containing hls4.
|
||||
let blocks = Hdporn92Provider::decode_packered_blocks(&html);
|
||||
assert!(!blocks.is_empty(), "real morencius HTML should be deobfuscable");
|
||||
let decoded = blocks
|
||||
.iter()
|
||||
.find(|block| Hdporn92Provider::extract_hls_url(block).is_some())
|
||||
.expect("one decoded block should contain an hls2/3/4 key");
|
||||
let url = Hdporn92Provider::extract_hls_url(decoded)
|
||||
.expect("an hls url should be present after decode");
|
||||
assert!(url.starts_with("https://"), "got {url}");
|
||||
// The duration is also present.
|
||||
let duration = Hdporn92Provider::extract_duration(&decoded);
|
||||
let duration = Hdporn92Provider::extract_duration(decoded);
|
||||
assert!(duration.unwrap_or(0) > 0, "duration should be > 0");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user