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 } }