diff --git a/src/providers/porn4fans.rs b/src/providers/porn4fans.rs index 63c9242..620e5bc 100644 --- a/src/providers/porn4fans.rs +++ b/src/providers/porn4fans.rs @@ -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, + rating: Option, +} + 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 { Regex::new(r"(?i)]+icon-eye[^>]*>.*?\s*([^<]+)") .ok() @@ -282,7 +282,28 @@ impl Porn4fansProvider { .and_then(|m| m.as_str().trim().parse::().ok()) } - fn get_video_items_from_html(&self, html: String, proxy_base_url: &str) -> Vec { + fn extract_direct_video_url_from_page(text: &str) -> Option { + let decoded = Self::decode_escaped_text(text); + + for key in ["video_url", "video_alt_url", "contentUrl"] { + let pattern = format!( + r#"(?is)(?:^|[{{\s,])["']?{}["']?\s*[:=]\s*["'](?Phttps?://[^"'<>]+?\.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 { 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 { + 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 {
23:47
- Horny Police Officer Melztube Gets Banged By BBC + Horny Police Officer Melztube Gets Banged By BBC
Horny Police Officer Melztube Gets Banged By BBC
@@ -446,12 +507,12 @@ mod tests { "##; - 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#" + + "#; + + 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" + ) + ); + } } diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs index f1f735f..c9e5b55 100644 --- a/src/proxies/mod.rs +++ b/src/proxies/mod.rs @@ -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) -> 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, diff --git a/src/proxies/porn4fans.rs b/src/proxies/porn4fans.rs deleted file mode 100644 index e075dff..0000000 --- a/src/proxies/porn4fans.rs +++ /dev/null @@ -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 { - let decoded = Self::decode_escaped_text(text); - let video_url_re = Regex::new( - r#"(?is)(?:^|[{\s,])["']?video_url["']?\s*[:=]\s*["'](?Phttps?://[^"'<>]+?\.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)(?Phttps?://[^"'<>\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 { - let decoded = Self::decode_escaped_text(text); - let rnd_re = - Regex::new(r#"(?is)(?:^|[{\s,])["']?rnd["']?\s*[:=]\s*["']?(?P\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 { - 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, - ) -> 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#" - - "#; - - 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/" - ); - } -} diff --git a/src/proxy.rs b/src/proxy.rs index b78200f..a3b0422 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -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 { 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())),