This commit is contained in:
Simon
2026-03-30 19:21:42 +00:00
parent 429fb16fbd
commit 01831c70e7
2 changed files with 217 additions and 22 deletions

View File

@@ -910,12 +910,29 @@ impl SextbProvider {
fn parse_player_episodes(html: &str) -> Result<Vec<PlayerEpisode>> {
let regex = Self::regex(
r#"(?is)<button[^>]*class="[^"]*\bbtn-player\b[^"]*"[^>]*data-source="(?P<film>\d+)"[^>]*data-id="(?P<episode>\d+)"[^>]*>.*?</i>\s*(?P<label>[A-Z]{2,3})\s*</button>"#,
r#"(?is)<(?:button|a)[^>]*data-source="(?P<film>\d+)"[^>]*data-id="(?P<episode>\d+)"[^>]*>(?P<body>.*?)</(?:button|a)>"#,
)?;
let body_text_regex = Self::regex(r#"(?is)<[^>]+>"#)?;
let mut episodes = regex
.captures_iter(html)
.filter_map(|captures| {
let label = captures.name("label")?.as_str().trim().to_string();
let body = captures.name("body")?.as_str();
let body_text = body_text_regex.replace_all(body, " ");
let body_upper = body_text.to_ascii_uppercase();
let label = ["DD", "TB", "SW", "FL", "US", "PP"]
.iter()
.find(|candidate| body_upper.contains(**candidate))
.map(|candidate| (*candidate).to_string())
.or_else(|| {
body_upper
.split_whitespace()
.find(|token| token.len() >= 2 && token.len() <= 3)
.map(|token| token.to_string())
})
.unwrap_or_default();
if label.is_empty() {
return None;
}
let film_id = captures.name("film")?.as_str().trim().to_string();
let episode_id = captures.name("episode")?.as_str().trim().to_string();
Some(PlayerEpisode {
@@ -938,19 +955,54 @@ impl SextbProvider {
Ok(episodes)
}
fn normalize_iframe_url(url: &str) -> Option<String> {
let trimmed = url.trim();
if let Some(without) = trimmed.strip_prefix("//") {
return Some(format!("https://{without}"));
}
if trimmed.starts_with("https://") || trimmed.starts_with("http://") {
return Some(trimmed.to_string());
}
None
}
fn extract_hoster_url_from_text(text: &str) -> Option<String> {
let regex = Regex::new(r#"https?://[^\s"'<>\\]+"#).ok()?;
regex.find_iter(text).find_map(|match_value| {
let candidate = match_value.as_str().trim();
proxy_name_for_url(candidate).map(|_| candidate.to_string())
})
}
fn parse_player_iframe_url(response: &str) -> Option<String> {
let regex = Regex::new(r#"(?is)<iframe[^>]+src="(?P<url>https://[^"]+)""#).ok()?;
let iframe_regex =
Regex::new(r#"(?is)<iframe[^>]+src=['"](?P<url>(?:https?:)?//[^'"]+)['"]"#).ok()?;
let src_json_regex = Regex::new(r#"(?i)"src"\s*:\s*"(?P<url>https?:\\/\\/[^"]+)""#).ok()?;
let player_html = serde_json::from_str::<serde_json::Value>(response)
.ok()
.and_then(|value| value.get("player").and_then(|player| player.as_str()).map(str::to_string));
player_html
let from_iframe = player_html
.as_deref()
.and_then(|html| regex.captures(html))
.or_else(|| regex.captures(response))
.and_then(|html| iframe_regex.captures(html))
.or_else(|| iframe_regex.captures(response))
.and_then(|captures| captures.name("url"))
.map(|value| value.as_str().trim().to_string())
.and_then(|value| Self::normalize_iframe_url(value.as_str()));
if from_iframe.is_some() {
return from_iframe;
}
if let Some(url) = src_json_regex
.captures(response)
.and_then(|captures| captures.name("url"))
.map(|value| value.as_str().replace("\\/", "/"))
{
return Self::normalize_iframe_url(&url);
}
Self::extract_hoster_url_from_text(response)
}
async fn resolve_proxy_video_url(
@@ -960,6 +1012,9 @@ impl SextbProvider {
) -> Option<String> {
let mut requester = requester_or_default(options, CHANNEL_ID, "resolve_proxy_video_url");
let html = requester.get(seed_url, Some(Version::HTTP_2)).await.ok()?;
if let Some(url) = Self::extract_hoster_url_from_text(&html) {
return Some(rewrite_hoster_url(options, &url));
}
let episodes = Self::parse_player_episodes(&html).ok()?;
for episode in episodes {
@@ -967,10 +1022,21 @@ impl SextbProvider {
"{}/ajax/player?episode={}&filmId={}",
self.url, episode.episode_id, episode.film_id
);
let Some(response) = requester.get(&ajax_url, Some(Version::HTTP_2)).await.ok() else {
let headers = vec![
("Referer".to_string(), seed_url.to_string()),
("X-Requested-With".to_string(), "XMLHttpRequest".to_string()),
("Accept".to_string(), "application/json, text/html, */*".to_string()),
];
let Some(response) = requester
.get_with_headers(&ajax_url, headers, Some(Version::HTTP_2))
.await
.ok()
else {
continue;
};
let Some(iframe_url) = Self::parse_player_iframe_url(&response) else {
let iframe_or_hoster = Self::parse_player_iframe_url(&response)
.or_else(|| Self::extract_hoster_url_from_text(&response));
let Some(iframe_url) = iframe_or_hoster else {
continue;
};
if proxy_name_for_url(&iframe_url).is_some() {
@@ -1325,6 +1391,25 @@ mod tests {
);
}
#[test]
fn parses_dd_player_anchor_layout() {
let html = r#"
<div class="episode-list">
<a class="episode" data-source="200" data-id="301"><span>SW</span></a>
<a class="episode active" data-source="200" data-id="302"><span>DD</span></a>
</div>
"#;
let episodes = SextbProvider::parse_player_episodes(html).expect("episodes should parse");
assert_eq!(
episodes.first(),
Some(&PlayerEpisode {
label: "DD".to_string(),
film_id: "200".to_string(),
episode_id: "302".to_string(),
})
);
}
#[test]
fn parses_iframe_url_from_ajax_player_response() {
let response = r#"{"player":"<iframe src=\"https://trailerhg.xyz/e/ttdc7a6qpskt\" width=\"100%\" height=\"100%\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>"}"#;
@@ -1333,4 +1418,13 @@ mod tests {
Some("https://trailerhg.xyz/e/ttdc7a6qpskt")
);
}
#[test]
fn parses_protocol_relative_iframe_url_from_ajax_player_response() {
let response = r#"{"player":"<iframe src=\"//turboplayers.xyz/t/69bdfb21cc640\"></iframe>"}"#;
assert_eq!(
SextbProvider::parse_player_iframe_url(response).as_deref(),
Some("https://turboplayers.xyz/t/69bdfb21cc640")
);
}
}

View File

@@ -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")
);
}
}