porn4fans fix
This commit is contained in:
@@ -8,6 +8,7 @@ use crate::util::time::parse_time_to_seconds;
|
||||
use crate::videos::{ServerOptions, VideoItem};
|
||||
use async_trait::async_trait;
|
||||
use error_chain::error_chain;
|
||||
use futures::future::join_all;
|
||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
@@ -24,6 +25,17 @@ pub struct Porn4fansProvider {
|
||||
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 {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -137,8 +149,7 @@ impl Porn4fansProvider {
|
||||
return Ok(old_items);
|
||||
}
|
||||
|
||||
let video_items =
|
||||
self.get_video_items_from_html(text, options.public_url_base.as_deref().unwrap_or(""));
|
||||
let video_items = self.get_video_items_from_html(text, requester).await;
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
@@ -195,8 +206,7 @@ impl Porn4fansProvider {
|
||||
return Ok(old_items);
|
||||
}
|
||||
|
||||
let video_items =
|
||||
self.get_video_items_from_html(text, options.public_url_base.as_deref().unwrap_or(""));
|
||||
let video_items = self.get_video_items_from_html(text, requester).await;
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
@@ -232,20 +242,6 @@ impl Porn4fansProvider {
|
||||
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 {
|
||||
let thumb_raw = Self::first_non_empty_attr(
|
||||
segment,
|
||||
@@ -266,6 +262,10 @@ impl Porn4fansProvider {
|
||||
self.normalize_url(&thumb_raw)
|
||||
}
|
||||
|
||||
fn decode_escaped_text(text: &str) -> String {
|
||||
text.replace("\\/", "/").replace("&", "&")
|
||||
}
|
||||
|
||||
fn extract_views(text: &str) -> Option<u32> {
|
||||
Regex::new(r"(?i)<svg[^>]+icon-eye[^>]*>.*?</svg>\s*<span>([^<]+)</span>")
|
||||
.ok()
|
||||
@@ -282,7 +282,28 @@ impl Porn4fansProvider {
|
||||
.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() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -296,7 +317,7 @@ impl Porn4fansProvider {
|
||||
let mut items = Vec::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 {
|
||||
continue;
|
||||
};
|
||||
@@ -328,25 +349,65 @@ impl Porn4fansProvider {
|
||||
let views = Self::extract_views(body).unwrap_or(0);
|
||||
let rating = Self::extract_rating(body);
|
||||
|
||||
let mut item = VideoItem::new(
|
||||
items.push(Porn4fansCard {
|
||||
id,
|
||||
title,
|
||||
self.proxy_url(proxy_base_url, &href),
|
||||
"porn4fans".to_string(),
|
||||
page_url: href,
|
||||
thumb,
|
||||
duration,
|
||||
);
|
||||
if views > 0 {
|
||||
item = item.views(views);
|
||||
}
|
||||
if let Some(rating) = rating {
|
||||
item = item.rating(rating);
|
||||
}
|
||||
items.push(item);
|
||||
views: (views > 0).then_some(views),
|
||||
rating,
|
||||
});
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -424,7 +485,7 @@ mod tests {
|
||||
<div class="duration">23:47</div>
|
||||
<picture>
|
||||
<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>
|
||||
</div>
|
||||
<div class="video-text">Horny Police Officer Melztube Gets Banged By BBC</div>
|
||||
@@ -446,12 +507,12 @@ mod tests {
|
||||
</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[0].id, "10194");
|
||||
assert_eq!(
|
||||
items[0].url,
|
||||
"https://example.com/proxy/porn4fans/video/10194/horny-police-officer-melztube-gets-banged-by-bbc/"
|
||||
items[0].page_url,
|
||||
"https://www.porn4fans.com/video/10194/horny-police-officer-melztube-gets-banged-by-bbc/"
|
||||
);
|
||||
assert_eq!(
|
||||
items[0].thumb,
|
||||
@@ -461,4 +522,23 @@ mod tests {
|
||||
assert_eq!(items[0].views, Some(14_000));
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use ntex::web;
|
||||
|
||||
use crate::proxies::porn4fans::Porn4fansProxy;
|
||||
use crate::proxies::spankbang::SpankbangProxy;
|
||||
use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester};
|
||||
|
||||
@@ -8,13 +7,11 @@ pub mod hanimecdn;
|
||||
pub mod hqpornerthumb;
|
||||
pub mod javtiful;
|
||||
pub mod noodlemagazine;
|
||||
pub mod porn4fans;
|
||||
pub mod spankbang;
|
||||
pub mod sxyprn;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AnyProxy {
|
||||
Porn4fans(Porn4fansProxy),
|
||||
Sxyprn(SxyprnProxy),
|
||||
Javtiful(javtiful::JavtifulProxy),
|
||||
Spankbang(SpankbangProxy),
|
||||
@@ -27,7 +24,6 @@ pub trait Proxy {
|
||||
impl Proxy for AnyProxy {
|
||||
async fn get_video_url(&self, url: String, requester: web::types::State<Requester>) -> String {
|
||||
match self {
|
||||
AnyProxy::Porn4fans(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::Spankbang(p) => p.get_video_url(url, requester).await,
|
||||
|
||||
@@ -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("&", "&")
|
||||
}
|
||||
|
||||
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/"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use ntex::web::{self, HttpRequest};
|
||||
|
||||
use crate::proxies::javtiful::JavtifulProxy;
|
||||
use crate::proxies::porn4fans::Porn4fansProxy;
|
||||
use crate::proxies::spankbang::SpankbangProxy;
|
||||
use crate::proxies::sxyprn::SxyprnProxy;
|
||||
use crate::proxies::*;
|
||||
@@ -23,11 +22,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.route(web::post().to(proxy2redirect))
|
||||
.route(web::get().to(proxy2redirect)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/porn4fans/{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))
|
||||
@@ -62,7 +56,6 @@ async fn proxy2redirect(
|
||||
|
||||
fn get_proxy(proxy: &str) -> Option<AnyProxy> {
|
||||
match proxy {
|
||||
"porn4fans" => Some(AnyProxy::Porn4fans(Porn4fansProxy::new())),
|
||||
"sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())),
|
||||
"javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())),
|
||||
"spankbang" => Some(AnyProxy::Spankbang(SpankbangProxy::new())),
|
||||
|
||||
Reference in New Issue
Block a user