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 htmlentity::entity::{ICodedDataTrait, decode};
use regex::Regex; use regex::Regex;
use scraper::{ElementRef, Html, Selector}; use scraper::{ElementRef, Html, Selector};
use std::process::Command;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
error_chain! { error_chain! {
foreign_links { foreign_links {
@@ -263,52 +263,22 @@ impl PorndishProvider {
); );
} }
async fn fetch_with_curl_cffi(url: &str, referer: Option<&str>) -> Result<String> { fn request_headers(referer: Option<&str>) -> Vec<(String, String)> {
let url = url.to_string(); let referer = referer
let referer = referer.unwrap_or("").to_string(); .filter(|referer| !referer.is_empty())
.unwrap_or("https://www.porndish.com/");
vec![("Referer".to_string(), referer.to_string())]
}
let output = tokio::task::spawn_blocking(move || { async fn fetch_html(
Command::new("python3") requester: &mut Requester,
.arg("-c") url: &str,
.arg( referer: Option<&str>,
r#" ) -> Result<String> {
import sys requester
from curl_cffi import requests .get_with_headers(url, Self::request_headers(referer), None)
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 .await
.map_err(|error| Error::from(format!("spawn_blocking failed: {error}")))? .map_err(|error| Error::from(format!("request 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())
}
async fn fetch_html(url: &str) -> Result<String> {
Self::fetch_with_curl_cffi(url, None).await
} }
async fn load_filters( async fn load_filters(
@@ -317,6 +287,7 @@ sys.stdout.buffer.write(response.content)
tags: Arc<RwLock<Vec<FilterOption>>>, tags: Arc<RwLock<Vec<FilterOption>>>,
uploaders: Arc<RwLock<Vec<FilterOption>>>, uploaders: Arc<RwLock<Vec<FilterOption>>>,
) -> Result<()> { ) -> Result<()> {
let mut requester = Requester::new();
let link_selector = Self::selector("a[href]")?; let link_selector = Self::selector("a[href]")?;
let article_selector = Self::selector("article.entry-tpl-grid, article.post")?; let article_selector = Self::selector("article.entry-tpl-grid, article.post")?;
let pages = vec![ let pages = vec![
@@ -328,7 +299,7 @@ sys.stdout.buffer.write(response.content)
]; ];
for url in pages { 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, Ok(html) => html,
Err(error) => { Err(error) => {
report_provider_error_background( report_provider_error_background(
@@ -648,59 +619,64 @@ sys.stdout.buffer.write(response.content)
Ok(fragments) Ok(fragments)
} }
async fn resolve_myvidplay_stream(&self, iframe_url: &str) -> Result<String> { async fn resolve_myvidplay_stream(
let iframe_url = iframe_url.to_string(); &self,
let output = tokio::task::spawn_blocking(move || { requester: &mut Requester,
Command::new("python3") iframe_url: &str,
.arg("-c") referer: &str,
.arg( ) -> Result<String> {
r#" let html = Self::fetch_html(requester, iframe_url, Some(referer)).await?;
import re let pass_regex = Self::regex(r#"\$\.get\(\s*['"](/pass_md5/[^'"]+)['"]"#)?;
import sys let path = pass_regex
import time .captures(&html)
from curl_cffi import requests .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] let token = path
session = requests.Session(impersonate="chrome") .trim_end_matches('/')
html = session.get(iframe_url, timeout=30).text .rsplit('/')
match = re.search(r"\$\.get\(\s*['\"](/pass_md5/[^'\"]+)['\"]", html) .next()
if not match: .unwrap_or_default()
sys.stderr.write("missing pass_md5 path\n") .to_string();
sys.exit(1) if token.is_empty() {
path = match.group(1) return Err(Error::from(
token = path.rstrip("/").split("/")[-1] "myvidplay resolution failed: missing pass_md5 token".to_string(),
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}")))?;
if !output.status.success() { let pass_url = if path.starts_with("http://") || path.starts_with("https://") {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); 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!( 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") { if resolved.is_empty() || !resolved.starts_with("http") {
return Err(Error::from( return Err(Error::from(
"myvidplay resolution returned empty url".to_string(), "myvidplay resolution returned empty url".to_string(),
@@ -725,7 +701,7 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
html: &str, html: &str,
page_url: &str, page_url: &str,
options: &ServerOptions, options: &ServerOptions,
_requester: &mut Requester, requester: &mut Requester,
) -> Result<VideoItem> { ) -> Result<VideoItem> {
let ( let (
parsed_title, parsed_title,
@@ -864,7 +840,10 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
}); });
if iframe_url.contains("myvidplay.com") { 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) => { Ok(stream_url) => {
item.url = stream_url.clone(); item.url = stream_url.clone();
let mut format = VideoFormat::new( let mut format = VideoFormat::new(
@@ -919,7 +898,7 @@ sys.stdout.write(f"{base}{suffix}?token={token}&expiry={now}")
None => Requester::new(), 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, Ok(html) => html,
Err(error) => { Err(error) => {
report_provider_error_background( 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"); 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, Ok(html) => html,
Err(error) => { Err(error) => {
report_provider_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::{ use ntex::{
http::Response, http::Response,
web::{self, HttpRequest, error}, web::{self, HttpRequest, error},
}; };
use std::process::Command;
use crate::util::requester::Requester; use crate::util::requester::Requester;
pub async fn get_image( pub async fn get_image(
req: HttpRequest, req: HttpRequest,
_requester: web::types::State<Requester>, requester: web::types::State<Requester>,
) -> Result<impl web::Responder, web::Error> { ) -> Result<impl web::Responder, web::Error> {
let endpoint = req.match_info().query("endpoint").to_string(); let endpoint = req.match_info().query("endpoint").to_string();
let image_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { 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('/')) format!("https://{}", endpoint.trim_start_matches('/'))
}; };
let output = tokio::task::spawn_blocking(move || { let upstream = match requester
Command::new("python3") .get_ref()
.arg("-c") .clone()
.arg( .get_raw_with_headers(
r#" image_url.as_str(),
import sys vec![(
from curl_cffi import requests "Referer".to_string(),
"https://www.porndish.com/".to_string(),
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 .await
.map_err(error::ErrorBadGateway)? {
.map_err(error::ErrorBadGateway)?; Ok(response) => response,
Err(_) => return Ok(web::HttpResponse::NotFound().finish()),
};
if !output.status.success() { let status = upstream.status();
let headers = upstream.headers().clone();
let bytes = upstream.bytes().await.map_err(error::ErrorBadGateway)?;
if !status.is_success() {
return Ok(web::HttpResponse::NotFound().finish()); return Ok(web::HttpResponse::NotFound().finish());
} }
let content_type = String::from_utf8_lossy(&output.stderr).trim().to_string(); let mut resp = Response::build(status);
let mut resp = Response::build(ntex::http::StatusCode::OK); if let Some(ct) = headers.get(CONTENT_TYPE) {
if !content_type.is_empty() { if let Ok(ct_str) = ct.to_str() {
resp.set_header(CONTENT_TYPE, content_type); 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