porndish fix

This commit is contained in:
Simon
2026-03-16 22:30:20 +00:00
parent e19bc50ad1
commit 538f934b06
3 changed files with 114 additions and 136 deletions

View File

@@ -13,9 +13,9 @@ use futures::stream::{self, StreamExt};
use htmlentity::entity::{ICodedDataTrait, decode};
use regex::Regex;
use scraper::{ElementRef, Html, Selector};
use std::process::Command;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
error_chain! {
foreign_links {
@@ -263,52 +263,22 @@ impl PorndishProvider {
);
}
async fn fetch_with_curl_cffi(url: &str, referer: Option<&str>) -> Result<String> {
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 = {"Referer": referer} if referer else {}
response = requests.get(
url,
impersonate="chrome",
timeout=30,
allow_redirects=True,
headers=headers,
)
if response.status_code >= 400:
sys.stderr.write(f"status={response.status_code} url={response.url}\n")
sys.exit(1)
sys.stdout.buffer.write(response.content)
"#,
)
.arg(url)
.arg(referer)
.output()
})
.await
.map_err(|error| Error::from(format!("spawn_blocking failed: {error}")))?
.map_err(|error| Error::from(format!("python3 execution failed: {error}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(Error::from(format!("curl_cffi request failed: {stderr}")));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
fn request_headers(referer: Option<&str>) -> Vec<(String, String)> {
let referer = referer
.filter(|referer| !referer.is_empty())
.unwrap_or("https://www.porndish.com/");
vec![("Referer".to_string(), referer.to_string())]
}
async fn fetch_html(url: &str) -> Result<String> {
Self::fetch_with_curl_cffi(url, None).await
async fn fetch_html(
requester: &mut Requester,
url: &str,
referer: Option<&str>,
) -> Result<String> {
requester
.get_with_headers(url, Self::request_headers(referer), None)
.await
.map_err(|error| Error::from(format!("request failed: {error}")))
}
async fn load_filters(
@@ -317,6 +287,7 @@ sys.stdout.buffer.write(response.content)
tags: Arc<RwLock<Vec<FilterOption>>>,
uploaders: Arc<RwLock<Vec<FilterOption>>>,
) -> Result<()> {
let mut requester = Requester::new();
let link_selector = Self::selector("a[href]")?;
let article_selector = Self::selector("article.entry-tpl-grid, article.post")?;
let pages = vec![
@@ -328,7 +299,7 @@ sys.stdout.buffer.write(response.content)
];
for url in pages {
let html = match Self::fetch_html(&url).await {
let html = match Self::fetch_html(&mut requester, &url, None).await {
Ok(html) => html,
Err(error) => {
report_provider_error_background(
@@ -648,59 +619,64 @@ sys.stdout.buffer.write(response.content)
Ok(fragments)
}
async fn resolve_myvidplay_stream(&self, iframe_url: &str) -> Result<String> {
let iframe_url = iframe_url.to_string();
let output = tokio::task::spawn_blocking(move || {
Command::new("python3")
.arg("-c")
.arg(
r#"
import re
import sys
import time
from curl_cffi import requests
async fn resolve_myvidplay_stream(
&self,
requester: &mut Requester,
iframe_url: &str,
referer: &str,
) -> Result<String> {
let html = Self::fetch_html(requester, iframe_url, Some(referer)).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()))
.ok_or_else(|| Error::from("myvidplay resolution failed: missing pass_md5 path"))?;
iframe_url = sys.argv[1]
session = requests.Session(impersonate="chrome")
html = session.get(iframe_url, timeout=30).text
match = re.search(r"\$\.get\(\s*['\"](/pass_md5/[^'\"]+)['\"]", html)
if not match:
sys.stderr.write("missing pass_md5 path\n")
sys.exit(1)
path = match.group(1)
token = path.rstrip("/").split("/")[-1]
if not token:
sys.stderr.write("missing pass_md5 token\n")
sys.exit(1)
if path.startswith("http://") or path.startswith("https://"):
pass_url = path
else:
pass_url = "/".join(iframe_url.split("/")[:3]) + path
base = session.get(pass_url, headers={"Referer": iframe_url}, timeout=30).text.strip()
if not base or base == "RELOAD" or not base.startswith("http"):
sys.stderr.write(f"unusable pass_md5 response: {base[:120]}\n")
sys.exit(1)
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
now = int(time.time() * 1000)
suffix = "".join(chars[(now + i * 17) % len(chars)] for i in range(10))
sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
"#,
)
.arg(iframe_url)
.output()
})
.await
.map_err(|error| Error::from(format!("spawn_blocking failed: {error}")))?
.map_err(|error| Error::from(format!("python3 execution failed: {error}")))?;
let token = path
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or_default()
.to_string();
if token.is_empty() {
return Err(Error::from(
"myvidplay resolution failed: missing pass_md5 token".to_string(),
));
}
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let pass_url = if path.starts_with("http://") || path.starts_with("https://") {
path
} else {
let base = url::Url::parse(iframe_url)
.map_err(|error| Error::from(format!("invalid iframe url: {error}")))?;
base.join(&path)
.map_err(|error| Error::from(format!("invalid pass_md5 url: {error}")))?
.to_string()
};
let base = Self::fetch_html(requester, &pass_url, Some(iframe_url))
.await?
.trim()
.to_string();
if base.is_empty() || base == "RELOAD" || !base.starts_with("http") {
return Err(Error::from(format!(
"myvidplay resolution failed: {stderr}"
"myvidplay resolution failed: unusable pass_md5 response: {}",
&base.chars().take(120).collect::<String>()
)));
}
let resolved = String::from_utf8_lossy(&output.stdout).trim().to_string();
let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| Error::from(format!("time error: {error}")))?
.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::<String>();
let resolved = format!("{base}{suffix}?token={token}&expiry={now}");
if resolved.is_empty() || !resolved.starts_with("http") {
return Err(Error::from(
"myvidplay resolution returned empty url".to_string(),
@@ -725,7 +701,7 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
html: &str,
page_url: &str,
options: &ServerOptions,
_requester: &mut Requester,
requester: &mut Requester,
) -> Result<VideoItem> {
let (
parsed_title,
@@ -864,7 +840,10 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
});
if iframe_url.contains("myvidplay.com") {
match self.resolve_myvidplay_stream(&iframe_url).await {
match self
.resolve_myvidplay_stream(requester, &iframe_url, page_url)
.await
{
Ok(stream_url) => {
item.url = stream_url.clone();
let mut format = VideoFormat::new(
@@ -919,7 +898,7 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
None => Requester::new(),
};
let html = match Self::fetch_with_curl_cffi(&page_url, None).await {
let html = match Self::fetch_html(&mut requester, &page_url, None).await {
Ok(html) => html,
Err(error) => {
report_provider_error_background(
@@ -959,10 +938,10 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
}
}
let _requester =
let mut requester =
crate::providers::requester_or_default(options, "porndish", "missing_requester");
let html = match Self::fetch_with_curl_cffi(&url, None).await {
let html = match Self::fetch_html(&mut requester, &url, None).await {
Ok(html) => html,
Err(error) => {
report_provider_error(

View File

@@ -1,15 +1,14 @@
use ntex::http::header::CONTENT_TYPE;
use ntex::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
use ntex::{
http::Response,
web::{self, HttpRequest, error},
};
use std::process::Command;
use crate::util::requester::Requester;
pub async fn get_image(
req: HttpRequest,
_requester: web::types::State<Requester>,
requester: web::types::State<Requester>,
) -> Result<impl web::Responder, web::Error> {
let endpoint = req.match_info().query("endpoint").to_string();
let image_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
@@ -18,45 +17,41 @@ pub async fn get_image(
format!("https://{}", endpoint.trim_start_matches('/'))
};
let output = tokio::task::spawn_blocking(move || {
Command::new("python3")
.arg("-c")
.arg(
r#"
import sys
from curl_cffi import requests
let upstream = match requester
.get_ref()
.clone()
.get_raw_with_headers(
image_url.as_str(),
vec![(
"Referer".to_string(),
"https://www.porndish.com/".to_string(),
)],
)
.await
{
Ok(response) => response,
Err(_) => return Ok(web::HttpResponse::NotFound().finish()),
};
url = sys.argv[1]
response = requests.get(
url,
impersonate="chrome",
timeout=30,
allow_redirects=True,
headers={"Referer": "https://www.porndish.com/"},
)
if response.status_code >= 400:
sys.stderr.write(f"status={response.status_code}\n")
sys.exit(1)
sys.stderr.write(response.headers.get("content-type", "application/octet-stream"))
sys.stdout.buffer.write(response.content)
"#,
)
.arg(image_url)
.output()
})
.await
.map_err(error::ErrorBadGateway)?
.map_err(error::ErrorBadGateway)?;
let status = upstream.status();
let headers = upstream.headers().clone();
let bytes = upstream.bytes().await.map_err(error::ErrorBadGateway)?;
if !output.status.success() {
if !status.is_success() {
return Ok(web::HttpResponse::NotFound().finish());
}
let content_type = String::from_utf8_lossy(&output.stderr).trim().to_string();
let mut resp = Response::build(ntex::http::StatusCode::OK);
if !content_type.is_empty() {
resp.set_header(CONTENT_TYPE, content_type);
let mut resp = Response::build(status);
if let Some(ct) = headers.get(CONTENT_TYPE) {
if let Ok(ct_str) = ct.to_str() {
resp.set_header(CONTENT_TYPE, ct_str);
}
}
if let Some(cl) = headers.get(CONTENT_LENGTH) {
if let Ok(cl_str) = cl.to_str() {
resp.set_header(CONTENT_LENGTH, cl_str);
}
}
Ok(resp.body(output.stdout))
Ok(resp.body(bytes.to_vec()))
}

View File

@@ -1 +1,5 @@
/app/target/release/hottub
#!/usr/bin/env bash
set -euo pipefail
cd /app
exec cargo run --release