use ntex::web; use regex::Regex; use std::collections::HashMap; use url::Url; use crate::util::requester::Requester; #[derive(Debug, Clone)] pub struct HqpornerProxy {} impl HqpornerProxy { pub fn new() -> Self { Self {} } fn normalize_detail_request(endpoint: &str) -> Option<(String, Option)> { let endpoint = endpoint.trim().trim_start_matches('/'); if endpoint.is_empty() { return None; } let (detail_part, quality) = match endpoint.split_once("/__quality__/") { Some((detail, quality)) => { let requested = quality .trim() .trim_end_matches('/') .trim_end_matches('p') .parse::() .ok(); (detail, requested) } None => (endpoint, None), }; let detail_url = if detail_part.starts_with("http://") || detail_part.starts_with("https://") { detail_part.to_string() } else { format!("https://{}", detail_part.trim_start_matches('/')) }; Self::is_allowed_detail_url(&detail_url).then_some((detail_url, quality)) } fn is_allowed_detail_url(url: &str) -> bool { let Some(url) = Url::parse(url).ok() else { return false; }; if url.scheme() != "https" { return false; } let Some(host) = url.host_str() else { return false; }; (host == "hqporner.com" || host == "www.hqporner.com") && url.path().starts_with("/hdporn/") } fn normalize_url(raw: &str) -> String { let value = raw.trim(); if value.is_empty() { return String::new(); } if value.starts_with("//") { return format!("https:{value}"); } if value.starts_with('/') { return format!("https://www.hqporner.com{value}"); } if value.starts_with("http://") { return value.replacen("http://", "https://", 1); } value.to_string() } fn regex(value: &str) -> Option { Regex::new(value).ok() } fn extract_player_url(detail_html: &str) -> Option { let pattern = r#"(?is)url\s*:\s*['"](/blocks/(?:altplayer|nativeplayer)\.php\?i=[^'"]+)['"]"#; let captures = Self::regex(pattern)?.captures(detail_html)?; let path = captures.get(1)?.as_str(); Some(Self::normalize_url(path)) } fn extract_source_url(player_html: &str) -> Option { for source in player_html.split("]+src="([^"]+)""#, r#"(?is)]+src='([^']+)'"#, r#"(?is)src=\\\"([^\\"]+)\\\""#, r#"(?is)src=\\'([^\\']+)\\'"#, ]; for pattern in iframe_regexes { let Some(regex) = Self::regex(pattern) else { continue; }; if let Some(url) = regex .captures(player_html) .and_then(|caps| caps.get(1)) .map(|m| Self::normalize_url(m.as_str())) .filter(|value| !value.is_empty()) { return Some(url); } } let source_regex = Self::regex(r#"src=\\\"([^\\"]+)\\\""#)?; source_regex .captures(player_html) .and_then(|caps| caps.get(1)) .map(|m| Self::normalize_url(m.as_str())) .filter(|value| !value.is_empty()) } fn extract_quality_urls(video_page_html: &str) -> HashMap { let mut urls = HashMap::new(); let Some(regex) = Self::regex(r#"(?i)(?:https?:)?//[^"'\\\s]+/pubs/[A-Za-z0-9._-]+/(360|720|1080)\.mp4"#) else { return urls; }; for captures in regex.captures_iter(video_page_html) { let Some(full_match) = captures.get(0) else { continue; }; let Some(quality_match) = captures.get(1) else { continue; }; let Some(quality) = quality_match.as_str().parse::().ok() else { continue; }; let normalized = Self::normalize_url(full_match.as_str()); if !normalized.is_empty() { urls.insert(quality, normalized); } } urls } fn select_quality_url(quality_urls: &HashMap, requested: Option) -> Option { let fallbacks = match requested.unwrap_or(1080) { 1080 => [1080u16, 720, 360].as_slice(), 720 => [720u16, 360].as_slice(), 360 => [360u16].as_slice(), other if other > 1080 => [1080u16, 720, 360].as_slice(), other if other > 720 => [720u16, 360].as_slice(), _ => [360u16].as_slice(), }; for quality in fallbacks { if let Some(url) = quality_urls.get(quality) { return Some(url.clone()); } } if let Some(url) = quality_urls.get(&1080) { return Some(url.clone()); } if let Some(url) = quality_urls.get(&720) { return Some(url.clone()); } quality_urls.get(&360).cloned() } } impl crate::proxies::Proxy for HqpornerProxy { async fn get_video_url(&self, url: String, requester: web::types::State) -> String { let Some((detail_url, requested_quality)) = Self::normalize_detail_request(&url) else { return String::new(); }; let mut requester = requester.get_ref().clone(); let headers = vec![("Referer".to_string(), "https://hqporner.com/".to_string())]; let detail_html = requester .get_with_headers(&detail_url, headers.clone(), None) .await .unwrap_or_default(); if detail_html.is_empty() { return String::new(); } let mut source_page_url = String::new(); if let Some(player_url) = Self::extract_player_url(&detail_html) { let player_html = requester .get_with_headers(&player_url, headers.clone(), None) .await .unwrap_or_default(); if !player_html.is_empty() { if let Some(url) = Self::extract_source_url(&player_html) { source_page_url = url; } } } if source_page_url.is_empty() { source_page_url = Self::extract_source_url(&detail_html).unwrap_or_default(); } if source_page_url.is_empty() { return String::new(); } let source_page_html = requester .get_with_headers(&source_page_url, headers, None) .await .unwrap_or_default(); if source_page_html.is_empty() { return String::new(); } let quality_urls = Self::extract_quality_urls(&source_page_html); if quality_urls.is_empty() { return String::new(); } Self::select_quality_url(&quality_urls, requested_quality).unwrap_or_default() } } #[cfg(test)] mod tests { use super::HqpornerProxy; use std::collections::HashMap; #[test] fn extract_source_url_supports_iframe_src() { let html = r#""#; let extracted = HqpornerProxy::extract_source_url(html); assert_eq!( extracted.as_deref(), Some("https://mydaddy.cc/video/f7cbb41e218d3b1dca/&alt") ); } #[test] fn extract_source_url_supports_source_tag_src() { let html = r#""#; let extracted = HqpornerProxy::extract_source_url(html); assert_eq!( extracted.as_deref(), Some("https://cdn.example.com/video.mp4") ); } #[test] fn extract_player_url_supports_altplayer_path() { let html = r#" "#; let extracted = HqpornerProxy::extract_player_url(html); assert_eq!( extracted.as_deref(), Some( "https://www.hqporner.com/blocks/altplayer.php?i=//mydaddy.cc/video/f7cbb41e218d3b1dca/" ) ); } #[test] fn extract_quality_urls_from_mydaddy_html() { let html = r#" timelinePreview:{file:"//s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/tile.vtt",spriteRelativePath:true,type:"VTT"} "#; let urls = HqpornerProxy::extract_quality_urls(html); assert_eq!( urls.get(&360).map(String::as_str), Some("https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/360.mp4") ); assert_eq!( urls.get(&720).map(String::as_str), Some("https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/720.mp4") ); assert_eq!( urls.get(&1080).map(String::as_str), Some("https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/1080.mp4") ); } #[test] fn select_quality_url_falls_back_to_next_lower_quality() { let mut urls = HashMap::new(); urls.insert( 360, "https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/360.mp4".to_string(), ); urls.insert( 720, "https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/720.mp4".to_string(), ); let requested_1080 = HqpornerProxy::select_quality_url(&urls, Some(1080)); assert_eq!( requested_1080.as_deref(), Some("https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/720.mp4") ); let requested_720 = HqpornerProxy::select_quality_url(&urls, Some(720)); assert_eq!( requested_720.as_deref(), Some("https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/720.mp4") ); let requested_360 = HqpornerProxy::select_quality_url(&urls, Some(360)); assert_eq!( requested_360.as_deref(), Some("https://s43.bigcdn.cc/pubs/69ecfb39b17117.73515587/360.mp4") ); } }