sextb
This commit is contained in:
@@ -8,8 +8,6 @@ use crate::util::requester::Requester;
|
||||
pub struct DoodstreamProxy {}
|
||||
|
||||
impl DoodstreamProxy {
|
||||
const ROOT_REFERER: &'static str = "https://turboplayers.xyz/";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
@@ -55,23 +53,23 @@ impl DoodstreamProxy {
|
||||
|| url.path().starts_with("/d/")
|
||||
}
|
||||
|
||||
fn request_origin(detail_url: &str) -> Option<String> {
|
||||
let parsed = Url::parse(detail_url).ok()?;
|
||||
let host = parsed.host_str()?;
|
||||
Some(format!("{}://{}", parsed.scheme(), host))
|
||||
}
|
||||
|
||||
fn request_headers(detail_url: &str) -> Vec<(String, String)> {
|
||||
let origin = Self::request_origin(detail_url).unwrap_or_else(|| "https://turboplayers.xyz".to_string());
|
||||
vec![
|
||||
("Referer".to_string(), Self::ROOT_REFERER.to_string()),
|
||||
("Origin".to_string(), "https://turboplayers.xyz".to_string()),
|
||||
("Referer".to_string(), format!("{}/", origin.trim_end_matches('/'))),
|
||||
("Origin".to_string(), origin),
|
||||
(
|
||||
"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.9".to_string()),
|
||||
(
|
||||
"Sec-Fetch-Site".to_string(),
|
||||
if detail_url.contains("trailerhg.xyz") {
|
||||
"cross-site".to_string()
|
||||
} else {
|
||||
"same-origin".to_string()
|
||||
},
|
||||
),
|
||||
("Sec-Fetch-Site".to_string(), "same-origin".to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -223,6 +221,75 @@ impl DoodstreamProxy {
|
||||
.next()
|
||||
.or_else(|| Self::extract_literal_url(&unpacked))
|
||||
}
|
||||
|
||||
fn extract_pass_md5_url(text: &str, detail_url: &str) -> Option<String> {
|
||||
let decoded = text.replace("\\/", "/");
|
||||
let absolute_regex =
|
||||
Self::regex(r#"https?://[^\s"'<>]+/pass_md5/[^\s"'<>]+"#)?;
|
||||
if let Some(url) = absolute_regex.find(&decoded).map(|value| value.as_str().to_string()) {
|
||||
return Some(url);
|
||||
}
|
||||
|
||||
let relative_regex = Self::regex(r#"/pass_md5/[^\s"'<>]+"#)?;
|
||||
let relative = relative_regex.find(&decoded)?.as_str();
|
||||
let origin = Self::request_origin(detail_url)?;
|
||||
Some(format!("{origin}{relative}"))
|
||||
}
|
||||
|
||||
fn compose_pass_md5_media_url(pass_md5_url: &str, response_body: &str) -> Option<String> {
|
||||
let raw = response_body
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'')
|
||||
.replace("\\/", "/");
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut media_url = if raw.starts_with("https://") || raw.starts_with("http://") {
|
||||
raw
|
||||
} else if let Some(rest) = raw.strip_prefix("//") {
|
||||
format!("https://{rest}")
|
||||
} else {
|
||||
let parsed = Url::parse(pass_md5_url).ok()?;
|
||||
let host = parsed.host_str()?;
|
||||
format!("{}://{}{}", parsed.scheme(), host, raw)
|
||||
};
|
||||
|
||||
let query = Url::parse(pass_md5_url)
|
||||
.ok()
|
||||
.and_then(|url| url.query().map(str::to_string));
|
||||
if let Some(query) = query {
|
||||
if !query.is_empty() && !media_url.contains("token=") {
|
||||
let separator = if media_url.contains('?') { '&' } else { '?' };
|
||||
media_url.push(separator);
|
||||
media_url.push_str(&query);
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self::sanitize_media_url(&media_url))
|
||||
}
|
||||
|
||||
async fn resolve_stream_from_pass_md5(
|
||||
detail_url: &str,
|
||||
html: &str,
|
||||
requester: &mut Requester,
|
||||
) -> Option<String> {
|
||||
let pass_md5_url = Self::extract_pass_md5_url(html, detail_url).or_else(|| {
|
||||
Self::unpack_packer(html).and_then(|unpacked| Self::extract_pass_md5_url(&unpacked, detail_url))
|
||||
})?;
|
||||
|
||||
let headers = vec![
|
||||
("Referer".to_string(), detail_url.to_string()),
|
||||
("X-Requested-With".to_string(), "XMLHttpRequest".to_string()),
|
||||
("Accept".to_string(), "*/*".to_string()),
|
||||
];
|
||||
let response = requester
|
||||
.get_with_headers(&pass_md5_url, headers, None)
|
||||
.await
|
||||
.ok()?;
|
||||
Self::compose_pass_md5_media_url(&pass_md5_url, &response)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::proxies::Proxy for DoodstreamProxy {
|
||||
@@ -240,7 +307,15 @@ impl crate::proxies::Proxy for DoodstreamProxy {
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
|
||||
Self::extract_stream_url(&html).unwrap_or_default()
|
||||
if let Some(url) = Self::extract_stream_url(&html) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if let Some(url) = Self::resolve_stream_from_pass_md5(&detail_url, &html, &mut requester).await {
|
||||
return url;
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,4 +367,30 @@ mod tests {
|
||||
Some("https://cdn.example/master.m3u8?t=1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composes_media_url_from_pass_md5_response() {
|
||||
let pass_md5_url = "https://trailerhg.xyz/pass_md5/abc123/def456?token=t0k3n&expiry=1775000000";
|
||||
let body = "https://g4vsrqvtrj.pinebrookproductionlab.shop/1ghkpx2e8jnal/hls3/01/08534/syyzvotfnhaa_l/master.txt";
|
||||
assert_eq!(
|
||||
DoodstreamProxy::compose_pass_md5_media_url(pass_md5_url, body).as_deref(),
|
||||
Some(
|
||||
"https://g4vsrqvtrj.pinebrookproductionlab.shop/1ghkpx2e8jnal/hls3/01/08534/syyzvotfnhaa_l/master.txt?token=t0k3n&expiry=1775000000"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_relative_pass_md5_url() {
|
||||
let html = r#"
|
||||
<script>
|
||||
var file = "/pass_md5/abc123/def456?token=t0k3n&expiry=1775000000";
|
||||
</script>
|
||||
"#;
|
||||
assert_eq!(
|
||||
DoodstreamProxy::extract_pass_md5_url(html, "https://trailerhg.xyz/e/ttdc7a6qpskt")
|
||||
.as_deref(),
|
||||
Some("https://trailerhg.xyz/pass_md5/abc123/def456?token=t0k3n&expiry=1775000000")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user