Route Cloudflare challenges to FlareSolverr on cumlouder and hypnotube raw paths

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
Simon
2026-10-10 11:59:42 +00:00
parent 2addcc2e7c
commit af6674b4ed
4 changed files with 83 additions and 14 deletions

View File

@@ -84,3 +84,10 @@ impersonation target; if a minimum client version is needed, gate on `ClientVers
`porndish`, `supjav`, `jable`, `fapello`, `missav`, `wowxxx`, `hentaihaven` return Cloudflare 403 to
yt-dlp/curl from this machine while the server itself returns items. `hsex` returned 429 (rate limit).
## Cloudflare challenge rule
Any request path that bypasses the requester's fallback chain must still hand a Cloudflare challenge (403/503 or
"Just a moment...") to FlareSolverr. `cumlouder` (raw 404-aware listing fetch) now falls back to
`get_with_headers`; `hypnotube` (isolated search session) solves via the shared requester and replays the solved
cookies + user agent in a fresh isolated session.

View File

@@ -3,6 +3,7 @@ use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::requester::Requester;
use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoItem};
use async_trait::async_trait;
@@ -370,10 +371,30 @@ impl CumlouderProvider {
{
Ok(response) => {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
let mut body = response.text().await.unwrap_or_default();
if status == 404 {
return Ok(vec![]);
}
// Cloudflare challenge: hand over to the requester's full
// chain (FlareSolverr solve + identity replay).
if status == 403
|| status == 503
|| Requester::looks_like_cloudflare_challenge(&body)
{
match requester.get_with_headers(&url, headers.clone(), None).await {
Ok(solved) => {
body = solved;
if body.contains("muestra-escena") {
html = Some(body);
break;
}
}
Err(e) => {
last_err = e.to_string();
continue;
}
}
}
if body.contains("muestra-escena") {
html = Some(body);
break;

View File

@@ -258,33 +258,74 @@ impl HypnotubeProvider {
/// isolated cookie session instead of the shared requester jar. Page 1 is
/// the redirect target of the POST; page N>1 is fetched directly.
async fn fetch_search_html(&self, query: &str, page: u8) -> Result<String> {
let (status, body) = self.search_once(query, page, None).await?;
if !(status == 403 || status == 503 || Requester::looks_like_cloudflare_challenge(&body)) {
return Ok(body);
}
// Cloudflare challenged the isolated session: solve it through the
// shared requester (FlareSolverr fallback), then replay the solved
// cookies and user agent in a fresh isolated session.
let mut requester = Requester::new();
let home = format!("{}/", self.url);
let _ = requester.get(&home, Some(Version::HTTP_11)).await;
let identity = (
requester.cookie_header_for_url(&home),
Requester::remembered_flaresolverr_user_agent(&home),
);
let (_, body) = self.search_once(query, page, Some(identity)).await?;
Ok(body)
}
async fn search_once(
&self,
query: &str,
page: u8,
identity: Option<(Option<String>, Option<String>)>,
) -> Result<(u16, String)> {
let jar = Arc::new(Jar::default());
let mut user_agent = None;
if let Some((cookies, ua)) = identity {
if let Some(cookies) = cookies {
for pair in cookies.split(';').map(str::trim).filter(|p| !p.is_empty()) {
jar.add(pair, self.url.as_str());
}
}
user_agent = ua;
}
let client = Client::builder()
.tls_cert_verification(false)
.emulation(Emulation::Firefox136)
.cookie_provider(Arc::new(Jar::default()))
.cookie_provider(jar)
.redirect(if page <= 1 { Policy::default() } else { Policy::none() })
.build()?;
let body = format!("q={}&type=videos", query.trim().replace(' ', "+"));
let gate = client
let mut gate = client
.post(format!("{}/searchgate.php", self.url))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("{}/", self.url))
.body(body)
.send()
.await?;
.body(body);
if let Some(ua) = &user_agent {
gate = gate.header("User-Agent", ua.as_str());
}
let gate = gate.send().await?;
if page <= 1 {
return Ok(gate.text().await?);
let status = gate.status().as_u16();
return Ok((status, gate.text().await?));
}
let slug = query.trim().replace(' ', "%20");
let page_url = format!("{}/search/videos/{}/page{}.html", self.url, slug, page);
let response = client
let mut request = client
.get(page_url)
.header("Referer", format!("{}/search/videos/{}/", self.url, slug))
.send()
.await?;
Ok(response.text().await?)
.header("Referer", format!("{}/search/videos/{}/", self.url, slug));
if let Some(ua) = &user_agent {
request = request.header("User-Agent", ua.as_str());
}
let response = request.send().await?;
let status = response.status().as_u16();
Ok((status, response.text().await?))
}
async fn query(

View File

@@ -87,7 +87,7 @@ impl Requester {
}
}
fn remembered_flaresolverr_user_agent(url: &str) -> Option<String> {
pub(crate) fn remembered_flaresolverr_user_agent(url: &str) -> Option<String> {
let host = Self::host_key(url)?;
Self::flaresolverr_user_agents()
.read()
@@ -218,7 +218,7 @@ impl Requester {
.unwrap_or_else(|| "none".to_string())
}
fn looks_like_cloudflare_challenge(body: &str) -> bool {
pub(crate) fn looks_like_cloudflare_challenge(body: &str) -> bool {
body.contains("Just a moment...")
|| body.contains("cf-turnstile-response")
|| body.contains("Enable JavaScript and cookies to continue")