hooporn: probe queries as pornstar/studio archives before keyword search

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-17 05:25:30 +00:00
parent 99bc80adac
commit 665fb2a0ce
2 changed files with 81 additions and 13 deletions

View File

@@ -111,6 +111,9 @@ enum Target {
Pornstar(String),
Studio(String),
Search(String),
/// Free-form query: a pornstar page or a studio page if one exists by that
/// name (probed on the site), else a keyword search.
Keyword(String),
}
/// Detail-page data: the player iframe plus taxonomy links.
@@ -239,7 +242,7 @@ impl HoopornProvider {
if let Some(slug) = Self::category_slug(q) {
return Target::Category(slug.to_string());
}
return Target::Search(q.to_string());
return Target::Keyword(q.to_string());
}
if let Some(cat) = options.categories.as_deref() {
@@ -260,7 +263,7 @@ impl HoopornProvider {
Target::Category(slug) => format!("{BASE_URL}/category/{slug}/{page}"),
Target::Pornstar(slug) => format!("{BASE_URL}/pornstar/{slug}/{page}"),
Target::Studio(slug) => format!("{BASE_URL}/studio/{slug}/{page}"),
Target::Search(query) => format!(
Target::Search(query) | Target::Keyword(query) => format!(
"{BASE_URL}/search?q={}&p={page}",
utf8_percent_encode(query, NON_ALPHANUMERIC)
),
@@ -427,17 +430,82 @@ impl HoopornProvider {
.await
}
/// Fetches a listing page. A real 404 (unknown archive, page past the end)
/// is `None` and answers immediately; a Cloudflare challenge is handed to
/// the requester's full chain (FlareSolverr).
async fn fetch_html(requester: &mut Requester, url: &str) -> Result<Option<String>> {
let headers = vec![("Referer".to_string(), format!("{BASE_URL}/"))];
let mut last_err = String::new();
for _ in 0..2 {
match requester.get_raw_with_headers(url, headers.clone()).await {
Ok(response) => {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
if status == 404 {
return Ok(None);
}
if status == 403
|| status == 503
|| Requester::looks_like_cloudflare_challenge(&body)
{
return requester
.get_with_headers(url, headers, None)
.await
.map(Some)
.map_err(|e| Error::from(format!("fetch failed for {url}: {e}")));
}
if status >= 500 || status == 429 {
last_err = format!("HTTP {status}");
continue;
}
return Ok(Some(body));
}
Err(e) => last_err = e.to_string(),
}
}
Err(Error::from(format!("fetch failed for {url}: {last_err}")))
}
async fn fetch(target: &Target, page: u32, options: &ServerOptions) -> Result<Vec<VideoItem>> {
let url = Self::build_listing_url(target, page);
let mut requester = requester_or_default(options, CHANNEL_ID, "fetch");
let html = requester
.get_with_headers(
&url,
vec![("Referer".to_string(), format!("{BASE_URL}/"))],
None,
)
.await
.map_err(|e| Error::from(format!("fetch failed for {url}: {e}")))?;
let html = match target {
Target::Keyword(query) => {
// Probe page 1 of the pornstar and studio archive for this
// name; the first that exists wins, otherwise keyword search.
let candidates = [
Target::Pornstar(Self::slugify(query)),
Target::Studio(Self::slugify(query).replace('-', "")),
];
let mut found = None;
for candidate in candidates {
let slug_ok = match &candidate {
Target::Pornstar(s) | Target::Studio(s) => !s.is_empty(),
_ => false,
};
if !slug_ok {
continue;
}
let first = Self::fetch_html(&mut requester, &Self::build_listing_url(&candidate, 1)).await?;
if let Some(body) = first.filter(|b| b.contains("class=\"video-card\"")) {
found = Some(if page <= 1 {
Some(body)
} else {
Self::fetch_html(&mut requester, &Self::build_listing_url(&candidate, page)).await?
});
break;
}
}
match found {
Some(html) => html,
None => Self::fetch_html(&mut requester, &Self::build_listing_url(target, page)).await?,
}
}
_ => Self::fetch_html(&mut requester, &Self::build_listing_url(target, page)).await?,
};
let Some(html) = html else {
return Ok(vec![]);
};
let items = Self::parse_listing(&html)?;
Ok(Self::enrich(items, requester).await)
}
@@ -529,7 +597,7 @@ mod tests {
);
assert_eq!(
HoopornProvider::pick_target(Some("golf"), &o),
Target::Search("golf".into())
Target::Keyword("golf".into())
);
assert_eq!(HoopornProvider::pick_target(None, &o), Target::Latest);
}