From ae516aba917315f866311fb1c404c27134d7ad43 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 10 Oct 2026 09:09:58 +0000 Subject: [PATCH] Fix xxdbx site-relative thumbs; cumlouder answers 404 immediately xxdbx assets are now /id.jpg, not //host/..; prefixing https: produced https:/id.jpg. cumlouder's unknown-tag 404 went through the requester's mirror/FlareSolverr fallbacks (146s); read the raw response and return an empty list at once while still retrying 5xx. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi --- docs/provider-fix-todo.md | 4 +- src/providers/cumlouder.rs | 29 ++++++---- src/providers/xxdbx.rs | 105 ++++++++++++++++++++++++------------- 3 files changed, 92 insertions(+), 46 deletions(-) diff --git a/docs/provider-fix-todo.md b/docs/provider-fix-todo.md index f53bd72..611894c 100644 --- a/docs/provider-fix-todo.md +++ b/docs/provider-fix-todo.md @@ -29,7 +29,7 @@ Status: `[ ]` open, `[~]` in progress, `[x]` fixed and verified, `[-]` not repro | [x] | `fullpornerorg` | `lulustream` proxy 403 / 502 | three separate causes: (1) hoster files deleted ("File is no longer available") but still listed -> now probed and dropped; (2) `myvidplay.com` embeds lost their host (bare ID sent to lulustream, wrong namespace) -> routed to the doodstream proxy with host kept, which now allows myvidplay/playmogo; (3) tnmr.org m3u8 is TLS-fingerprint gated (works with chrome TLS = the channel's `--impersonate chrome-120`; plain yt-dlp 403s are the checker's expected CF warnings). myvidplay playback could not be verified: the embed now returns a Turnstile captcha shell to this client | | [-] | `ripnsfw` | `doodstream` format 502 | lulustream format on the same items resolves (302). The doodstream format cannot be verified here: `*.dood.video` resolves to 127.0.0.1 on this host's resolver, so the CDN is unreachable. Needs the production network | | [-] | `freepornvideosxxx` | yt-dlp 502 on page URL | listing/search/p2 fine (24 items). Page URL is yt-dlp-native (html5 `` 480/720/2160). The 502 is the site's edge answering plain yt-dlp's TLS fingerprint (3 of 4 items 502; 4/4 fine with `--impersonate chrome-120`; curl always 200). The channel declares that command but see "Open decision" below: status never delivers it | -| [ ] | `hdporn92` | m3u8 formats time out (curl + yt-dlp) | morencius.com streams | +| [-] | `hdporn92` | m3u8 formats time out (curl + yt-dlp) | provider works (embed deobfuscated, signed m3u8 returned, yt-dlp resolves the first item). Every morencius.com request (embed page AND playlist) carries a constant ~20.1s server-side delay from this egress (detail page on hdporn92.com is 0.25s), which exceeds check.py's 15s HEAD / 20s yt-dlp socket timeouts. Raising enrichment concurrency 4 -> 10 made it worse (108s, then timeouts), so it was reverted. Likely a host tarpit for this network; verify from the production IP | ## 3. Thumbnails @@ -55,7 +55,7 @@ Status: `[ ]` open, `[~]` in progress, `[x]` fixed and verified, `[-]` not repro | | Channel | Symptom | Notes | |---|---|---|---| | [ ] | `aps` | 30-98s per request; check.py 30s timeout | | -| [ ] | `hdporn92` | feed about 66s per request | shares item with section 2 | +| [-] | `hdporn92` | feed about 66s per request | consequence of the 20s per-embed host delay above (10 embeds per page); not fixable in the provider without moving to play-time resolution, which would put the 20s on every play and every HLS playlist request | | [ ] | `porndish` | 14-23s feed/search | | ## 6. Tooling diff --git a/src/providers/cumlouder.rs b/src/providers/cumlouder.rs index d77cf5a..455ae83 100644 --- a/src/providers/cumlouder.rs +++ b/src/providers/cumlouder.rs @@ -357,22 +357,33 @@ impl CumlouderProvider { let headers = vec![("Referer".to_string(), format!("{BASE_URL}/"))]; // The origin sporadically answers its first uncached request with a - // Cloudflare 522; a single retry succeeds. + // Cloudflare 522; a retry succeeds. Use the raw response so a real + // 404 (unknown tag/category) answers immediately instead of + // escalating through the requester's mirror/FlareSolverr fallbacks, + // which cost minutes. let mut last_err = String::new(); let mut html = None; for _ in 0..3 { match requester - .get_with_headers(&url, headers.clone(), None) + .get_raw_with_headers(&url, headers.clone()) .await { - Ok(body) if body.contains("muestra-escena") => { - html = Some(body); - break; - } - Ok(body) => { + Ok(response) => { + let status = response.status().as_u16(); + let body = response.text().await.unwrap_or_default(); + if status == 404 { + return Ok(vec![]); + } + if body.contains("muestra-escena") { + html = Some(body); + break; + } + if status >= 500 || status == 429 { + last_err = format!("HTTP {status}"); + continue; + } + // A page with no cards is a real empty archive. last_err = format!("no cards in {} bytes", body.len()); - // A page with no cards is a real empty archive (404 body - // included); only keep retrying on transport errors. html = Some(body); break; } diff --git a/src/providers/xxdbx.rs b/src/providers/xxdbx.rs index cbb1dd8..2b60d2b 100644 --- a/src/providers/xxdbx.rs +++ b/src/providers/xxdbx.rs @@ -52,6 +52,23 @@ impl XxdbxProvider { provider } + /// Resolves the card's asset URLs, which the site serves protocol-relative + /// (`//prev.xxdbx.com/..`) or, now, site-relative (`/1234.jpg`). + fn absolute_url(raw: &str) -> String { + let raw = raw.trim(); + if raw.is_empty() { + String::new() + } else if raw.starts_with("http://") || raw.starts_with("https://") { + raw.to_string() + } else if let Some(rest) = raw.strip_prefix("//") { + format!("https://{rest}") + } else if raw.starts_with('/') { + format!("https://xxdbx.com{raw}") + } else { + format!("https://xxdbx.com/{raw}") + } + } + fn build_channel(&self, clientversion: ClientVersion) -> Channel { let _ = clientversion; Channel { @@ -272,26 +289,24 @@ impl XxdbxProvider { .unwrap_or_default() .to_string(); - let thumb = format!( - "https:{}", - video_segment - .split(">() - .get(1) - .copied() - .unwrap_or_default() - .split("src=\"") - .collect::>() - .last() - .copied() - .unwrap_or_default() - .split("\"") - .collect::>() - .get(0) - .copied() - .unwrap_or_default() - .to_string() - ); + let thumb_raw = video_segment + .split(">() + .get(1) + .copied() + .unwrap_or_default() + .split("src=\"") + .collect::>() + .last() + .copied() + .unwrap_or_default() + .split("\"") + .collect::>() + .get(0) + .copied() + .unwrap_or_default() + .to_string(); + let thumb = Self::absolute_url(&thumb_raw); let raw_duration = video_segment .split("
") .collect::>() @@ -306,21 +321,19 @@ impl XxdbxProvider { .to_string(); let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32; - let preview = format!( - "https:{}", - video_segment - .split("data-preview=\"") - .collect::>() - .get(1) - .copied() - .unwrap_or_default() - .split("\"") - .collect::>() - .get(0) - .copied() - .unwrap_or_default() - .to_string() - ); + let preview_raw = video_segment + .split("data-preview=\"") + .collect::>() + .get(1) + .copied() + .unwrap_or_default() + .split("\"") + .collect::>() + .get(0) + .copied() + .unwrap_or_default() + .to_string(); + let preview = Self::absolute_url(&preview_raw); let tags = video_segment .split("
") .collect::>() @@ -439,3 +452,25 @@ impl Provider for XxdbxProvider { Some(self.build_channel(clientversion)) } } + +#[cfg(test)] +mod tests { + use super::XxdbxProvider; + + #[test] + fn resolves_site_relative_protocol_relative_and_absolute_assets() { + assert_eq!( + XxdbxProvider::absolute_url("/7004779764736.jpg"), + "https://xxdbx.com/7004779764736.jpg" + ); + assert_eq!( + XxdbxProvider::absolute_url("//prev.xxdbx.com/700477973230.mp4"), + "https://prev.xxdbx.com/700477973230.mp4" + ); + assert_eq!( + XxdbxProvider::absolute_url("https://cdn.example/a.jpg"), + "https://cdn.example/a.jpg" + ); + assert_eq!(XxdbxProvider::absolute_url(""), ""); + } +}