From 5e5786010a65136f1a2e0d8ebb05d6f67eac9704 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 6 May 2026 11:17:25 +0000 Subject: [PATCH] doodstream and lulustream in sxyprn integrated Co-authored-by: Copilot --- src/providers/sxyprn.rs | 94 ++++++++++++++++++++++++++--------- src/proxies/lulustream.rs | 100 ++++++++++++++++++++++++++++++++++++++ src/proxies/mod.rs | 4 ++ src/proxies/sxyprn.rs | 12 ++--- src/util/hoster_proxy.rs | 28 ++++++++--- src/util/requester.rs | 2 +- src/videos.rs | 45 +++++++++++------ 7 files changed, 234 insertions(+), 51 deletions(-) create mode 100644 src/proxies/lulustream.rs diff --git a/src/providers/sxyprn.rs b/src/providers/sxyprn.rs index 32bb822..9038770 100644 --- a/src/providers/sxyprn.rs +++ b/src/providers/sxyprn.rs @@ -7,6 +7,7 @@ use crate::util::discord::format_error_chain; use crate::util::discord::send_discord_error_report; use crate::util::requester::Requester; use crate::util::time::parse_time_to_seconds; +use crate::util::hoster_proxy::{proxy_name_for_url, rewrite_hoster_url}; use crate::videos::ServerOptions; use crate::videos::VideoFormat; use crate::videos::VideoItem; @@ -332,6 +333,13 @@ impl SxyprnProvider { .and_then(|s| s.split("").next()) .ok_or_else(|| ErrorKind::Parse("failed to extract title_parts".into()))?; + let title_links: Vec = video_segment + .split("href='https://") + .skip(1) + .filter_map(|part| part.split("'").next().map(|u| u.to_string())) + .collect(); + + let document = Html::parse_document(title_parts); let selector = Selector::parse("*") .map_err(|e| ErrorKind::Parse(format!("selector parse failed: {e}")))?; @@ -358,10 +366,36 @@ impl SxyprnProvider { .trim() .to_string(); + // De-duplicate repeated titles + let words: Vec<&str> = title.split_whitespace().collect(); + if words.len() > 1 { + for pattern_len in (1..=words.len() / 2).rev() { + let pattern = &words[0..pattern_len]; + let mut all_match = true; + let mut idx = pattern_len; + + while idx < words.len() { + let end = std::cmp::min(idx + pattern_len, words.len()); + if &words[idx..end] != &pattern[0..(end - idx)] { + all_match = false; + break; + } + idx += pattern_len; + } + + if all_match && words.len() % pattern_len == 0 { + title = pattern.join(" "); + break; + } + } + } + if title.to_ascii_lowercase().starts_with("new ") { title = title[4..].to_string(); } + println!("{:?}", title_links); + // Extract tags from title (words starting with #) let mut tags = Vec::new(); let words: Vec<&str> = title.split_whitespace().collect(); @@ -401,7 +435,7 @@ impl SxyprnProvider { .nth(1) .and_then(|s| s.split("data-src='").nth(1)) .and_then(|s| s.split('\'').next()) - .ok_or_else(|| ErrorKind::Parse("failed to extract thumb".into()))?; + .unwrap_or(""); let thumb = format!("https:{thumb_path}"); @@ -462,29 +496,15 @@ impl SxyprnProvider { ); // Also collect and transform vidara.so URLs to proxy format and add as formats - let vidara_urls: Vec = video_segment - .split("extlink_icon extlink") - .filter_map(|part| { - part.split("href='") - .last() - .and_then(|s| s.split('\'').next()) - .map(|u| u.to_string()) - }) - .filter(|url| url.contains("vidara.so/v/")) - .filter_map(|url| { - url.split("/v/").last().map(|video_id| { - format!( - "{}/proxy/vidara/e/{}", - options.public_url_base.as_deref().unwrap_or(""), - video_id - ) - }) - }) + let vidara_urls: Vec = title_links + .iter() + .filter(|url| proxy_name_for_url(url).as_deref() == Some("vidara")) + .map(|url| rewrite_hoster_url(options, url)) .collect(); for vidara_url in vidara_urls { formats.push( - VideoFormat::new(vidara_url.clone(), "1080".to_string(), "m3u8".to_string()) + VideoFormat::m3u8(vidara_url.clone(), "1080".to_string(), "m3u8".to_string()) .format_note( vidara_url .split("/") @@ -492,11 +512,39 @@ impl SxyprnProvider { .unwrap_or("vidara") .to_string(), ) - .ext("m3u8".to_string()) - .format_id("hls".to_string()) - .video_ext("m3u8".to_string()), + .format_id("vidara".to_string()), ); } + + let doodstream_urls: Vec = title_links + .iter() + .filter(|url| proxy_name_for_url(url).as_deref() == Some("doodstream")) + .map(|url| rewrite_hoster_url(options, url)) + .collect(); + + for dood_url in doodstream_urls { + formats.push( + VideoFormat::m3u8(dood_url.clone(), "auto".to_string(), "m3u8".to_string()) + .format_note("doodstream".to_string()) + .format_id("doodstream".to_string()), + ); + } + + let lulustream_urls: Vec = title_links + .iter() + .filter(|url| proxy_name_for_url(url).as_deref() == Some("lulustream")) + .map(|url| rewrite_hoster_url(options, url)) + .collect(); + + for lulustream_url in lulustream_urls { + formats.push( + VideoFormat::m3u8(lulustream_url.clone(), "auto".to_string(), "m3u8".to_string()) + .format_note("lulustream".to_string()) + .format_id("lulustream".to_string()), + ); + } + + let mut video_item = VideoItem::new( id.clone(), title, diff --git a/src/proxies/lulustream.rs b/src/proxies/lulustream.rs new file mode 100644 index 0000000..8420372 --- /dev/null +++ b/src/proxies/lulustream.rs @@ -0,0 +1,100 @@ +use ntex::web; +use url::Url; +use serde_json::json; + +use crate::util::requester::Requester; + +#[derive(Debug, Clone)] +pub struct LulustreamProxy {} + +impl LulustreamProxy { + pub fn new() -> Self { + LulustreamProxy {} + } + + fn normalize_detail_request(endpoint: &str) -> Option<(String, String)> { + let endpoint = endpoint.trim().trim_start_matches('/'); + if endpoint.is_empty() { + return None; + } + + let detail_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + endpoint.to_string() + } else if endpoint.starts_with("lulustream.com/") || endpoint.starts_with("www.lulustream.com/") || + endpoint.starts_with("luluvdo.com/") + { + format!("https://{endpoint}") + } else { + format!("https://lulustream.com/{endpoint}") + }; + + if !Self::is_allowed_detail_url(&detail_url) { + return None; + } + + let parsed = Url::parse(&detail_url).ok()?; + let video_id = parsed.path_segments()? + .last() + .map(ToOwned::to_owned)?; + + Some((detail_url, video_id)) + } + + fn is_allowed_detail_url(url: &str) -> bool { + let Some(parsed) = Url::parse(url).ok() else { + return false; + }; + if parsed.scheme() != "https" { + return false; + } + let Some(host) = parsed.host_str() else { + return false; + }; + (host == "lulustream.com" || host == "www.lulustream.com" || host == "luluvdo.com") + && (parsed.path().starts_with("/v/")||parsed.path().starts_with("/e/")) + } + + pub async fn get_video_url( + &self, + url: String, + requester: web::types::State, + ) -> String { + let mut requester = requester.get_ref().clone(); + let Some((detail_url, video_id)) = Self::normalize_detail_request(&url) else { + println!("LulustreamProxy: Invalid detail URL: {url}"); + return String::new(); + }; + let text = requester.get(&detail_url, None).await.unwrap_or_default(); + let video_url = text.split("sources: [{file:\"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or_default() + .to_string(); + if video_url.is_empty() { + println!("LulustreamProxy: Failed to extract video URL for video ID: {video_id}"); + } + video_url + } +} + +#[cfg(test)] +mod tests { + use super::LulustreamProxy; + + #[test] + fn normalizes_detail_request_with_full_url() { + let (url, video_id) = + LulustreamProxy::normalize_detail_request("https://lulustream.com/d/s484n23k8opy") + .expect("detail request should parse"); + assert_eq!(url, "https://lulustream.com/d/s484n23k8opy"); + assert_eq!(video_id, "s484n23k8opy"); + } + + #[test] + fn normalizes_detail_request_with_path_only() { + let (url, video_id) = LulustreamProxy::normalize_detail_request("d/s484n23k8opy") + .expect("detail request should parse"); + assert_eq!(url, "https://lulustream.com/d/s484n23k8opy"); + assert_eq!(video_id, "s484n23k8opy"); + } +} diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs index dc891ec..2ae79f5 100644 --- a/src/proxies/mod.rs +++ b/src/proxies/mod.rs @@ -12,6 +12,7 @@ use crate::proxies::spankbang::SpankbangProxy; use crate::proxies::vjav::VjavProxy; use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester}; use crate::proxies::vidara::VidaraProxy; +use crate::proxies::lulustream::LulustreamProxy; pub mod archivebate; pub mod doodstream; @@ -20,6 +21,7 @@ pub mod heavyfetish; pub mod hqporner; pub mod hqpornerthumb; pub mod javtiful; +pub mod lulustream; pub mod noodlemagazine; pub mod pimpbunny; pub mod porndish; @@ -38,6 +40,7 @@ pub enum AnyProxy { Doodstream(DoodstreamProxy), Sxyprn(SxyprnProxy), Javtiful(javtiful::JavtifulProxy), + Lulustream(LulustreamProxy), Pornhd3x(Pornhd3xProxy), Pimpbunny(PimpbunnyProxy), Porndish(PorndishProxy), @@ -60,6 +63,7 @@ impl Proxy for AnyProxy { AnyProxy::Doodstream(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::Lulustream(p) => p.get_video_url(url, requester).await, AnyProxy::Pornhd3x(p) => p.get_video_url(url, requester).await, AnyProxy::Pimpbunny(p) => p.get_video_url(url, requester).await, AnyProxy::Porndish(p) => p.get_video_url(url, requester).await, diff --git a/src/proxies/sxyprn.rs b/src/proxies/sxyprn.rs index cb76ab8..ce98671 100644 --- a/src/proxies/sxyprn.rs +++ b/src/proxies/sxyprn.rs @@ -38,7 +38,7 @@ impl SxyprnProxy { ) -> String { let mut requester = requester.get_ref().clone(); let url = "https://sxyprn.com/".to_string() + &url; - println!("Fetching URL: {}", url); + // println!("Fetching URL: {}", url); let text = requester.get(&url, None).await.unwrap_or("".to_string()); if text.is_empty() { return "".to_string(); @@ -49,27 +49,27 @@ impl SxyprnProxy { .split("\"}") .collect::>()[0] .replace("\\", ""); - println!("src: {}", data_string); + // println!("src: {}", data_string); let mut tmp = data_string .split("/") .map(|s| s.to_string()) .collect::>(); - println!("tmp: {:?}", tmp); + // println!("tmp: {:?}", tmp); tmp[1] = format!( "{}8/{}", tmp[1], boo(ssut51(tmp[6].as_str()), ssut51(tmp[7].as_str())) ); - println!("tmp[1]: {:?}", tmp[1]); + // println!("tmp[1]: {:?}", tmp[1]); //preda tmp[5] = format!( "{}", tmp[5].parse::().unwrap() - ssut51(tmp[6].as_str()) - ssut51(tmp[7].as_str()) ); - println!("tmp: {:?}", tmp); + // println!("tmp: {:?}", tmp); let sxyprn_video_url = format!("https://sxyprn.com{}", tmp.join("/")); - println!("sxyprn_video_url: {}", sxyprn_video_url); + // println!("sxyprn_video_url: {}", sxyprn_video_url); match crate::util::get_redirect_location(&sxyprn_video_url) { Ok(Some(loc)) => {return format!("https:{}", loc)}, Ok(None) => println!("No redirect found for {}", sxyprn_video_url), diff --git a/src/util/hoster_proxy.rs b/src/util/hoster_proxy.rs index b04a121..a384a9e 100644 --- a/src/util/hoster_proxy.rs +++ b/src/util/hoster_proxy.rs @@ -3,25 +3,41 @@ use url::Url; use crate::providers::{build_proxy_url, strip_url_scheme}; use crate::videos::ServerOptions; -#[allow(dead_code)] const DOODSTREAM_HOSTS: &[&str] = &[ + "doodstream.com", "turboplayers.xyz", - "www.turboplayers.xyz", "trailerhg.xyz", - "www.trailerhg.xyz", "streamhg.com", - "www.streamhg.com", +]; + +const LULUSTREAM_HOSTS: &[&str] = &[ + "luluvdo.com", + "lulustream.com", +]; + +const VIDARA_HOSTS: &[&str] = &[ + "vidara.so", ]; #[allow(dead_code)] pub fn proxy_name_for_url(url: &str) -> Option<&'static str> { - let parsed = Url::parse(url).ok()?; + let parsed = match !url.starts_with("http://") && !url.starts_with("https://"){ + true => Url::parse(&format!("https://{}", url)).ok()?, + false => Url::parse(url).ok()? + }; let host = parsed.host_str()?.to_ascii_lowercase(); - if DOODSTREAM_HOSTS.contains(&host.as_str()) { return Some("doodstream"); } + if LULUSTREAM_HOSTS.contains(&host.as_str()) { + return Some("lulustream"); + } + + if VIDARA_HOSTS.contains(&host.as_str()) { + return Some("vidara"); + } + None } diff --git a/src/util/requester.rs b/src/util/requester.rs index 44efc78..be8fe0a 100644 --- a/src/util/requester.rs +++ b/src/util/requester.rs @@ -679,7 +679,7 @@ mod tests { let origin = "https://shared-cookie-requester-test.invalid/"; a.cookie_jar - .add_cookie_str("shared_cookie=1; Path=/; SameSite=Lax", origin); + .add_cookie_str("shared_cookie=1; Path=/; SameSite=Lax", &url::Url::parse(origin).unwrap()); let cookie_header = b .cookie_header_for_url("https://shared-cookie-requester-test.invalid/path") diff --git a/src/videos.rs b/src/videos.rs index 5aa0e02..1e1770a 100644 --- a/src/videos.rs +++ b/src/videos.rs @@ -282,13 +282,36 @@ impl VideoFormat { http_headers: None, } } - #[cfg(any( - not(hottub_single_provider), - hottub_provider = "vrporn", - hottub_provider = "perverzija", - hottub_provider = "porndish", - hottub_provider = "spankbang", - ))] + pub fn m3u8(url: String, quality: String, format: String) -> Self { + let _ = format; + VideoFormat { + url, + quality, + format: format, // Default format + format_id: Some("m3u8-1080".to_string()), + format_note: None, + filesize: None, + asr: None, + fps: None, + width: None, + height: None, + tbr: None, + language: None, + language_preference: None, + ext: Some("m3u8".to_string()), + vcodec: None, + acodec: None, + dynamic_range: None, + abr: None, + vbr: None, + container: None, + protocol: Some("m3u8_native".to_string()), + audio_ext: Some("none".to_string()), + video_ext: Some("m3u8".to_string()), + resolution: None, + http_headers: None, + } + } pub fn add_http_header(&mut self, key: String, value: String) { if self.http_headers.is_none() { self.http_headers = Some(HashMap::new()); @@ -297,14 +320,6 @@ impl VideoFormat { headers.insert(key, value); } } - #[cfg(any( - not(hottub_single_provider), - hottub_provider = "hentaihaven", - hottub_provider = "noodlemagazine", - hottub_provider = "shooshtime", - hottub_provider = "heavyfetish", - hottub_provider = "hsex", - ))] pub fn http_header(&mut self, key: String, value: String) -> Self { if self.http_headers.is_none() { self.http_headers = Some(HashMap::new());