From e34801f6f8d25a5cdf6d1ee4d71e253ea1453359 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 17 Oct 2026 05:53:43 +0000 Subject: [PATCH] thepornbunny: resolve pornstar/studio/tag names; get_probe treats redirect-away as a miss --- src/providers/thepornbunny.rs | 30 ++++++++++++++++++++++++++++-- src/util/requester.rs | 15 +++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/providers/thepornbunny.rs b/src/providers/thepornbunny.rs index 397921c..6b03a07 100644 --- a/src/providers/thepornbunny.rs +++ b/src/providers/thepornbunny.rs @@ -1,7 +1,8 @@ use crate::DbPool; use crate::api::ClientVersion; use crate::providers::{ - Provider, build_proxy_url, report_provider_error, requester_or_default, + Provider, build_proxy_url, probe_first_archive, report_provider_error, requester_or_default, + slug_dashed, }; use crate::status::*; use crate::util::cache::VideoCache; @@ -367,6 +368,26 @@ impl ThepornbunnyProvider { } } + /// A free-form query that names a pornstar, studio or tag goes to that + /// archive (probed on the site; unknown names redirect home), else keyword + /// search. + async fn resolve_keyword(query: &str, options: &ServerOptions) -> Option { + let slug = slug_dashed(query); + if slug.is_empty() { + return None; + } + let urls: Vec = ["pornstar", "studio", "tag"] + .iter() + .map(|kind| format!("{BASE_URL}/{kind}/{slug}/")) + .collect(); + let mut requester = requester_or_default(options, CHANNEL_ID, "resolve_keyword"); + let (index, _) = probe_first_archive(&mut requester, &urls, &format!("{BASE_URL}/"), |body| { + body.contains("th-description") + }) + .await?; + Some(Target::Archive(urls[index].trim_end_matches('/').to_string())) + } + fn resolve_option_target(options: &ServerOptions, sort: &str) -> Target { if let Some(cat) = options.categories.as_deref() { if cat.starts_with(BASE_URL) && cat != "all" { @@ -476,10 +497,15 @@ impl Provider for ThepornbunnyProvider { let page = page.parse::().unwrap_or(1).max(1); let per_page_limit = per_page.parse::().unwrap_or(10).clamp(1, 60); - let target = match query { + let mut target = match query { Some(ref q) if !q.trim().is_empty() => Self::resolve_query_target(q.trim()), _ => Self::resolve_option_target(&options, &sort), }; + if let Target::Search(query) = &target { + if let Some(found) = Self::resolve_keyword(query, &options).await { + target = found; + } + } match self .fetch_target(cache, target, page, per_page_limit, options) diff --git a/src/util/requester.rs b/src/util/requester.rs index 48699d1..e772493 100644 --- a/src/util/requester.rs +++ b/src/util/requester.rs @@ -661,6 +661,10 @@ impl Requester { Ok(response) } + fn path_of(url: &str) -> Option { + url::Url::parse(url).ok().map(|u| u.path().to_string()) + } + /// Existence probe for archive pages (pornstar / studio / tag / category). /// /// Reads the raw response so a real 404/410 answers immediately with @@ -678,6 +682,14 @@ impl Requester { match self.get_raw_with_headers(url, headers.clone()).await { Ok(response) => { let status = response.status().as_u16(); + // An unknown archive often redirects to the home page or a + // search page instead of answering 404: a redirect that + // lands on a different path is a miss. + let redirected_away = Self::path_of(url) + .zip(Some(response.uri().path().to_string())) + .is_some_and(|(asked, landed)| { + !asked.trim_end_matches('/').eq_ignore_ascii_case(landed.trim_end_matches('/')) + }); let body = response.text().await.unwrap_or_default(); if status == 404 || status == 410 { return Ok(None); @@ -688,6 +700,9 @@ impl Requester { { return self.get_with_headers(url, headers, None).await.map(Some); } + if redirected_away { + return Ok(None); + } if status >= 500 || status == 429 { last_err = format!("HTTP {status}"); continue;