tube8
This commit is contained in:
173
src/proxies/tube8.rs
Normal file
173
src/proxies/tube8.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user