From 26ef830f6e9a3022879efde958ff70df13c5d9d0 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 17 Oct 2026 05:56:31 +0000 Subject: [PATCH] pornhub: probe pornstar/model/channel for unknown free-form queries --- src/providers/pornhub.rs | 50 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/providers/pornhub.rs b/src/providers/pornhub.rs index 2f93ca1..811d28c 100644 --- a/src/providers/pornhub.rs +++ b/src/providers/pornhub.rs @@ -1,6 +1,9 @@ use crate::DbPool; use crate::api::ClientVersion; -use crate::providers::{Provider, proxy_aylo_thumb, report_provider_error, requester_or_default}; +use crate::providers::{ + Provider, probe_first_archive, proxy_aylo_thumb, report_provider_error, requester_or_default, + slug_dashed, +}; use crate::status::*; use crate::util::cache::VideoCache; use crate::util::parse_abbreviated_number; @@ -45,6 +48,8 @@ error_chain! { pub struct PornhubProvider { url: String, tag_map: Arc>>, + /// Queries already probed on the site that are neither a pornstar, model nor channel. + probe_misses: Arc>>, } #[derive(Debug, Clone, Copy)] @@ -91,6 +96,7 @@ impl PornhubProvider { let provider = Self { url: BASE_URL.to_string(), tag_map: Arc::new(RwLock::new(HashMap::new())), + probe_misses: Arc::new(RwLock::new(std::collections::HashSet::new())), }; provider.spawn_initial_load(); provider @@ -588,6 +594,43 @@ impl PornhubProvider { values.push(normalized.to_string()); } + /// A free-form query that is not in the loaded catalogs may still name a + /// pornstar, model or channel. Probe the site (unknown names 404 or + /// redirect away) and remember the hit in `tag_map` so later pages and + /// queries resolve without another request. + async fn learn_query_target(&self, query: &str, options: &ServerOptions) { + let key = query.trim().to_ascii_lowercase(); + let slug = slug_dashed(query); + if slug.is_empty() || self.probe_misses.read().unwrap().contains(&key) { + return; + } + let kinds = [ + QueryTargetKind::Pornstar, + QueryTargetKind::Model, + QueryTargetKind::Channel, + ]; + let urls: Vec = kinds + .iter() + .map(|kind| format!("{}/{}/{slug}/videos", self.url, kind.path_segment())) + .collect(); + let mut requester = requester_or_default(options, CHANNEL_ID, "learn_query_target"); + let hit = probe_first_archive(&mut requester, &urls, &format!("{}/", self.url), |body| { + body.contains("pcVideoListItem") + }) + .await; + match hit { + Some((index, _)) => { + self.tag_map.write().unwrap().insert( + key, + TagInfo { kind: kinds[index], slug, title: query.trim().to_string() }, + ); + } + None => { + self.probe_misses.write().unwrap().insert(key); + } + } + } + async fn fetch_listing( &self, cache: VideoCache, @@ -596,6 +639,11 @@ impl PornhubProvider { query: Option<&str>, options: ServerOptions, ) -> Result> { + if let Some(q) = query.map(str::trim).filter(|q| !q.is_empty()) { + if self.parse_query_target(q).is_none() { + self.learn_query_target(q, &options).await; + } + } if query.is_some() && self.tag_map.read().unwrap().is_empty() { let _ = Self::load_tags(&self.url, Arc::clone(&self.tag_map)).await; }