From a977381b3bb04fe4ed5f8592cdad11a482447e29 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Mar 2026 00:57:50 +0000 Subject: [PATCH] porndish fix --- src/providers/porndish.rs | 129 ++++++++++++- src/proxies/mod.rs | 4 + src/proxies/porndish.rs | 369 ++++++++++++++++++++++++++++++++++++++ src/proxy.rs | 7 + 4 files changed, 500 insertions(+), 9 deletions(-) create mode 100644 src/proxies/porndish.rs diff --git a/src/providers/porndish.rs b/src/providers/porndish.rs index 51d6d29..decddbd 100644 --- a/src/providers/porndish.rs +++ b/src/providers/porndish.rs @@ -231,6 +231,10 @@ impl PorndishProvider { matches!(host, "myvidplay.com" | "www.myvidplay.com") } + fn is_vidara_host(host: &str) -> bool { + matches!(host, "vidara.so" | "www.vidara.so") + } + fn is_allowed_list_url(url: &str) -> bool { let Some(url) = Self::parse_url(url) else { return false; @@ -294,6 +298,35 @@ impl PorndishProvider { Self::is_myvidplay_host(host) && url.path().starts_with("/pass_md5/") } + fn is_allowed_vidara_iframe_url(url: &str) -> bool { + let Some(url) = Self::parse_url(url) else { + return false; + }; + if url.scheme() != "https" { + return false; + } + let Some(host) = url.host_str() else { + return false; + }; + Self::is_vidara_host(host) && url.path().starts_with("/e/") + } + + fn vidara_api_url(iframe_url: &str) -> Option { + let url = Self::parse_url(iframe_url)?; + if !Self::is_allowed_vidara_iframe_url(iframe_url) { + return None; + } + let filecode = url + .path_segments()? + .filter(|segment| !segment.is_empty()) + .next_back()? + .to_string(); + if filecode.is_empty() { + return None; + } + Some(format!("https://vidara.so/api/stream?filecode={filecode}")) + } + fn proxied_thumb(&self, options: &ServerOptions, thumb: &str) -> String { if thumb.is_empty() { return String::new(); @@ -304,6 +337,13 @@ impl PorndishProvider { build_proxy_url(options, "porndish-thumb", &strip_url_scheme(thumb)) } + fn proxied_video(&self, options: &ServerOptions, page_url: &str) -> String { + if page_url.is_empty() || !Self::is_allowed_detail_url(page_url) { + return String::new(); + } + build_proxy_url(options, "porndish", &strip_url_scheme(page_url)) + } + fn push_unique(target: &Arc>>, item: FilterOption) { if let Ok(mut values) = target.write() { if !values.iter().any(|value| value.id == item.id) { @@ -805,6 +845,27 @@ sys.stdout.buffer.write(response.content) Ok(format!("{base}{suffix}?token={token}&expiry={now}")) } + async fn resolve_vidara_stream(&self, iframe_url: &str) -> Result { + let api_url = Self::vidara_api_url(iframe_url) + .ok_or_else(|| Error::from(format!("blocked vidara iframe url: {iframe_url}")))?; + + let response = Self::fetch_html(&api_url, Some(iframe_url)).await?; + let json: serde_json::Value = serde_json::from_str(&response) + .map_err(|error| Error::from(format!("vidara json parse failed: {error}")))?; + let stream_url = json + .get("streaming_url") + .and_then(|value| value.as_str()) + .unwrap_or("") + .trim() + .to_string(); + + if stream_url.is_empty() || !(stream_url.starts_with("https://") || stream_url.starts_with("http://")) { + return Err(Error::from("vidara stream missing streaming_url".to_string())); + } + + Ok(stream_url) + } + fn parse_embed_source(fragment: &str) -> Result> { let iframe_regex = Self::regex(r#"(?is)]+src="([^"]+)"[^>]*>"#)?; Ok(iframe_regex.captures(fragment).and_then(|captures| { @@ -947,25 +1008,71 @@ sys.stdout.buffer.write(response.content) item.tags = Some(tags); } + let mut fallback_embed: Option<(String, String)> = None; + let mut selected_embed: Option<(String, String)> = None; + for fragment in self.extract_iframe_fragments(html)? { let Some((embed_html, iframe_url)) = Self::parse_embed_source(&fragment)? else { continue; }; let iframe_url = self.normalize_url(&iframe_url); + if Self::is_allowed_vidara_iframe_url(&iframe_url) { + selected_embed = Some((embed_html, iframe_url)); + break; + } + + if fallback_embed.is_none() + && (Self::is_allowed_myvidplay_iframe_url(&iframe_url) + || iframe_url.starts_with("https://")) + { + fallback_embed = Some((embed_html, iframe_url)); + } + } + + if let Some((embed_html, iframe_url)) = selected_embed.or(fallback_embed) { item.embed = Some(VideoEmbed { html: embed_html, source: iframe_url.clone(), }); - if iframe_url.contains("myvidplay.com") { + let proxy_url = self.proxied_video(options, page_url); + if Self::is_allowed_vidara_iframe_url(&iframe_url) { + match self.resolve_vidara_stream(&iframe_url).await { + Ok(_stream_url) => { + if !proxy_url.is_empty() { + item.url = proxy_url.clone(); + item.formats = Some(vec![VideoFormat::new( + proxy_url, + "sd".to_string(), + "m3u8".to_string(), + )]); + } else { + item.url = page_url.to_string(); + } + } + Err(error) => { + report_provider_error_background( + "porndish", + "resolve_vidara_stream", + &format!("iframe_url={iframe_url}; error={error}"), + ); + item.url = page_url.to_string(); + } + } + } else if Self::is_allowed_myvidplay_iframe_url(&iframe_url) { match self.resolve_myvidplay_stream(&iframe_url).await { - Ok(stream_url) => { - item.url = stream_url.clone(); - let mut format = - VideoFormat::new(stream_url.clone(), "sd".to_string(), "mp4".to_string()); - format.add_http_header("Referer".to_string(), iframe_url.clone()); - item.formats = Some(vec![format]); + Ok(_stream_url) => { + if !proxy_url.is_empty() { + item.url = proxy_url.clone(); + item.formats = Some(vec![VideoFormat::new( + proxy_url, + "sd".to_string(), + "mp4".to_string(), + )]); + } else { + item.url = page_url.to_string(); + } } Err(error) => { report_provider_error_background( @@ -979,8 +1086,6 @@ sys.stdout.buffer.write(response.content) } else { item.url = iframe_url; } - - break; } if item.formats.is_none() && item.url != page_url { @@ -1286,6 +1391,9 @@ mod tests { assert!(PorndishProvider::is_allowed_myvidplay_pass_url( "https://myvidplay.com/pass_md5/abc/def" )); + assert!(PorndishProvider::is_allowed_vidara_iframe_url( + "https://vidara.so/e/abc123" + )); assert!(!PorndishProvider::is_allowed_list_url( "https://169.254.169.254/latest/meta-data/" @@ -1299,5 +1407,8 @@ mod tests { assert!(!PorndishProvider::is_allowed_myvidplay_pass_url( "https://example.com/pass_md5/abc/def" )); + assert!(!PorndishProvider::is_allowed_vidara_iframe_url( + "https://example.com/e/abc123" + )); } } diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs index c2af138..4a5cd8a 100644 --- a/src/proxies/mod.rs +++ b/src/proxies/mod.rs @@ -1,5 +1,6 @@ use ntex::web; +use crate::proxies::porndish::PorndishProxy; use crate::proxies::spankbang::SpankbangProxy; use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester}; @@ -7,6 +8,7 @@ pub mod hanimecdn; pub mod hqpornerthumb; pub mod javtiful; pub mod noodlemagazine; +pub mod porndish; pub mod porndishthumb; pub mod spankbang; pub mod sxyprn; @@ -15,6 +17,7 @@ pub mod sxyprn; pub enum AnyProxy { Sxyprn(SxyprnProxy), Javtiful(javtiful::JavtifulProxy), + Porndish(PorndishProxy), Spankbang(SpankbangProxy), } @@ -27,6 +30,7 @@ impl Proxy for AnyProxy { match self { AnyProxy::Sxyprn(p) => p.get_video_url(url, requester).await, AnyProxy::Javtiful(p) => p.get_video_url(url, requester).await, + AnyProxy::Porndish(p) => p.get_video_url(url, requester).await, AnyProxy::Spankbang(p) => p.get_video_url(url, requester).await, } } diff --git a/src/proxies/porndish.rs b/src/proxies/porndish.rs new file mode 100644 index 0000000..04ec2c2 --- /dev/null +++ b/src/proxies/porndish.rs @@ -0,0 +1,369 @@ +use ntex::web; +use regex::Regex; +use std::process::Command; +use url::Url; + +use crate::util::requester::Requester; + +#[derive(Debug, Clone)] +pub struct PorndishProxy {} + +impl PorndishProxy { + pub fn new() -> Self { + Self {} + } + + fn normalize_detail_url(endpoint: &str) -> Option { + let endpoint = endpoint.trim(); + if endpoint.is_empty() { + return None; + } + + if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + Some(endpoint.to_string()) + } else { + Some(format!("https://{}", endpoint.trim_start_matches('/'))) + } + } + + fn parse_url(url: &str) -> Option { + Url::parse(url).ok() + } + + fn is_porndish_host(host: &str) -> bool { + matches!(host, "www.porndish.com" | "porndish.com") + } + + fn is_myvidplay_host(host: &str) -> bool { + matches!(host, "myvidplay.com" | "www.myvidplay.com") + } + + fn is_vidara_host(host: &str) -> bool { + matches!(host, "vidara.so" | "www.vidara.so") + } + + fn is_allowed_detail_url(url: &str) -> bool { + let Some(url) = Self::parse_url(url) else { + return false; + }; + if url.scheme() != "https" { + return false; + } + let Some(host) = url.host_str() else { + return false; + }; + Self::is_porndish_host(host) && url.path().starts_with("/porn/") + } + + fn is_allowed_myvidplay_iframe_url(url: &str) -> bool { + let Some(url) = Self::parse_url(url) else { + return false; + }; + if url.scheme() != "https" { + return false; + } + let Some(host) = url.host_str() else { + return false; + }; + Self::is_myvidplay_host(host) && url.path().starts_with("/e/") + } + + fn is_allowed_myvidplay_pass_url(url: &str) -> bool { + let Some(url) = Self::parse_url(url) else { + return false; + }; + if url.scheme() != "https" { + return false; + } + let Some(host) = url.host_str() else { + return false; + }; + Self::is_myvidplay_host(host) && url.path().starts_with("/pass_md5/") + } + + fn is_allowed_vidara_iframe_url(url: &str) -> bool { + let Some(url) = Self::parse_url(url) else { + return false; + }; + if url.scheme() != "https" { + return false; + } + let Some(host) = url.host_str() else { + return false; + }; + Self::is_vidara_host(host) && url.path().starts_with("/e/") + } + + fn vidara_api_url(iframe_url: &str) -> Option { + let url = Self::parse_url(iframe_url)?; + if !Self::is_allowed_vidara_iframe_url(iframe_url) { + return None; + } + let filecode = url + .path_segments()? + .filter(|segment| !segment.is_empty()) + .next_back()? + .to_string(); + if filecode.is_empty() { + return None; + } + Some(format!("https://vidara.so/api/stream?filecode={filecode}")) + } + + fn regex(value: &str) -> Option { + Regex::new(value).ok() + } + + async fn fetch_with_curl_cffi(url: &str, referer: Option<&str>) -> Option { + let url = url.to_string(); + let referer = referer.unwrap_or("").to_string(); + + let output = tokio::task::spawn_blocking(move || { + Command::new("python3") + .arg("-c") + .arg( + r#" +import sys +from curl_cffi import requests + +url = sys.argv[1] +referer = sys.argv[2] if len(sys.argv) > 2 else "" +headers = {} +if referer: + headers["Referer"] = referer + +response = requests.get( + url, + impersonate="chrome", + timeout=30, + allow_redirects=True, + headers=headers, +) +if response.status_code >= 400: + sys.exit(1) +sys.stdout.buffer.write(response.content) +"#, + ) + .arg(url) + .arg(referer) + .output() + }) + .await + .ok()? + .ok()?; + + if !output.status.success() { + return None; + } + + Some(String::from_utf8_lossy(&output.stdout).to_string()) + } + + async fn resolve_first_redirect(url: &str) -> Option { + let url = url.to_string(); + let output = tokio::task::spawn_blocking(move || { + Command::new("python3") + .arg("-c") + .arg( + r#" +import sys +from curl_cffi import requests + +url = sys.argv[1] +response = requests.get( + url, + impersonate="chrome", + timeout=30, + allow_redirects=False, +) +location = response.headers.get("location", "") +if location: + sys.stdout.write(location) +"#, + ) + .arg(url) + .output() + }) + .await + .ok()? + .ok()?; + + let location = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if location.is_empty() { + None + } else { + Some(location) + } + } + + fn extract_iframe_fragments(html: &str) -> Vec { + let Some(regex) = + Self::regex(r#"const\s+[A-Za-z0-9_]+Content\s*=\s*"((?:\\.|[^"\\])*)";"#) + else { + return vec![]; + }; + + let mut fragments = Vec::new(); + for captures in regex.captures_iter(html) { + let Some(value) = captures.get(1).map(|value| value.as_str()) else { + continue; + }; + let encoded = format!("\"{value}\""); + let decoded = serde_json::from_str::(&encoded).unwrap_or_default(); + if decoded.contains(" Option { + let regex = Self::regex(r#"(?is)]+src="([^"]+)"[^>]*>"#)?; + regex + .captures(fragment) + .and_then(|captures| captures.get(1)) + .map(|value| value.as_str().to_string()) + } + + async fn resolve_myvidplay_stream(iframe_url: &str) -> Option { + if !Self::is_allowed_myvidplay_iframe_url(iframe_url) { + return None; + } + + let html = Self::fetch_with_curl_cffi(iframe_url, Some("https://www.porndish.com/")).await?; + let pass_regex = Self::regex(r#"\$\.get\(\s*['"](/pass_md5/[^'"]+)['"]"#)?; + let path = pass_regex + .captures(&html) + .and_then(|captures| captures.get(1)) + .map(|value| value.as_str().to_string())?; + + let token = path.trim_end_matches('/').rsplit('/').next()?.to_string(); + if token.is_empty() { + return None; + } + + let pass_url = if path.starts_with("http://") || path.starts_with("https://") { + path + } else { + let base = Url::parse(iframe_url).ok()?; + base.join(&path).ok()?.to_string() + }; + if !Self::is_allowed_myvidplay_pass_url(&pass_url) { + return None; + } + + let base = Self::fetch_with_curl_cffi(&pass_url, Some(iframe_url)) + .await? + .trim() + .to_string(); + if base.is_empty() || base == "RELOAD" || !base.starts_with("http") { + return None; + } + + let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok()? + .as_millis(); + let suffix = (0..10) + .map(|index| { + let pos = ((now + (index as u128 * 17)) % chars.len() as u128) as usize; + chars[pos] as char + }) + .collect::(); + + let stream_url = format!("{base}{suffix}?token={token}&expiry={now}"); + Some( + Self::resolve_first_redirect(&stream_url) + .await + .unwrap_or(stream_url), + ) + } + + async fn resolve_vidara_stream(iframe_url: &str) -> Option { + let api_url = Self::vidara_api_url(iframe_url)?; + let response = Self::fetch_with_curl_cffi(&api_url, Some(iframe_url)).await?; + let json: serde_json::Value = serde_json::from_str(&response).ok()?; + let stream_url = json + .get("streaming_url") + .and_then(|value| value.as_str())? + .trim() + .to_string(); + if stream_url.is_empty() { + return None; + } + Some(stream_url) + } + + pub async fn get_video_url( + &self, + url: String, + _requester: web::types::State, + ) -> String { + let Some(detail_url) = Self::normalize_detail_url(&url) else { + return String::new(); + }; + if !Self::is_allowed_detail_url(&detail_url) { + return String::new(); + } + + let Some(html) = + Self::fetch_with_curl_cffi(&detail_url, Some("https://www.porndish.com/")).await + else { + return String::new(); + }; + + let mut fallback_iframe: Option = None; + + for fragment in Self::extract_iframe_fragments(&html) { + let Some(iframe_url) = Self::parse_embed_source(&fragment) else { + continue; + }; + + let iframe_url = + if iframe_url.starts_with("http://") || iframe_url.starts_with("https://") { + iframe_url + } else if iframe_url.starts_with("//") { + format!("https:{iframe_url}") + } else { + continue; + }; + + if Self::is_allowed_vidara_iframe_url(&iframe_url) { + if let Some(stream_url) = Self::resolve_vidara_stream(&iframe_url).await { + return stream_url; + } + } + + if fallback_iframe.is_none() && Self::is_allowed_myvidplay_iframe_url(&iframe_url) { + fallback_iframe = Some(iframe_url); + } + } + + if let Some(iframe_url) = fallback_iframe { + if let Some(stream_url) = Self::resolve_myvidplay_stream(&iframe_url).await { + return stream_url; + } + } + + String::new() + } +} + +#[cfg(test)] +mod tests { + use super::PorndishProxy; + + #[test] + fn allows_only_porndish_detail_urls() { + assert!(PorndishProxy::is_allowed_detail_url( + "https://www.porndish.com/porn/example/" + )); + assert!(!PorndishProxy::is_allowed_detail_url( + "https://www.porndish.com/search/example/" + )); + assert!(!PorndishProxy::is_allowed_detail_url( + "https://example.com/porn/example/" + )); + } +} diff --git a/src/proxy.rs b/src/proxy.rs index 3794696..fa6e025 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,6 +1,7 @@ use ntex::web::{self, HttpRequest}; use crate::proxies::javtiful::JavtifulProxy; +use crate::proxies::porndish::PorndishProxy; use crate::proxies::spankbang::SpankbangProxy; use crate::proxies::sxyprn::SxyprnProxy; use crate::proxies::*; @@ -22,6 +23,11 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route(web::post().to(proxy2redirect)) .route(web::get().to(proxy2redirect)), ) + .service( + web::resource("/porndish/{endpoint}*") + .route(web::post().to(proxy2redirect)) + .route(web::get().to(proxy2redirect)), + ) .service( web::resource("/noodlemagazine/{endpoint}*") .route(web::post().to(crate::proxies::noodlemagazine::serve_media)) @@ -63,6 +69,7 @@ fn get_proxy(proxy: &str) -> Option { match proxy { "sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())), "javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())), + "porndish" => Some(AnyProxy::Porndish(PorndishProxy::new())), "spankbang" => Some(AnyProxy::Spankbang(SpankbangProxy::new())), _ => None, }