174 lines
5.9 KiB
Rust
174 lines
5.9 KiB
Rust
use ntex::web;
|
|
|
|
use crate::util::requester::Requester;
|
|
|
|
const BASE_URL: &str = "https://www.tube8.com";
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Tube8Proxy {}
|
|
|
|
impl Tube8Proxy {
|
|
pub fn new() -> Self {
|
|
Tube8Proxy {}
|
|
}
|
|
|
|
fn html_headers() -> Vec<(String, String)> {
|
|
vec![
|
|
(
|
|
"User-Agent".to_string(),
|
|
"Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"
|
|
.to_string(),
|
|
),
|
|
(
|
|
"Accept".to_string(),
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string(),
|
|
),
|
|
("Accept-Language".to_string(), "en-US,en;q=0.5".to_string()),
|
|
("Referer".to_string(), format!("{BASE_URL}/")),
|
|
]
|
|
}
|
|
|
|
fn api_headers(referer: &str) -> Vec<(String, String)> {
|
|
vec![
|
|
(
|
|
"User-Agent".to_string(),
|
|
"Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"
|
|
.to_string(),
|
|
),
|
|
("Accept".to_string(), "application/json, text/javascript, */*; q=0.01".to_string()),
|
|
("Referer".to_string(), referer.to_string()),
|
|
("X-Requested-With".to_string(), "XMLHttpRequest".to_string()),
|
|
]
|
|
}
|
|
|
|
// Extract the first /media/hls/?s=... URL from a video page.
|
|
// The page embeds it as: "videoUrl":"https:\/\/www.tube8.com\/media\/hls\/?s=TOKEN"
|
|
fn extract_hls_endpoint(html: &str) -> Option<String> {
|
|
let needle = r#""format":"hls","videoUrl":""#;
|
|
let start = html.find(needle)? + needle.len();
|
|
let rest = &html[start..];
|
|
let end = rest.find('"')?;
|
|
let raw = &rest[..end];
|
|
// JSON-escaped forward slashes → real URL
|
|
Some(raw.replace(r"\/", "/"))
|
|
}
|
|
|
|
// Parse the JSON quality array returned by /media/hls/?s=...
|
|
// Returns the highest-quality HLS master playlist URL.
|
|
fn best_hls_url(json: &str) -> Option<String> {
|
|
let parsed: serde_json::Value = serde_json::from_str(json).ok()?;
|
|
let arr = parsed.as_array()?;
|
|
|
|
// Prefer highest numeric quality; fall back to defaultQuality
|
|
let mut best_quality: i64 = -1;
|
|
let mut best_url: Option<String> = None;
|
|
let mut default_url: Option<String> = None;
|
|
|
|
for entry in arr {
|
|
let url = entry
|
|
.get("videoUrl")
|
|
.and_then(|v| v.as_str())
|
|
.map(|v| v.replace(r"\/", "/"))
|
|
.filter(|v| !v.is_empty())?;
|
|
|
|
if entry
|
|
.get("defaultQuality")
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false)
|
|
&& default_url.is_none()
|
|
{
|
|
default_url = Some(url.clone());
|
|
}
|
|
|
|
if let Some(q) = entry
|
|
.get("quality")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|v| v.parse::<i64>().ok())
|
|
{
|
|
if q > best_quality {
|
|
best_quality = q;
|
|
best_url = Some(url);
|
|
}
|
|
}
|
|
}
|
|
|
|
best_url.or(default_url)
|
|
}
|
|
|
|
pub async fn get_video_url(
|
|
&self,
|
|
video_id: String,
|
|
requester: web::types::State<Requester>,
|
|
) -> String {
|
|
let video_id = video_id.trim_matches('/').trim();
|
|
if video_id.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let page_url = format!("{BASE_URL}/porn-video/{video_id}/");
|
|
let mut req = requester.get_ref().clone();
|
|
|
|
// Step 1: fetch video page to get the signed /media/hls/ endpoint
|
|
let html = match req
|
|
.get_with_headers(&page_url, Self::html_headers(), None)
|
|
.await
|
|
{
|
|
Ok(v) => v,
|
|
Err(_) => return String::new(),
|
|
};
|
|
|
|
let hls_endpoint = match Self::extract_hls_endpoint(&html) {
|
|
Some(url) => url,
|
|
None => return String::new(),
|
|
};
|
|
|
|
// Step 2: call the signed endpoint to get quality options
|
|
let json = match req
|
|
.get_with_headers(&hls_endpoint, Self::api_headers(&page_url), None)
|
|
.await
|
|
{
|
|
Ok(v) => v,
|
|
Err(_) => return String::new(),
|
|
};
|
|
|
|
Self::best_hls_url(&json).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::Tube8Proxy;
|
|
|
|
#[test]
|
|
fn extracts_hls_endpoint_from_page() {
|
|
let html = r#"
|
|
mediaDefinition: [{"format":"hls","videoUrl":"https:\/\/www.tube8.com\/media\/hls\/?s=eyJTOKEN","remote":true},
|
|
{"format":"mp4","videoUrl":"https:\/\/www.tube8.com\/media\/mp4\/?s=eyJTOKEN","remote":true}],
|
|
"#;
|
|
let url = Tube8Proxy::extract_hls_endpoint(html).expect("should extract");
|
|
assert_eq!(url, "https://www.tube8.com/media/hls/?s=eyJTOKEN");
|
|
}
|
|
|
|
#[test]
|
|
fn picks_best_hls_quality() {
|
|
let json = r#"[
|
|
{"defaultQuality":true,"format":"hls","quality":"480","videoUrl":"https://cdn.example/480/master.m3u8"},
|
|
{"defaultQuality":false,"format":"hls","quality":"720","videoUrl":"https://cdn.example/720/master.m3u8"},
|
|
{"defaultQuality":false,"format":"hls","quality":"1080","videoUrl":"https://cdn.example/1080/master.m3u8"},
|
|
{"defaultQuality":false,"format":"hls","quality":"240","videoUrl":"https://cdn.example/240/master.m3u8"}
|
|
]"#;
|
|
let url = Tube8Proxy::best_hls_url(json).expect("should parse");
|
|
assert_eq!(url, "https://cdn.example/1080/master.m3u8");
|
|
}
|
|
|
|
#[test]
|
|
fn falls_back_to_default_quality_when_no_numeric() {
|
|
let json = r#"[
|
|
{"defaultQuality":true,"format":"hls","videoUrl":"https://cdn.example/default/master.m3u8"},
|
|
{"defaultQuality":false,"format":"hls","videoUrl":"https://cdn.example/other/master.m3u8"}
|
|
]"#;
|
|
let url = Tube8Proxy::best_hls_url(json).expect("should parse");
|
|
assert_eq!(url, "https://cdn.example/default/master.m3u8");
|
|
}
|
|
}
|