188 lines
5.3 KiB
Rust
188 lines
5.3 KiB
Rust
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
|
||
}
|
||
}
|