sextb
This commit is contained in:
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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