From cc66f045cd43777a5a8745865c51d0e8122800db Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 25 Apr 2026 18:22:05 +0000 Subject: [PATCH] hqporner fix --- src/providers/hqporner.rs | 35 +++++- src/proxies/hqporner.rs | 256 ++++++++++++++++++++++++++++++++++---- 2 files changed, 264 insertions(+), 27 deletions(-) diff --git a/src/providers/hqporner.rs b/src/providers/hqporner.rs index a2e985d..bba506d 100644 --- a/src/providers/hqporner.rs +++ b/src/providers/hqporner.rs @@ -6,7 +6,7 @@ use crate::util::cache::VideoCache; use crate::util::discord::{format_error_chain, send_discord_error_report}; use crate::util::requester::Requester; use crate::util::time::parse_time_to_seconds; -use crate::videos::{ServerOptions, VideoItem}; +use crate::videos::{ServerOptions, VideoFormat, VideoItem}; use async_trait::async_trait; use error_chain::error_chain; use htmlentity::entity::{ICodedDataTrait, decode}; @@ -323,12 +323,40 @@ impl HqpornerProvider { .unwrap_or_default(); let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; + let stripped_detail_url = crate::providers::strip_url_scheme(&detail_url); let proxied_url = crate::providers::build_proxy_url( options, "hqporner", - &crate::providers::strip_url_scheme(&detail_url), + &stripped_detail_url, ); + let quality_target = |quality: &str| -> String { + format!("{stripped_detail_url}/__quality__/{quality}") + }; + let formats = vec![ + VideoFormat::new( + crate::providers::build_proxy_url(options, "hqporner", &quality_target("1080")), + "1080p".to_string(), + "mp4".to_string(), + ) + .format_id("1080p".to_string()) + .format_note("1080p Full HD".to_string()), + VideoFormat::new( + crate::providers::build_proxy_url(options, "hqporner", &quality_target("720")), + "720p".to_string(), + "mp4".to_string(), + ) + .format_id("720p".to_string()) + .format_note("720p HD".to_string()), + VideoFormat::new( + crate::providers::build_proxy_url(options, "hqporner", &quality_target("360")), + "360p".to_string(), + "mp4".to_string(), + ) + .format_id("360p".to_string()) + .format_note("360p".to_string()), + ]; + Ok(VideoItem::new( id, title, @@ -336,7 +364,8 @@ impl HqpornerProvider { "hqporner".into(), thumb, duration, - )) + ) + .formats(formats)) } } diff --git a/src/proxies/hqporner.rs b/src/proxies/hqporner.rs index fcb649f..d84e026 100644 --- a/src/proxies/hqporner.rs +++ b/src/proxies/hqporner.rs @@ -1,5 +1,6 @@ use ntex::web; use regex::Regex; +use std::collections::HashMap; use url::Url; use crate::util::requester::Requester; @@ -12,19 +13,33 @@ impl HqpornerProxy { Self {} } - fn normalize_detail_url(endpoint: &str) -> Option { + fn normalize_detail_request(endpoint: &str) -> Option<(String, Option)> { let endpoint = endpoint.trim().trim_start_matches('/'); if endpoint.is_empty() { return None; } - let detail_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { - endpoint.to_string() - } else { - format!("https://{}", endpoint.trim_start_matches('/')) + 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), }; - Self::is_allowed_detail_url(&detail_url).then_some(detail_url) + 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 { @@ -62,13 +77,10 @@ impl HqpornerProxy { } fn extract_player_url(detail_html: &str) -> Option { - let path = detail_html - .split("url: '/blocks/altplayer.php?i=") - .nth(1) - .and_then(|s| s.split('\'').next())?; - Some(Self::normalize_url(&format!( - "/blocks/altplayer.php?i={path}" - ))) + 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 { @@ -90,6 +102,26 @@ impl HqpornerProxy { } } + let iframe_regexes = [ + r#"(?is)]+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) @@ -97,11 +129,63 @@ impl HqpornerProxy { .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) = Self::normalize_detail_url(&url) else { + let Some((detail_url, requested_quality)) = Self::normalize_detail_request(&url) else { return String::new(); }; @@ -116,18 +200,142 @@ impl crate::proxies::Proxy for HqpornerProxy { return String::new(); } - let Some(player_url) = Self::extract_player_url(&detail_html) else { - return String::new(); - }; - - let player_html = requester - .get_with_headers(&player_url, headers, None) - .await - .unwrap_or_default(); - if player_html.is_empty() { + 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(); } - Self::extract_source_url(&player_html).unwrap_or_default() + 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") + ); } }