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 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("&amp;", "&")
}
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"
)
);
}
}