porn4fans fix

This commit is contained in:
Simon
2026-03-13 12:53:33 +00:00
parent 6a62582c09
commit 0137313c6e
4 changed files with 115 additions and 192 deletions

View File

@@ -8,6 +8,7 @@ use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoItem}; use crate::videos::{ServerOptions, VideoItem};
use async_trait::async_trait; use async_trait::async_trait;
use error_chain::error_chain; use error_chain::error_chain;
use futures::future::join_all;
use htmlentity::entity::{ICodedDataTrait, decode}; use htmlentity::entity::{ICodedDataTrait, decode};
use regex::Regex; use regex::Regex;
use std::collections::HashSet; use std::collections::HashSet;
@@ -24,6 +25,17 @@ pub struct Porn4fansProvider {
url: String, url: String,
} }
#[derive(Debug, Clone)]
struct Porn4fansCard {
id: String,
title: String,
page_url: String,
thumb: String,
duration: u32,
views: Option<u32>,
rating: Option<f32>,
}
impl Porn4fansProvider { impl Porn4fansProvider {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -137,8 +149,7 @@ impl Porn4fansProvider {
return Ok(old_items); return Ok(old_items);
} }
let video_items = let video_items = self.get_video_items_from_html(text, requester).await;
self.get_video_items_from_html(text, options.public_url_base.as_deref().unwrap_or(""));
if !video_items.is_empty() { if !video_items.is_empty() {
cache.remove(&video_url); cache.remove(&video_url);
cache.insert(video_url.clone(), video_items.clone()); cache.insert(video_url.clone(), video_items.clone());
@@ -195,8 +206,7 @@ impl Porn4fansProvider {
return Ok(old_items); return Ok(old_items);
} }
let video_items = let video_items = self.get_video_items_from_html(text, requester).await;
self.get_video_items_from_html(text, options.public_url_base.as_deref().unwrap_or(""));
if !video_items.is_empty() { if !video_items.is_empty() {
cache.remove(&video_url); cache.remove(&video_url);
cache.insert(video_url.clone(), video_items.clone()); cache.insert(video_url.clone(), video_items.clone());
@@ -232,20 +242,6 @@ impl Porn4fansProvider {
format!("{}/{}", self.url, url.trim_start_matches("./")) format!("{}/{}", self.url, url.trim_start_matches("./"))
} }
fn proxy_url(&self, proxy_base_url: &str, url: &str) -> String {
let path = url
.strip_prefix(&self.url)
.unwrap_or(url)
.trim_start_matches('/');
if proxy_base_url.is_empty() {
return format!("/proxy/porn4fans/{path}");
}
format!(
"{}/proxy/porn4fans/{path}",
proxy_base_url.trim_end_matches('/')
)
}
fn extract_thumb_url(&self, segment: &str) -> String { fn extract_thumb_url(&self, segment: &str) -> String {
let thumb_raw = Self::first_non_empty_attr( let thumb_raw = Self::first_non_empty_attr(
segment, segment,
@@ -266,6 +262,10 @@ impl Porn4fansProvider {
self.normalize_url(&thumb_raw) self.normalize_url(&thumb_raw)
} }
fn decode_escaped_text(text: &str) -> String {
text.replace("\\/", "/").replace("&amp;", "&")
}
fn extract_views(text: &str) -> Option<u32> { fn extract_views(text: &str) -> Option<u32> {
Regex::new(r"(?i)<svg[^>]+icon-eye[^>]*>.*?</svg>\s*<span>([^<]+)</span>") Regex::new(r"(?i)<svg[^>]+icon-eye[^>]*>.*?</svg>\s*<span>([^<]+)</span>")
.ok() .ok()
@@ -282,7 +282,28 @@ impl Porn4fansProvider {
.and_then(|m| m.as_str().trim().parse::<f32>().ok()) .and_then(|m| m.as_str().trim().parse::<f32>().ok())
} }
fn get_video_items_from_html(&self, html: String, proxy_base_url: &str) -> Vec<VideoItem> { fn extract_direct_video_url_from_page(text: &str) -> Option<String> {
let decoded = Self::decode_escaped_text(text);
for key in ["video_url", "video_alt_url", "contentUrl"] {
let pattern = format!(
r#"(?is)(?:^|[{{\s,])["']?{}["']?\s*[:=]\s*["'](?P<url>https?://[^"'<>]+?\.mp4)"#,
regex::escape(key)
);
let regex = Regex::new(&pattern).ok()?;
if let Some(url) = regex
.captures(&decoded)
.and_then(|captures| captures.name("url"))
.map(|value| value.as_str().to_string())
{
return Some(url);
}
}
None
}
fn parse_video_cards_from_html(&self, html: &str) -> Vec<Porn4fansCard> {
if html.trim().is_empty() { if html.trim().is_empty() {
return vec![]; return vec![];
} }
@@ -296,7 +317,7 @@ impl Porn4fansProvider {
let mut items = Vec::new(); let mut items = Vec::new();
let mut seen = HashSet::new(); let mut seen = HashSet::new();
for captures in link_re.captures_iter(&html) { for captures in link_re.captures_iter(html) {
let Some(id) = captures.name("id").map(|m| m.as_str().to_string()) else { let Some(id) = captures.name("id").map(|m| m.as_str().to_string()) else {
continue; continue;
}; };
@@ -328,25 +349,65 @@ impl Porn4fansProvider {
let views = Self::extract_views(body).unwrap_or(0); let views = Self::extract_views(body).unwrap_or(0);
let rating = Self::extract_rating(body); let rating = Self::extract_rating(body);
let mut item = VideoItem::new( items.push(Porn4fansCard {
id, id,
title, title,
self.proxy_url(proxy_base_url, &href), page_url: href,
"porn4fans".to_string(),
thumb, thumb,
duration, duration,
); views: (views > 0).then_some(views),
if views > 0 { rating,
item = item.views(views); });
}
if let Some(rating) = rating {
item = item.rating(rating);
}
items.push(item);
} }
items items
} }
async fn enrich_video_card(
&self,
card: Porn4fansCard,
mut requester: crate::util::requester::Requester,
) -> VideoItem {
let direct_url = requester
.get_with_headers(
&card.page_url,
vec![("Referer".to_string(), format!("{}/", self.url))],
None,
)
.await
.ok()
.and_then(|text| Self::extract_direct_video_url_from_page(&text))
.unwrap_or_else(|| card.page_url.clone());
let mut item = VideoItem::new(
card.id,
card.title,
direct_url,
"porn4fans".to_string(),
card.thumb,
card.duration,
);
if let Some(views) = card.views {
item = item.views(views);
}
if let Some(rating) = card.rating {
item = item.rating(rating);
}
item
}
async fn get_video_items_from_html(
&self,
html: String,
requester: crate::util::requester::Requester,
) -> Vec<VideoItem> {
let cards = self.parse_video_cards_from_html(&html);
let futures = cards
.into_iter()
.map(|card| self.enrich_video_card(card, requester.clone()));
join_all(futures).await
}
} }
#[async_trait] #[async_trait]
@@ -424,7 +485,7 @@ mod tests {
<div class="duration">23:47</div> <div class="duration">23:47</div>
<picture> <picture>
<source srcset="https://www.porn4fans.com/contents/videos_screenshots/10000/10194/800x450/1.jpg" type="image/webp"> <source srcset="https://www.porn4fans.com/contents/videos_screenshots/10000/10194/800x450/1.jpg" type="image/webp">
<img class="thumb lazy-load" src="data:image/gif;base64,AAAA" data-original="https://www.porn4fans.com/contents/videos_screenshots/10000/10194/800x450/1.jpg" data-webp="https://www.porn4fans.com/contents/videos_screenshots/10000/10194/800x450/1.jpg" alt="Horny Police Officer Melztube Gets Banged By BBC" /> <img class="thumb lazy-load" src="data:image/gif;base64,AAAA" data-original="https://www.porn4fans.com/contents/videos_screenshots/10000/10194/800x450/1.jpg" data-webp="https://www.porn4fans.com/contents/videos_screenshots/10000/10194/800x450/1.jpg" data-preview="https://www.porn4fans.com/get_file/3/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f/10000/10194/10194_preview_high.mp4/" alt="Horny Police Officer Melztube Gets Banged By BBC" />
</picture> </picture>
</div> </div>
<div class="video-text">Horny Police Officer Melztube Gets Banged By BBC</div> <div class="video-text">Horny Police Officer Melztube Gets Banged By BBC</div>
@@ -446,12 +507,12 @@ mod tests {
</div> </div>
"##; "##;
let items = provider.get_video_items_from_html(html.to_string(), "https://example.com"); let items = provider.parse_video_cards_from_html(html);
assert_eq!(items.len(), 1); assert_eq!(items.len(), 1);
assert_eq!(items[0].id, "10194"); assert_eq!(items[0].id, "10194");
assert_eq!( assert_eq!(
items[0].url, items[0].page_url,
"https://example.com/proxy/porn4fans/video/10194/horny-police-officer-melztube-gets-banged-by-bbc/" "https://www.porn4fans.com/video/10194/horny-police-officer-melztube-gets-banged-by-bbc/"
); );
assert_eq!( assert_eq!(
items[0].thumb, items[0].thumb,
@@ -461,4 +522,23 @@ mod tests {
assert_eq!(items[0].views, Some(14_000)); assert_eq!(items[0].views, Some(14_000));
assert_eq!(items[0].rating, Some(66.0)); assert_eq!(items[0].rating, Some(66.0));
} }
#[test]
fn extracts_direct_video_url_from_video_page() {
let html = r#"
<script>
var flashvars = {
video_url: 'https:\/\/www.porn4fans.com\/get_file\/3\/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f\/10000\/10951\/10951.mp4\/',
video_alt_url: 'https:\/\/www.porn4fans.com\/get_file\/3\/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f\/10000\/10951\/10951_720p.mp4\/'
};
</script>
"#;
assert_eq!(
Porn4fansProvider::extract_direct_video_url_from_page(html).as_deref(),
Some(
"https://www.porn4fans.com/get_file/3/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f/10000/10951/10951.mp4"
)
);
}
} }

View File

@@ -1,6 +1,5 @@
use ntex::web; use ntex::web;
use crate::proxies::porn4fans::Porn4fansProxy;
use crate::proxies::spankbang::SpankbangProxy; use crate::proxies::spankbang::SpankbangProxy;
use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester}; use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester};
@@ -8,13 +7,11 @@ pub mod hanimecdn;
pub mod hqpornerthumb; pub mod hqpornerthumb;
pub mod javtiful; pub mod javtiful;
pub mod noodlemagazine; pub mod noodlemagazine;
pub mod porn4fans;
pub mod spankbang; pub mod spankbang;
pub mod sxyprn; pub mod sxyprn;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum AnyProxy { pub enum AnyProxy {
Porn4fans(Porn4fansProxy),
Sxyprn(SxyprnProxy), Sxyprn(SxyprnProxy),
Javtiful(javtiful::JavtifulProxy), Javtiful(javtiful::JavtifulProxy),
Spankbang(SpankbangProxy), Spankbang(SpankbangProxy),
@@ -27,7 +24,6 @@ pub trait Proxy {
impl Proxy for AnyProxy { impl Proxy for AnyProxy {
async fn get_video_url(&self, url: String, requester: web::types::State<Requester>) -> String { async fn get_video_url(&self, url: String, requester: web::types::State<Requester>) -> String {
match self { match self {
AnyProxy::Porn4fans(p) => p.get_video_url(url, requester).await,
AnyProxy::Sxyprn(p) => p.get_video_url(url, requester).await, AnyProxy::Sxyprn(p) => p.get_video_url(url, requester).await,
AnyProxy::Javtiful(p) => p.get_video_url(url, requester).await, AnyProxy::Javtiful(p) => p.get_video_url(url, requester).await,
AnyProxy::Spankbang(p) => p.get_video_url(url, requester).await, AnyProxy::Spankbang(p) => p.get_video_url(url, requester).await,

View File

@@ -1,146 +0,0 @@
use ntex::web;
use regex::Regex;
use crate::util::requester::Requester;
#[derive(Debug, Clone)]
pub struct Porn4fansProxy {}
impl Porn4fansProxy {
pub fn new() -> Self {
Porn4fansProxy {}
}
fn request_headers() -> Vec<(String, String)> {
vec![(
"Referer".to_string(),
"https://www.porn4fans.com/".to_string(),
)]
}
fn normalize_page_url(url: &str) -> String {
if url.starts_with("http://") || url.starts_with("https://") {
return url.to_string();
}
let trimmed = url.trim_start_matches('/');
if trimmed.starts_with("www.porn4fans.com/") || trimmed.starts_with("porn4fans.com/") {
return format!("https://{trimmed}");
}
format!("https://www.porn4fans.com/{trimmed}")
}
fn decode_escaped_text(text: &str) -> String {
text.replace("\\/", "/").replace("&amp;", "&")
}
fn extract_preferred_video_url(text: &str) -> Option<String> {
let decoded = Self::decode_escaped_text(text);
let video_url_re = Regex::new(
r#"(?is)(?:^|[{\s,])["']?video_url["']?\s*[:=]\s*["'](?P<url>https?://[^"'<>]+?\.mp4/?(?:\?[^"'<>]*)?)["']"#,
)
.ok()?;
if let Some(url) = video_url_re
.captures(&decoded)
.and_then(|captures| captures.name("url"))
.map(|value| value.as_str().to_string())
{
return Some(url);
}
let generic_mp4_re = Regex::new(
r#"(?is)(?P<url>https?://[^"'<>\s]+/get_file/[^"'<>\s]+?\.mp4/?(?:\?[^"'<>]*)?)"#,
)
.ok()?;
generic_mp4_re
.captures(&decoded)
.and_then(|captures| captures.name("url"))
.map(|value| value.as_str().to_string())
}
fn extract_rnd(text: &str) -> Option<String> {
let decoded = Self::decode_escaped_text(text);
let rnd_re =
Regex::new(r#"(?is)(?:^|[{\s,])["']?rnd["']?\s*[:=]\s*["']?(?P<rnd>\d{8,})"#).ok()?;
rnd_re
.captures(&decoded)
.and_then(|captures| captures.name("rnd"))
.map(|value| value.as_str().to_string())
}
fn attach_rnd(url: String, rnd: Option<String>) -> String {
if url.is_empty() || url.contains("rnd=") {
return url;
}
let Some(rnd) = rnd else {
return url;
};
let separator = if url.contains('?') { '&' } else { '?' };
format!("{url}{separator}rnd={rnd}")
}
pub async fn get_video_url(
&self,
url: String,
requester: web::types::State<Requester>,
) -> String {
let mut requester = requester.get_ref().clone();
let page_url = Self::normalize_page_url(&url);
let text = requester
.get_with_headers(&page_url, Self::request_headers(), None)
.await
.unwrap_or_default();
if text.is_empty() {
return String::new();
}
let Some(video_url) = Self::extract_preferred_video_url(&text) else {
return String::new();
};
Self::attach_rnd(video_url, Self::extract_rnd(&text))
}
}
#[cfg(test)]
mod tests {
use super::Porn4fansProxy;
#[test]
fn extracts_video_url_and_appends_rnd() {
let html = r#"
<script>
var flashvars = {
video_url: 'https:\/\/www.porn4fans.com\/get_file\/3\/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f\/10000\/10951\/10951.mp4\/',
video_alt_url: 'https:\/\/www.porn4fans.com\/get_file\/3\/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f\/10000\/10951\/10951_720p.mp4\/',
rnd: '1773402926076'
};
</script>
"#;
let video_url = Porn4fansProxy::extract_preferred_video_url(html).unwrap();
assert_eq!(
Porn4fansProxy::attach_rnd(video_url, Porn4fansProxy::extract_rnd(html)),
"https://www.porn4fans.com/get_file/3/9df8de1fc2da5dfcbf9a4ad512dc8f306c4997e60f/10000/10951/10951.mp4/?rnd=1773402926076"
);
}
#[test]
fn normalizes_relative_proxy_target() {
assert_eq!(
Porn4fansProxy::normalize_page_url("video/10951/example/"),
"https://www.porn4fans.com/video/10951/example/"
);
assert_eq!(
Porn4fansProxy::normalize_page_url("www.porn4fans.com/video/10951/example/"),
"https://www.porn4fans.com/video/10951/example/"
);
}
}

View File

@@ -1,7 +1,6 @@
use ntex::web::{self, HttpRequest}; use ntex::web::{self, HttpRequest};
use crate::proxies::javtiful::JavtifulProxy; use crate::proxies::javtiful::JavtifulProxy;
use crate::proxies::porn4fans::Porn4fansProxy;
use crate::proxies::spankbang::SpankbangProxy; use crate::proxies::spankbang::SpankbangProxy;
use crate::proxies::sxyprn::SxyprnProxy; use crate::proxies::sxyprn::SxyprnProxy;
use crate::proxies::*; use crate::proxies::*;
@@ -23,11 +22,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route(web::post().to(proxy2redirect)) .route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)), .route(web::get().to(proxy2redirect)),
) )
.service(
web::resource("/porn4fans/{endpoint}*")
.route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)),
)
.service( .service(
web::resource("/noodlemagazine/{endpoint}*") web::resource("/noodlemagazine/{endpoint}*")
.route(web::post().to(crate::proxies::noodlemagazine::serve_media)) .route(web::post().to(crate::proxies::noodlemagazine::serve_media))
@@ -62,7 +56,6 @@ async fn proxy2redirect(
fn get_proxy(proxy: &str) -> Option<AnyProxy> { fn get_proxy(proxy: &str) -> Option<AnyProxy> {
match proxy { match proxy {
"porn4fans" => Some(AnyProxy::Porn4fans(Porn4fansProxy::new())),
"sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())), "sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())),
"javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())), "javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())),
"spankbang" => Some(AnyProxy::Spankbang(SpankbangProxy::new())), "spankbang" => Some(AnyProxy::Spankbang(SpankbangProxy::new())),