fixes
This commit is contained in:
5
build.rs
5
build.rs
@@ -89,11 +89,6 @@ const PROVIDERS: &[ProviderDef] = &[
|
|||||||
module: "yesporn",
|
module: "yesporn",
|
||||||
ty: "YespornProvider",
|
ty: "YespornProvider",
|
||||||
},
|
},
|
||||||
ProviderDef {
|
|
||||||
id: "arabpornxxx",
|
|
||||||
module: "arabpornxxx",
|
|
||||||
ty: "ArabpornxxxProvider",
|
|
||||||
},
|
|
||||||
ProviderDef {
|
ProviderDef {
|
||||||
id: "sxyprn",
|
id: "sxyprn",
|
||||||
module: "sxyprn",
|
module: "sxyprn",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ use crate::providers::{
|
|||||||
};
|
};
|
||||||
use crate::status::*;
|
use crate::status::*;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
|
use crate::util::hoster_proxy::{proxy_name_for_url, rewrite_hoster_url};
|
||||||
use crate::util::parse_abbreviated_number;
|
use crate::util::parse_abbreviated_number;
|
||||||
use crate::util::requester::Requester;
|
use crate::util::requester::Requester;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoItem};
|
||||||
@@ -97,6 +98,13 @@ struct DetailMetadata {
|
|||||||
rating: Option<f32>,
|
rating: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct PlayerEpisode {
|
||||||
|
label: String,
|
||||||
|
film_id: String,
|
||||||
|
episode_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl SextbProvider {
|
impl SextbProvider {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let provider = Self {
|
let provider = Self {
|
||||||
@@ -900,6 +908,79 @@ impl SextbProvider {
|
|||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>"#,
|
||||||
|
)?;
|
||||||
|
let mut episodes = regex
|
||||||
|
.captures_iter(html)
|
||||||
|
.filter_map(|captures| {
|
||||||
|
let label = captures.name("label")?.as_str().trim().to_string();
|
||||||
|
let film_id = captures.name("film")?.as_str().trim().to_string();
|
||||||
|
let episode_id = captures.name("episode")?.as_str().trim().to_string();
|
||||||
|
Some(PlayerEpisode {
|
||||||
|
label,
|
||||||
|
film_id,
|
||||||
|
episode_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
episodes.sort_by_key(|episode| {
|
||||||
|
if episode.label == "DD" {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
episodes.dedup();
|
||||||
|
|
||||||
|
Ok(episodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_player_iframe_url(response: &str) -> Option<String> {
|
||||||
|
let regex = Regex::new(r#"(?is)<iframe[^>]+src="(?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
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|html| regex.captures(html))
|
||||||
|
.or_else(|| regex.captures(response))
|
||||||
|
.and_then(|captures| captures.name("url"))
|
||||||
|
.map(|value| value.as_str().trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_proxy_video_url(
|
||||||
|
&self,
|
||||||
|
seed_url: &str,
|
||||||
|
options: &ServerOptions,
|
||||||
|
) -> 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()?;
|
||||||
|
let episodes = Self::parse_player_episodes(&html).ok()?;
|
||||||
|
|
||||||
|
for episode in episodes {
|
||||||
|
let ajax_url = format!(
|
||||||
|
"{}/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 {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(iframe_url) = Self::parse_player_iframe_url(&response) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if proxy_name_for_url(&iframe_url).is_some() {
|
||||||
|
return Some(rewrite_hoster_url(options, &iframe_url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_detail_metadata(&self, seed: ListItemSeed, detail: DetailMetadata) -> VideoItem {
|
fn apply_detail_metadata(&self, seed: ListItemSeed, detail: DetailMetadata) -> VideoItem {
|
||||||
let mut item = VideoItem::new(
|
let mut item = VideoItem::new(
|
||||||
seed.id,
|
seed.id,
|
||||||
@@ -979,7 +1060,7 @@ impl SextbProvider {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match detail_fetch {
|
let mut item = match detail_fetch {
|
||||||
Ok(Ok(markdown)) => match Self::parse_detail(&markdown) {
|
Ok(Ok(markdown)) => match Self::parse_detail(&markdown) {
|
||||||
Ok(detail) => self.apply_detail_metadata(seed.clone(), detail),
|
Ok(detail) => self.apply_detail_metadata(seed.clone(), detail),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -995,9 +1076,24 @@ impl SextbProvider {
|
|||||||
report_provider_error_background(CHANNEL_ID, "fetch_detail_timeout", &seed.url);
|
report_provider_error_background(CHANNEL_ID, "fetch_detail_timeout", &seed.url);
|
||||||
self.apply_detail_metadata(seed, DetailMetadata::default())
|
self.apply_detail_metadata(seed, DetailMetadata::default())
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match timeout(
|
||||||
|
StdDuration::from_secs(10),
|
||||||
|
self.resolve_proxy_video_url(&item.url, options),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(proxy_url)) => item.url = proxy_url,
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(_) => {
|
||||||
|
report_provider_error_background(CHANNEL_ID, "resolve_proxy_video_url_timeout", &item.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_items_for_url(
|
async fn fetch_items_for_url(
|
||||||
&self,
|
&self,
|
||||||
cache: VideoCache,
|
cache: VideoCache,
|
||||||
@@ -1207,4 +1303,34 @@ mod tests {
|
|||||||
assert!(detail.release_date.is_some());
|
assert!(detail.release_date.is_some());
|
||||||
assert!(detail.added_at.is_some());
|
assert!(detail.added_at.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_dd_player_buttons_from_detail_html() {
|
||||||
|
let html = r#"
|
||||||
|
<div class="episode-list">
|
||||||
|
<button class="btn-player episode active" data-source="16900830" data-id="3708319"><i class="fa fa-play-circle"></i> TB</button>
|
||||||
|
<button class="btn-player episode" data-source="16900830" data-id="3708330"><i class="fa fa-play-circle"></i> SW</button>
|
||||||
|
<button class="btn-player episode" data-source="16900830" data-id="3708339"><i class="fa fa-play-circle"></i> DD</button>
|
||||||
|
</div>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let episodes = SextbProvider::parse_player_episodes(html).expect("episodes should parse");
|
||||||
|
assert_eq!(
|
||||||
|
episodes.first(),
|
||||||
|
Some(&PlayerEpisode {
|
||||||
|
label: "DD".to_string(),
|
||||||
|
film_id: "16900830".to_string(),
|
||||||
|
episode_id: "3708339".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>"}"#;
|
||||||
|
assert_eq!(
|
||||||
|
SextbProvider::parse_player_iframe_url(response).as_deref(),
|
||||||
|
Some("https://trailerhg.xyz/e/ttdc7a6qpskt")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
292
src/proxies/doodstream.rs
Normal file
292
src/proxies/doodstream.rs
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
use ntex::web;
|
||||||
|
use regex::{Captures, Regex};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DoodstreamProxy {}
|
||||||
|
|
||||||
|
impl DoodstreamProxy {
|
||||||
|
const ROOT_REFERER: &'static str = "https://turboplayers.xyz/";
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_detail_url(endpoint: &str) -> Option<String> {
|
||||||
|
let normalized = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
|
||||||
|
endpoint.trim().to_string()
|
||||||
|
} else {
|
||||||
|
format!("https://{}", endpoint.trim_start_matches('/'))
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::is_allowed_detail_url(&normalized).then_some(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_allowed_host(host: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
host,
|
||||||
|
"turboplayers.xyz"
|
||||||
|
| "www.turboplayers.xyz"
|
||||||
|
| "trailerhg.xyz"
|
||||||
|
| "www.trailerhg.xyz"
|
||||||
|
| "streamhg.com"
|
||||||
|
| "www.streamhg.com"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_allowed_detail_url(url: &str) -> bool {
|
||||||
|
let Some(url) = Url::parse(url).ok() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if url.scheme() != "https" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(host) = url.host_str() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !Self::is_allowed_host(host) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
url.path().starts_with("/t/")
|
||||||
|
|| url.path().starts_with("/e/")
|
||||||
|
|| url.path().starts_with("/d/")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_headers(detail_url: &str) -> Vec<(String, String)> {
|
||||||
|
vec![
|
||||||
|
("Referer".to_string(), Self::ROOT_REFERER.to_string()),
|
||||||
|
("Origin".to_string(), "https://turboplayers.xyz".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.9".to_string()),
|
||||||
|
(
|
||||||
|
"Sec-Fetch-Site".to_string(),
|
||||||
|
if detail_url.contains("trailerhg.xyz") {
|
||||||
|
"cross-site".to_string()
|
||||||
|
} else {
|
||||||
|
"same-origin".to_string()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn regex(pattern: &str) -> Option<Regex> {
|
||||||
|
Regex::new(pattern).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_base36(token: &str) -> Option<usize> {
|
||||||
|
usize::from_str_radix(token, 36).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_media_url(url: &str) -> String {
|
||||||
|
url.trim()
|
||||||
|
.trim_end_matches('\\')
|
||||||
|
.trim_end_matches('"')
|
||||||
|
.trim_end_matches('\'')
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_literal_url(text: &str) -> Option<String> {
|
||||||
|
let direct_patterns = [
|
||||||
|
r#"urlPlay\s*=\s*'(?P<url>https?://[^']+)'"#,
|
||||||
|
r#"data-hash\s*=\s*"(?P<url>https?://[^"]+)""#,
|
||||||
|
r#""(?P<url>https?://[^"]+\.(?:m3u8|mp4)(?:\?[^"]*)?)""#,
|
||||||
|
r#"'(?P<url>https?://[^']+\.(?:m3u8|mp4)(?:\?[^']*)?)'"#,
|
||||||
|
];
|
||||||
|
|
||||||
|
for pattern in direct_patterns {
|
||||||
|
let Some(regex) = Self::regex(pattern) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(url) = regex
|
||||||
|
.captures(text)
|
||||||
|
.and_then(|captures| captures.name("url"))
|
||||||
|
.map(|value| Self::sanitize_media_url(value.as_str()))
|
||||||
|
{
|
||||||
|
return Some(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_packed_eval_args(text: &str) -> Option<(String, usize, usize, Vec<String>)> {
|
||||||
|
let regex = Self::regex(
|
||||||
|
r#"eval\(function\(p,a,c,k,e,d\)\{.*?\}\('(?P<payload>(?:\\'|\\\\|[^'])*)',(?P<radix>\d+),(?P<count>\d+),'(?P<symbols>(?:\\'|\\\\|[^'])*)'\.split\('\|'\)"#,
|
||||||
|
)?;
|
||||||
|
let captures = regex.captures(text)?;
|
||||||
|
let payload = Self::decode_js_single_quoted(captures.name("payload")?.as_str());
|
||||||
|
let radix = captures.name("radix")?.as_str().parse::<usize>().ok()?;
|
||||||
|
let count = captures.name("count")?.as_str().parse::<usize>().ok()?;
|
||||||
|
let symbols = Self::decode_js_single_quoted(captures.name("symbols")?.as_str());
|
||||||
|
let parts = symbols.split('|').map(|value| value.to_string()).collect();
|
||||||
|
Some((payload, radix, count, parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_js_single_quoted(value: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(value.len());
|
||||||
|
let mut chars = value.chars();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch != '\\' {
|
||||||
|
result.push(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match chars.next() {
|
||||||
|
Some('\\') => result.push('\\'),
|
||||||
|
Some('\'') => result.push('\''),
|
||||||
|
Some('"') => result.push('"'),
|
||||||
|
Some('n') => result.push('\n'),
|
||||||
|
Some('r') => result.push('\r'),
|
||||||
|
Some('t') => result.push('\t'),
|
||||||
|
Some(other) => {
|
||||||
|
result.push('\\');
|
||||||
|
result.push(other);
|
||||||
|
}
|
||||||
|
None => result.push('\\'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unpack_packer(text: &str) -> Option<String> {
|
||||||
|
let (mut payload, radix, count, symbols) = Self::extract_packed_eval_args(text)?;
|
||||||
|
if radix != 36 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_regex = Self::regex(r"\b[0-9a-z]+\b")?;
|
||||||
|
payload = token_regex
|
||||||
|
.replace_all(&payload, |captures: &Captures| {
|
||||||
|
let token = captures.get(0).map(|value| value.as_str()).unwrap_or_default();
|
||||||
|
let Some(index) = Self::decode_base36(token) else {
|
||||||
|
return token.to_string();
|
||||||
|
};
|
||||||
|
if index >= count {
|
||||||
|
return token.to_string();
|
||||||
|
}
|
||||||
|
let replacement = symbols.get(index).map(|value| value.as_str()).unwrap_or("");
|
||||||
|
if replacement.is_empty() {
|
||||||
|
token.to_string()
|
||||||
|
} else {
|
||||||
|
replacement.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Some(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_media_candidates(text: &str) -> Vec<String> {
|
||||||
|
let Some(regex) = Self::regex(r#"https?://[^\s"'<>]+?\.(?:m3u8|mp4|txt)(?:\?[^\s"'<>]*)?"#)
|
||||||
|
else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut urls = regex
|
||||||
|
.find_iter(text)
|
||||||
|
.map(|value| Self::sanitize_media_url(value.as_str()))
|
||||||
|
.filter(|url| url.starts_with("https://"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
urls.sort_by_key(|url| {
|
||||||
|
if url.contains(".m3u8") {
|
||||||
|
0
|
||||||
|
} else if url.contains(".mp4") {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
urls.dedup();
|
||||||
|
urls
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_stream_url(text: &str) -> Option<String> {
|
||||||
|
if let Some(url) = Self::extract_literal_url(text) {
|
||||||
|
return Some(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let unpacked = Self::unpack_packer(text)?;
|
||||||
|
Self::collect_media_candidates(&unpacked)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.or_else(|| Self::extract_literal_url(&unpacked))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::proxies::Proxy for DoodstreamProxy {
|
||||||
|
async fn get_video_url(&self, url: String, requester: web::types::State<Requester>) -> String {
|
||||||
|
let Some(detail_url) = Self::normalize_detail_url(&url) else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut requester = requester.get_ref().clone();
|
||||||
|
let html = match requester
|
||||||
|
.get_with_headers(&detail_url, Self::request_headers(&detail_url), None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(_) => return String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::extract_stream_url(&html).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::DoodstreamProxy;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_only_known_doodstream_hosts() {
|
||||||
|
assert!(DoodstreamProxy::is_allowed_detail_url(
|
||||||
|
"https://turboplayers.xyz/t/69bdfb21cc640"
|
||||||
|
));
|
||||||
|
assert!(DoodstreamProxy::is_allowed_detail_url(
|
||||||
|
"https://trailerhg.xyz/e/ttdc7a6qpskt"
|
||||||
|
));
|
||||||
|
assert!(!DoodstreamProxy::is_allowed_detail_url(
|
||||||
|
"http://turboplayers.xyz/t/69bdfb21cc640"
|
||||||
|
));
|
||||||
|
assert!(!DoodstreamProxy::is_allowed_detail_url(
|
||||||
|
"https://example.com/t/69bdfb21cc640"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_clear_hls_url_from_turboplayers_layout() {
|
||||||
|
let html = r#"
|
||||||
|
<div id="video_player" data-hash="https://cdn4.turboviplay.com/data1/69bdfa8ce1f4d/69bdfa8ce1f4d.m3u8"></div>
|
||||||
|
<script>
|
||||||
|
var urlPlay = 'https://cdn4.turboviplay.com/data1/69bdfa8ce1f4d/69bdfa8ce1f4d.m3u8';
|
||||||
|
</script>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DoodstreamProxy::extract_stream_url(html).as_deref(),
|
||||||
|
Some("https://cdn4.turboviplay.com/data1/69bdfa8ce1f4d/69bdfa8ce1f4d.m3u8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unpacks_streamhg_style_player_config() {
|
||||||
|
let html = r#"
|
||||||
|
<script type='text/javascript'>
|
||||||
|
eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}('0 1={\"2\":\"https://cdn.example/master.m3u8?t=1\",\"3\":\"https://cdn.example/master.txt\"};4(\"5\").6({7:[{8:1.2,9:\"a\"}]});',36,11,'var|links|hls2|hls3|jwplayer|vplayer|setup|sources|file|type|hls'.split('|')))
|
||||||
|
</script>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DoodstreamProxy::extract_stream_url(html).as_deref(),
|
||||||
|
Some("https://cdn.example/master.m3u8?t=1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::proxies::doodstream::DoodstreamProxy;
|
||||||
use ntex::web;
|
use ntex::web;
|
||||||
|
|
||||||
use crate::proxies::pimpbunny::PimpbunnyProxy;
|
use crate::proxies::pimpbunny::PimpbunnyProxy;
|
||||||
@@ -5,6 +6,7 @@ use crate::proxies::porndish::PorndishProxy;
|
|||||||
use crate::proxies::spankbang::SpankbangProxy;
|
use crate::proxies::spankbang::SpankbangProxy;
|
||||||
use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester};
|
use crate::{proxies::sxyprn::SxyprnProxy, util::requester::Requester};
|
||||||
|
|
||||||
|
pub mod doodstream;
|
||||||
pub mod hanimecdn;
|
pub mod hanimecdn;
|
||||||
pub mod hqpornerthumb;
|
pub mod hqpornerthumb;
|
||||||
pub mod javtiful;
|
pub mod javtiful;
|
||||||
@@ -18,6 +20,7 @@ pub mod sxyprn;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AnyProxy {
|
pub enum AnyProxy {
|
||||||
|
Doodstream(DoodstreamProxy),
|
||||||
Sxyprn(SxyprnProxy),
|
Sxyprn(SxyprnProxy),
|
||||||
Javtiful(javtiful::JavtifulProxy),
|
Javtiful(javtiful::JavtifulProxy),
|
||||||
Pimpbunny(PimpbunnyProxy),
|
Pimpbunny(PimpbunnyProxy),
|
||||||
@@ -32,6 +35,7 @@ pub trait Proxy {
|
|||||||
impl Proxy for AnyProxy {
|
impl Proxy for AnyProxy {
|
||||||
async fn get_video_url(&self, url: String, requester: web::types::State<Requester>) -> String {
|
async fn get_video_url(&self, url: String, requester: web::types::State<Requester>) -> String {
|
||||||
match self {
|
match self {
|
||||||
|
AnyProxy::Doodstream(p) => p.get_video_url(url, requester).await,
|
||||||
AnyProxy::Sxyprn(p) => p.get_video_url(url, requester).await,
|
AnyProxy::Sxyprn(p) => p.get_video_url(url, requester).await,
|
||||||
AnyProxy::Javtiful(p) => p.get_video_url(url, requester).await,
|
AnyProxy::Javtiful(p) => p.get_video_url(url, requester).await,
|
||||||
AnyProxy::Pimpbunny(p) => p.get_video_url(url, requester).await,
|
AnyProxy::Pimpbunny(p) => p.get_video_url(url, requester).await,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use ntex::web::{self, HttpRequest};
|
use ntex::web::{self, HttpRequest};
|
||||||
|
|
||||||
|
use crate::proxies::doodstream::DoodstreamProxy;
|
||||||
use crate::proxies::javtiful::JavtifulProxy;
|
use crate::proxies::javtiful::JavtifulProxy;
|
||||||
use crate::proxies::pimpbunny::PimpbunnyProxy;
|
use crate::proxies::pimpbunny::PimpbunnyProxy;
|
||||||
use crate::proxies::porndish::PorndishProxy;
|
use crate::proxies::porndish::PorndishProxy;
|
||||||
@@ -10,6 +11,11 @@ use crate::util::requester::Requester;
|
|||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
|
web::resource("/doodstream/{endpoint}*")
|
||||||
|
.route(web::post().to(proxy2redirect))
|
||||||
|
.route(web::get().to(proxy2redirect)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
web::resource("/sxyprn/{endpoint}*")
|
web::resource("/sxyprn/{endpoint}*")
|
||||||
.route(web::post().to(proxy2redirect))
|
.route(web::post().to(proxy2redirect))
|
||||||
.route(web::get().to(proxy2redirect)),
|
.route(web::get().to(proxy2redirect)),
|
||||||
@@ -83,6 +89,7 @@ async fn proxy2redirect(
|
|||||||
|
|
||||||
fn get_proxy(proxy: &str) -> Option<AnyProxy> {
|
fn get_proxy(proxy: &str) -> Option<AnyProxy> {
|
||||||
match proxy {
|
match proxy {
|
||||||
|
"doodstream" => Some(AnyProxy::Doodstream(DoodstreamProxy::new())),
|
||||||
"sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())),
|
"sxyprn" => Some(AnyProxy::Sxyprn(SxyprnProxy::new())),
|
||||||
"javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())),
|
"javtiful" => Some(AnyProxy::Javtiful(JavtifulProxy::new())),
|
||||||
"pimpbunny" => Some(AnyProxy::Pimpbunny(PimpbunnyProxy::new())),
|
"pimpbunny" => Some(AnyProxy::Pimpbunny(PimpbunnyProxy::new())),
|
||||||
|
|||||||
84
src/util/hoster_proxy.rs
Normal file
84
src/util/hoster_proxy.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::providers::{build_proxy_url, strip_url_scheme};
|
||||||
|
use crate::videos::ServerOptions;
|
||||||
|
|
||||||
|
const DOODSTREAM_HOSTS: &[&str] = &[
|
||||||
|
"turboplayers.xyz",
|
||||||
|
"www.turboplayers.xyz",
|
||||||
|
"trailerhg.xyz",
|
||||||
|
"www.trailerhg.xyz",
|
||||||
|
"streamhg.com",
|
||||||
|
"www.streamhg.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn proxy_name_for_url(url: &str) -> Option<&'static str> {
|
||||||
|
let parsed = Url::parse(url).ok()?;
|
||||||
|
let host = parsed.host_str()?.to_ascii_lowercase();
|
||||||
|
|
||||||
|
if DOODSTREAM_HOSTS.contains(&host.as_str()) {
|
||||||
|
return Some("doodstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rewrite_hoster_url(options: &ServerOptions, url: &str) -> String {
|
||||||
|
match proxy_name_for_url(url) {
|
||||||
|
Some(proxy_name) => build_proxy_url(options, proxy_name, &strip_url_scheme(url)),
|
||||||
|
None => url.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{proxy_name_for_url, rewrite_hoster_url};
|
||||||
|
use crate::videos::ServerOptions;
|
||||||
|
|
||||||
|
fn options() -> ServerOptions {
|
||||||
|
ServerOptions {
|
||||||
|
featured: None,
|
||||||
|
category: None,
|
||||||
|
sites: None,
|
||||||
|
filter: None,
|
||||||
|
language: None,
|
||||||
|
public_url_base: Some("https://example.com".to_string()),
|
||||||
|
requester: None,
|
||||||
|
network: None,
|
||||||
|
stars: None,
|
||||||
|
categories: None,
|
||||||
|
duration: None,
|
||||||
|
sort: None,
|
||||||
|
sexuality: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_doodstream_family_hosts() {
|
||||||
|
assert_eq!(
|
||||||
|
proxy_name_for_url("https://turboplayers.xyz/t/69bdfb21cc640"),
|
||||||
|
Some("doodstream")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
proxy_name_for_url("https://trailerhg.xyz/e/ttdc7a6qpskt"),
|
||||||
|
Some("doodstream")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
proxy_name_for_url("https://streamhg.com/about"),
|
||||||
|
Some("doodstream")
|
||||||
|
);
|
||||||
|
assert_eq!(proxy_name_for_url("https://example.com/video"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrites_known_hoster_urls_to_proxy_urls() {
|
||||||
|
assert_eq!(
|
||||||
|
rewrite_hoster_url(&options(), "https://turboplayers.xyz/t/69bdfb21cc640"),
|
||||||
|
"https://example.com/proxy/doodstream/turboplayers.xyz/t/69bdfb21cc640"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rewrite_hoster_url(&options(), "https://example.com/video"),
|
||||||
|
"https://example.com/video"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod cache;
|
|||||||
pub mod discord;
|
pub mod discord;
|
||||||
pub mod flaresolverr;
|
pub mod flaresolverr;
|
||||||
pub mod flow_debug;
|
pub mod flow_debug;
|
||||||
|
pub mod hoster_proxy;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
pub mod requester;
|
pub mod requester;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
|
|||||||
Reference in New Issue
Block a user