From 772835d4d1179dac4e01e6848644a5e4df5abf14 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Apr 2026 06:23:34 +0000 Subject: [PATCH] vjav proxy --- src/proxies/mod.rs | 4 + src/proxies/vjav.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++ src/proxy.rs | 7 ++ 3 files changed, 198 insertions(+) create mode 100644 src/proxies/vjav.rs diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs index 8b81bdc..1fe4adb 100644 --- a/src/proxies/mod.rs +++ b/src/proxies/mod.rs @@ -8,6 +8,7 @@ use crate::proxies::pimpbunny::PimpbunnyProxy; use crate::proxies::porndish::PorndishProxy; use crate::proxies::shooshtime::ShooshtimeProxy; use crate::proxies::spankbang::SpankbangProxy; +use crate::proxies::vjav::VjavProxy; use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester}; pub mod doodstream; @@ -26,6 +27,7 @@ pub mod pornhubthumb; pub mod shooshtime; pub mod spankbang; pub mod sxyprn; +pub mod vjav; #[derive(Debug, Clone)] pub enum AnyProxy { @@ -39,6 +41,7 @@ pub enum AnyProxy { Shooshtime(ShooshtimeProxy), Hqporner(HqpornerProxy), Heavyfetish(HeavyfetishProxy), + Vjav(VjavProxy), } pub trait Proxy { @@ -58,6 +61,7 @@ impl Proxy for AnyProxy { AnyProxy::Shooshtime(p) => p.get_video_url(url, requester).await, AnyProxy::Hqporner(p) => p.get_video_url(url, requester).await, AnyProxy::Heavyfetish(p) => p.get_video_url(url, requester).await, + AnyProxy::Vjav(p) => p.get_video_url(url, requester).await, } } } diff --git a/src/proxies/vjav.rs b/src/proxies/vjav.rs new file mode 100644 index 0000000..cf8e69c --- /dev/null +++ b/src/proxies/vjav.rs @@ -0,0 +1,187 @@ +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use ntex::web; +use serde::Deserialize; +use url::Url; + +use crate::util::requester::Requester; + +const BASE_URL: &str = "https://vjav.com"; + +#[derive(Debug, Clone)] +pub struct VjavProxy {} + +#[derive(Debug, Deserialize, Clone, Default)] +struct VideofileEntry { + #[serde(default)] + video_url: String, + #[serde(default)] + is_default: i32, +} + +impl VjavProxy { + pub fn new() -> Self { + Self {} + } + + fn normalize_detail_url(endpoint: &str) -> Option { + 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 { + format!("https://{}", endpoint.trim_start_matches('/')) + }; + + Self::is_allowed_detail_url(&detail_url).then_some(detail_url) + } + + 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; + }; + + if host != "vjav.com" && host != "www.vjav.com" { + return false; + } + + let Some(video_id) = Self::extract_video_id(parsed.path()) else { + return false; + }; + + !video_id.is_empty() + } + + fn extract_video_id(path: &str) -> Option { + let mut segments = path.split('/').filter(|segment| !segment.is_empty()); + let first = segments.next()?; + let second = segments.next()?; + + if first != "videos" { + return None; + } + + second + .chars() + .all(|value| value.is_ascii_digit()) + .then_some(second.to_string()) + } + + fn decode_obfuscated_base64(value: &str) -> String { + value + .chars() + .map(|character| match character { + 'А' => 'A', + 'В' => 'B', + 'Е' => 'E', + 'К' => 'K', + 'М' => 'M', + 'Н' => 'H', + 'О' => 'O', + 'Р' => 'P', + 'С' => 'C', + 'Т' => 'T', + 'Х' => 'X', + 'а' => 'a', + 'е' => 'e', + 'о' => 'o', + 'р' => 'p', + 'с' => 'c', + 'у' => 'y', + 'х' => 'x', + 'к' => 'k', + 'м' => 'm', + 'і' => 'i', + 'І' => 'I', + _ => character, + }) + .collect() + } + + fn decode_base64ish(value: &str) -> Option { + let mut normalized = value.trim().replace('~', "="); + while normalized.len() % 4 != 0 { + normalized.push('='); + } + let bytes = STANDARD.decode(normalized).ok()?; + String::from_utf8(bytes).ok() + } + + fn decode_video_url(value: &str) -> Option { + let normalized = Self::decode_obfuscated_base64(value); + if normalized.contains(',') { + let mut parts = normalized.split(','); + let path_part = parts.next()?; + let query_part = parts.next()?; + + let path = Self::decode_base64ish(path_part)?; + let query = Self::decode_base64ish(query_part)?; + let separator = if path.contains('?') { "&" } else { "?" }; + return Some(format!("{BASE_URL}{path}{separator}{query}&f=video.m3u8")); + } + + let decoded = Self::decode_base64ish(&normalized)?; + if decoded.starts_with("http://") || decoded.starts_with("https://") { + return Some(decoded); + } + if decoded.starts_with('/') { + return Some(format!("{BASE_URL}{decoded}")); + } + None + } +} + +impl crate::proxies::Proxy for VjavProxy { + async fn get_video_url(&self, url: String, requester: web::types::State) -> String { + let Some(detail_url) = Self::normalize_detail_url(&url) else { + return String::new(); + }; + + let Some(video_id) = Url::parse(&detail_url) + .ok() + .and_then(|value| Self::extract_video_id(value.path())) + else { + return String::new(); + }; + + let api_url = format!("{BASE_URL}/api/videofile.php?video_id={video_id}&lifetime=8640000"); + + let mut requester = requester.get_ref().clone(); + let text = requester.get(&api_url, None).await.unwrap_or_default(); + if text.is_empty() { + return String::new(); + } + + let Ok(entries) = serde_json::from_str::>(&text) else { + return String::new(); + }; + + let mut fallback = String::new(); + for entry in entries { + if entry.video_url.trim().is_empty() { + continue; + } + let Some(decoded) = Self::decode_video_url(&entry.video_url) else { + continue; + }; + if entry.is_default == 1 { + return decoded; + } + if fallback.is_empty() { + fallback = decoded; + } + } + + fallback + } +} diff --git a/src/proxy.rs b/src/proxy.rs index af0cdf5..f5a9113 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -10,6 +10,7 @@ use crate::proxies::pornhd3x::Pornhd3xProxy; use crate::proxies::shooshtime::ShooshtimeProxy; use crate::proxies::spankbang::SpankbangProxy; use crate::proxies::sxyprn::SxyprnProxy; +use crate::proxies::vjav::VjavProxy; use crate::proxies::*; use crate::util::requester::Requester; @@ -49,6 +50,11 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route(web::post().to(proxy2redirect)) .route(web::get().to(proxy2redirect)), ) + .service( + web::resource("/vjav/{endpoint}*") + .route(web::post().to(proxy2redirect)) + .route(web::get().to(proxy2redirect)), + ) .service( web::resource("/pornhd3x/{endpoint}*") .route(web::post().to(proxy2redirect)) @@ -128,6 +134,7 @@ fn get_proxy(proxy: &str) -> Option { "javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())), "hqporner" => Some(AnyProxy::Hqporner(HqpornerProxy::new())), "heavyfetish" => Some(AnyProxy::Heavyfetish(HeavyfetishProxy::new())), + "vjav" => Some(AnyProxy::Vjav(VjavProxy::new())), "pornhd3x" => Some(AnyProxy::Pornhd3x(Pornhd3xProxy::new())), "shooshtime" => Some(AnyProxy::Shooshtime(ShooshtimeProxy::new())), "pimpbunny" => Some(AnyProxy::Pimpbunny(PimpbunnyProxy::new())),