porn4fans fix
This commit is contained in:
@@ -188,7 +188,9 @@ impl HqpornerProvider {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
|
||||||
let video_items = self.get_video_items_from_html(text, &mut requester, &options).await;
|
let video_items = self
|
||||||
|
.get_video_items_from_html(text, &mut requester, &options)
|
||||||
|
.await;
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.insert(video_url, video_items.clone());
|
cache.insert(video_url, video_items.clone());
|
||||||
}
|
}
|
||||||
@@ -234,7 +236,9 @@ impl HqpornerProvider {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
.map_err(|e| Error::from(format!("Request failed: {}", e)))?;
|
||||||
|
|
||||||
let video_items = self.get_video_items_from_html(text, &mut requester, &options).await;
|
let video_items = self
|
||||||
|
.get_video_items_from_html(text, &mut requester, &options)
|
||||||
|
.await;
|
||||||
if !video_items.is_empty() {
|
if !video_items.is_empty() {
|
||||||
cache.insert(video_url, video_items.clone());
|
cache.insert(video_url, video_items.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,7 +356,9 @@ impl JavtifulProvider {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||||
let (tags, formats, views) = self.extract_media(&video_url, &mut requester, options).await?;
|
let (tags, formats, views) = self
|
||||||
|
.extract_media(&video_url, &mut requester, options)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if preview.len() == 0 {
|
if preview.len() == 0 {
|
||||||
preview = format!("https://trailers.jav.si/preview/{id}.mp4");
|
preview = format!("https://trailers.jav.si/preview/{id}.mp4");
|
||||||
|
|||||||
@@ -296,7 +296,11 @@ pub fn strip_url_scheme(url: &str) -> String {
|
|||||||
|
|
||||||
pub fn build_proxy_url(options: &ServerOptions, proxy: &str, target: &str) -> String {
|
pub fn build_proxy_url(options: &ServerOptions, proxy: &str, target: &str) -> String {
|
||||||
let target = target.trim_start_matches('/');
|
let target = target.trim_start_matches('/');
|
||||||
let base = options.public_url_base.as_deref().unwrap_or("").trim_end_matches('/');
|
let base = options
|
||||||
|
.public_url_base
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
if base.is_empty() {
|
if base.is_empty() {
|
||||||
format!("/proxy/{proxy}/{target}")
|
format!("/proxy/{proxy}/{target}")
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ impl NoodlemagazineProvider {
|
|||||||
|
|
||||||
list.split("<div class=\"item\">")
|
list.split("<div class=\"item\">")
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.filter_map(|segment| self.get_video_item(segment.to_string(), proxy_base_url).ok())
|
.filter_map(|segment| {
|
||||||
|
self.get_video_item(segment.to_string(), proxy_base_url)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ impl Porn4fansProvider {
|
|||||||
|
|
||||||
fn sort_by(sort: &str) -> &'static str {
|
fn sort_by(sort: &str) -> &'static str {
|
||||||
match sort {
|
match sort {
|
||||||
|
"popular" => "video_viewed",
|
||||||
_ => "post_date",
|
_ => "post_date",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +137,8 @@ impl Porn4fansProvider {
|
|||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
let video_items = self.get_video_items_from_html(text);
|
let video_items =
|
||||||
|
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());
|
||||||
@@ -193,7 +195,8 @@ impl Porn4fansProvider {
|
|||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
let video_items = self.get_video_items_from_html(text);
|
let video_items =
|
||||||
|
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());
|
||||||
@@ -229,6 +232,20 @@ 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,
|
||||||
@@ -265,7 +282,7 @@ 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) -> Vec<VideoItem> {
|
fn get_video_items_from_html(&self, html: String, proxy_base_url: &str) -> Vec<VideoItem> {
|
||||||
if html.trim().is_empty() {
|
if html.trim().is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
@@ -311,8 +328,14 @@ 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 =
|
let mut item = VideoItem::new(
|
||||||
VideoItem::new(id, title, href, "porn4fans".to_string(), thumb, duration);
|
id,
|
||||||
|
title,
|
||||||
|
self.proxy_url(proxy_base_url, &href),
|
||||||
|
"porn4fans".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
if views > 0 {
|
if views > 0 {
|
||||||
item = item.views(views);
|
item = item.views(views);
|
||||||
}
|
}
|
||||||
@@ -423,12 +446,12 @@ mod tests {
|
|||||||
</div>
|
</div>
|
||||||
"##;
|
"##;
|
||||||
|
|
||||||
let items = provider.get_video_items_from_html(html.to_string());
|
let items = provider.get_video_items_from_html(html.to_string(), "https://example.com");
|
||||||
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].url,
|
||||||
"https://www.porn4fans.com/video/10194/horny-police-officer-melztube-gets-banged-by-bbc/"
|
"https://example.com/proxy/porn4fans/video/10194/horny-police-officer-melztube-gets-banged-by-bbc/"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
items[0].thumb,
|
items[0].thumb,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use ntex::web;
|
use ntex::web;
|
||||||
|
|
||||||
use crate::proxies::noodlemagazine::NoodlemagazineProxy;
|
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,15 +8,16 @@ 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),
|
||||||
Noodlemagazine(NoodlemagazineProxy),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Proxy {
|
pub trait Proxy {
|
||||||
@@ -26,10 +27,10 @@ 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,
|
||||||
AnyProxy::Noodlemagazine(p) => p.get_video_url(url, requester).await,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use ntex::web;
|
use ntex::http::header::CONTENT_TYPE;
|
||||||
|
use ntex::web::{self, HttpRequest, error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use url::Url;
|
||||||
use wreq::Version;
|
use wreq::Version;
|
||||||
|
|
||||||
use crate::util::requester::Requester;
|
use crate::util::requester::Requester;
|
||||||
@@ -47,33 +49,141 @@ impl NoodlemagazineProxy {
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_video_url(
|
fn normalize_video_page_url(url: &str) -> String {
|
||||||
|
if url.starts_with("http://") || url.starts_with("https://") {
|
||||||
|
url.to_string()
|
||||||
|
} else {
|
||||||
|
format!("https://{}", url.trim_start_matches('/'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hls_url(url: &str) -> bool {
|
||||||
|
Url::parse(url)
|
||||||
|
.ok()
|
||||||
|
.map(|parsed| parsed.path().ends_with(".m3u8"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absolutize_uri(base_url: &Url, value: &str) -> String {
|
||||||
|
if value.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.starts_with('#')
|
||||||
|
|| value.starts_with("data:")
|
||||||
|
|| value.starts_with("http://")
|
||||||
|
|| value.starts_with("https://")
|
||||||
|
{
|
||||||
|
return value.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
base_url
|
||||||
|
.join(value)
|
||||||
|
.map(|url| url.to_string())
|
||||||
|
.unwrap_or_else(|_| value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_manifest_line(base_url: &Url, line: &str) -> String {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
return line.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !line.starts_with('#') {
|
||||||
|
return Self::absolutize_uri(base_url, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(uri_start) = line.find("URI=\"") else {
|
||||||
|
return line.to_string();
|
||||||
|
};
|
||||||
|
let value_start = uri_start + 5;
|
||||||
|
let Some(relative_end) = line[value_start..].find('"') else {
|
||||||
|
return line.to_string();
|
||||||
|
};
|
||||||
|
let value_end = value_start + relative_end;
|
||||||
|
let value = &line[value_start..value_end];
|
||||||
|
let rewritten = Self::absolutize_uri(base_url, value);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
&line[..value_start],
|
||||||
|
rewritten,
|
||||||
|
&line[value_end..]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_manifest(manifest_url: &str, body: &str) -> Option<String> {
|
||||||
|
let base_url = Url::parse(manifest_url).ok()?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
body.lines()
|
||||||
|
.map(|line| Self::rewrite_manifest_line(&base_url, line))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_source_url(
|
||||||
&self,
|
&self,
|
||||||
url: String,
|
url: String,
|
||||||
requester: web::types::State<Requester>,
|
requester: web::types::State<Requester>,
|
||||||
) -> String {
|
) -> Option<(String, String)> {
|
||||||
let mut requester = requester.get_ref().clone();
|
let mut requester = requester.get_ref().clone();
|
||||||
let url = if url.starts_with("http://") || url.starts_with("https://") {
|
let url = Self::normalize_video_page_url(&url);
|
||||||
url
|
|
||||||
} else {
|
|
||||||
format!("https://{}", url.trim_start_matches('/'))
|
|
||||||
};
|
|
||||||
let text = requester
|
let text = requester
|
||||||
.get(&url, Some(Version::HTTP_2))
|
.get(&url, Some(Version::HTTP_2))
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
return String::new();
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(playlist) = Self::extract_playlist(&text) else {
|
let Some(playlist) = Self::extract_playlist(&text) else {
|
||||||
return String::new();
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::select_best_source(playlist).unwrap_or_default()
|
Self::select_best_source(playlist).map(|source_url| (url, source_url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn serve_media(
|
||||||
|
req: HttpRequest,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> Result<impl web::Responder, web::Error> {
|
||||||
|
let endpoint = req.match_info().query("endpoint").to_string();
|
||||||
|
let proxy = NoodlemagazineProxy::new();
|
||||||
|
let Some((video_page_url, source_url)) =
|
||||||
|
proxy.resolve_source_url(endpoint, requester.clone()).await
|
||||||
|
else {
|
||||||
|
return Ok(web::HttpResponse::BadGateway().finish());
|
||||||
|
};
|
||||||
|
|
||||||
|
if !NoodlemagazineProxy::is_hls_url(&source_url) {
|
||||||
|
return Ok(web::HttpResponse::Found()
|
||||||
|
.header("Location", source_url)
|
||||||
|
.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut upstream_requester = requester.get_ref().clone();
|
||||||
|
let upstream = match upstream_requester
|
||||||
|
.get_raw_with_headers(&source_url, vec![("Referer".to_string(), video_page_url)])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(_) => return Ok(web::HttpResponse::BadGateway().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let manifest_body = upstream.text().await.map_err(error::ErrorBadGateway)?;
|
||||||
|
let rewritten_manifest =
|
||||||
|
match NoodlemagazineProxy::rewrite_manifest(&source_url, &manifest_body) {
|
||||||
|
Some(body) => body,
|
||||||
|
None => return Ok(web::HttpResponse::BadGateway().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(web::HttpResponse::Ok()
|
||||||
|
.header(CONTENT_TYPE, "application/vnd.apple.mpegurl")
|
||||||
|
.body(rewritten_manifest))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::NoodlemagazineProxy;
|
use super::NoodlemagazineProxy;
|
||||||
@@ -107,4 +217,18 @@ mod tests {
|
|||||||
Some("https://cdn.example/master.m3u8")
|
Some("https://cdn.example/master.m3u8")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrites_manifest_to_direct_absolute_urls() {
|
||||||
|
let manifest = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nlow/index.m3u8\n#EXT-X-KEY:METHOD=AES-128,URI=\"keys/key.bin\"\nsegment0.ts";
|
||||||
|
|
||||||
|
let rewritten =
|
||||||
|
NoodlemagazineProxy::rewrite_manifest("https://cdn.example/hls/master.m3u8", manifest)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rewritten,
|
||||||
|
"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nhttps://cdn.example/hls/low/index.m3u8\n#EXT-X-KEY:METHOD=AES-128,URI=\"https://cdn.example/hls/keys/key.bin\"\nhttps://cdn.example/hls/segment0.ts"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
146
src/proxies/porn4fans.rs
Normal file
146
src/proxies/porn4fans.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
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/"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/proxy.rs
11
src/proxy.rs
@@ -1,7 +1,7 @@
|
|||||||
use ntex::web::{self, HttpRequest};
|
use ntex::web::{self, HttpRequest};
|
||||||
|
|
||||||
use crate::proxies::javtiful::JavtifulProxy;
|
use crate::proxies::javtiful::JavtifulProxy;
|
||||||
use crate::proxies::noodlemagazine::NoodlemagazineProxy;
|
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::*;
|
||||||
@@ -24,10 +24,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
.route(web::get().to(proxy2redirect)),
|
.route(web::get().to(proxy2redirect)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/noodlemagazine/{endpoint}*")
|
web::resource("/porn4fans/{endpoint}*")
|
||||||
.route(web::post().to(proxy2redirect))
|
.route(web::post().to(proxy2redirect))
|
||||||
.route(web::get().to(proxy2redirect)),
|
.route(web::get().to(proxy2redirect)),
|
||||||
)
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/noodlemagazine/{endpoint}*")
|
||||||
|
.route(web::post().to(crate::proxies::noodlemagazine::serve_media))
|
||||||
|
.route(web::get().to(crate::proxies::noodlemagazine::serve_media)),
|
||||||
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/hanime-cdn/{endpoint}*")
|
web::resource("/hanime-cdn/{endpoint}*")
|
||||||
.route(web::post().to(crate::proxies::hanimecdn::get_image))
|
.route(web::post().to(crate::proxies::hanimecdn::get_image))
|
||||||
@@ -57,10 +62,10 @@ 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())),
|
||||||
"noodlemagazine" => Some(AnyProxy::Noodlemagazine(NoodlemagazineProxy::new())),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ pub struct VideoItem {
|
|||||||
pub views: Option<u32>, // 14622653,
|
pub views: Option<u32>, // 14622653,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub rating: Option<f32>, // 0.0,
|
pub rating: Option<f32>, // 0.0,
|
||||||
pub id: String, // "c85017ca87477168d648727753c4ded8a35f173e22ef93743e707b296becb299",
|
pub id: String, // "c85017ca87477168d648727753c4ded8a35f173e22ef93743e707b296becb299",
|
||||||
pub title: String, // "20 Minutes of Adorable Kittens BEST Compilation",
|
pub title: String, // "20 Minutes of Adorable Kittens BEST Compilation",
|
||||||
pub url: String, // "https://www.youtube.com/watch?v=y0sF5xhGreA",
|
pub url: String, // "https://www.youtube.com/watch?v=y0sF5xhGreA",
|
||||||
pub channel: String, // "youtube",
|
pub channel: String, // "youtube",
|
||||||
pub thumb: String, // "https://i.ytimg.com/vi/y0sF5xhGreA/hqdefault.jpg",
|
pub thumb: String, // "https://i.ytimg.com/vi/y0sF5xhGreA/hqdefault.jpg",
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -127,8 +127,8 @@ impl VideoItem {
|
|||||||
VideoItem {
|
VideoItem {
|
||||||
duration: duration, // Placeholder, adjust as needed
|
duration: duration, // Placeholder, adjust as needed
|
||||||
isLive: false,
|
isLive: false,
|
||||||
views: None, // Placeholder, adjust as needed
|
views: None, // Placeholder, adjust as needed
|
||||||
rating: None, // Placeholder, adjust as needed
|
rating: None, // Placeholder, adjust as needed
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
url,
|
url,
|
||||||
|
|||||||
Reference in New Issue
Block a user