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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user