Files
hottub/src/proxies/vjav.rs
2026-04-06 06:23:34 +00:00

188 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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 {
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<String> {
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<String> {
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<String> {
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<Requester>) -> 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::<Vec<VideofileEntry>>(&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
}
}