224 lines
6.8 KiB
Rust
224 lines
6.8 KiB
Rust
use base64::{Engine as _, engine::general_purpose};
|
|
use ntex::web;
|
|
use std::time::Duration;
|
|
|
|
use crate::util::requester::Requester;
|
|
|
|
fn ssut51(arg: &str) -> u32 {
|
|
arg.chars()
|
|
.filter(|c| c.is_ascii_digit())
|
|
.map(|c| c.to_digit(10).unwrap())
|
|
.sum()
|
|
}
|
|
|
|
fn boo(sum1: u32, sum2: u32) -> String {
|
|
let raw = format!("{}-{}-{}", sum1, "sxyprn.com", sum2);
|
|
let encoded = general_purpose::STANDARD.encode(raw);
|
|
encoded
|
|
.replace('+', "-")
|
|
.replace('/', "_")
|
|
.replace('=', ".")
|
|
}
|
|
|
|
/// Extracts all CDN path values from the data-vnfo JSON attribute.
|
|
pub(crate) fn extract_all_cdn_paths(html: &str) -> Vec<String> {
|
|
let json_str = match html.split("data-vnfo='").nth(1) {
|
|
Some(s) => s.split('\'').next().unwrap_or(""),
|
|
None => return vec![],
|
|
};
|
|
|
|
if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(json_str) {
|
|
let paths: Vec<String> = map
|
|
.values()
|
|
.filter_map(|v| v.as_str().map(|s| s.replace('\\', "")))
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
if !paths.is_empty() {
|
|
return paths;
|
|
}
|
|
}
|
|
|
|
// Fallback: original single-value extraction
|
|
let first = json_str
|
|
.split("\":\"")
|
|
.nth(1)
|
|
.and_then(|s| s.split("\"}").next())
|
|
.map(|s| s.replace('\\', ""))
|
|
.unwrap_or_default();
|
|
if first.is_empty() {
|
|
vec![]
|
|
} else {
|
|
vec![first]
|
|
}
|
|
}
|
|
|
|
/// Applies the sxyprn segment transformation to produce the pre-redirect CDN URL.
|
|
pub(crate) fn transform_cdn_path(path: &str) -> Option<String> {
|
|
let mut tmp: Vec<String> = path.split('/').map(|s| s.to_string()).collect();
|
|
if tmp.len() < 8 {
|
|
return None;
|
|
}
|
|
let s6 = ssut51(&tmp[6]);
|
|
let s7 = ssut51(&tmp[7]);
|
|
tmp[1] = format!("{}8/{}", tmp[1], boo(s6, s7));
|
|
tmp[5] = format!("{}", tmp[5].parse::<u32>().unwrap_or(0).saturating_sub(s6 + s7));
|
|
Some(format!("https://sxyprn.com{}", tmp.join("/")))
|
|
}
|
|
|
|
/// Races all candidate CDN URLs concurrently via blocking curl HEAD requests.
|
|
/// Returns the final CDN URL from the first candidate that successfully redirects.
|
|
async fn race_cdn_urls(candidate_urls: Vec<String>) -> String {
|
|
if candidate_urls.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(candidate_urls.len());
|
|
|
|
for cdn_url in candidate_urls {
|
|
let tx = tx.clone();
|
|
tokio::spawn(async move {
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
crate::util::get_redirect_location(&cdn_url)
|
|
.ok()
|
|
.flatten()
|
|
.map(|loc| format!("https:{}", loc))
|
|
})
|
|
.await
|
|
.ok()
|
|
.flatten();
|
|
|
|
if let Some(url) = result {
|
|
let _ = tx.send(url).await;
|
|
}
|
|
});
|
|
}
|
|
drop(tx);
|
|
|
|
tokio::time::timeout(Duration::from_secs(15), rx.recv())
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Resolves all candidate CDN mirror URLs concurrently, collecting every
|
|
/// mirror that redirects successfully within the timeout window. Unlike
|
|
/// `race_cdn_urls`, this does not stop at the first success -- the mirrors
|
|
/// are redundant copies of the same stream, and callers that want to serve
|
|
/// directly-playable format URLs need as many working ones as possible.
|
|
pub(crate) async fn resolve_all_cdn_urls(candidate_urls: Vec<String>) -> Vec<String> {
|
|
if candidate_urls.is_empty() {
|
|
return vec![];
|
|
}
|
|
|
|
let handles: Vec<_> = candidate_urls
|
|
.into_iter()
|
|
.map(|cdn_url| {
|
|
tokio::spawn(async move {
|
|
tokio::task::spawn_blocking(move || {
|
|
crate::util::get_redirect_location(&cdn_url)
|
|
.ok()
|
|
.flatten()
|
|
.map(|loc| format!("https:{}", loc))
|
|
})
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
|
|
let mut resolved = Vec::new();
|
|
for handle in handles {
|
|
if let Ok(Ok(Some(url))) = tokio::time::timeout_at(deadline, handle).await {
|
|
if !resolved.contains(&url) {
|
|
resolved.push(url);
|
|
}
|
|
}
|
|
}
|
|
resolved
|
|
}
|
|
|
|
/// Fetches the sxyprn detail page for `slug` and resolves every mirror CDN
|
|
/// URL it advertises. Used by the provider to eagerly populate `formats`
|
|
/// for app clients, instead of the lazy single-mirror `/proxy/sxyprn/...`
|
|
/// redirect used by other clients.
|
|
pub(crate) async fn resolve_all_media_urls(
|
|
requester: &mut Requester,
|
|
base_url: &str,
|
|
slug: &str,
|
|
) -> Vec<String> {
|
|
let full_url = format!("{}/post/{}", base_url, slug);
|
|
let text = requester.get(&full_url, None).await.unwrap_or_default();
|
|
if text.is_empty() {
|
|
return vec![];
|
|
}
|
|
|
|
let candidate_urls: Vec<String> = extract_all_cdn_paths(&text)
|
|
.iter()
|
|
.filter_map(|p| transform_cdn_path(p))
|
|
.collect();
|
|
|
|
resolve_all_cdn_urls(candidate_urls).await
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SxyprnProxy {}
|
|
|
|
impl SxyprnProxy {
|
|
pub fn new() -> Self {
|
|
SxyprnProxy {}
|
|
}
|
|
|
|
pub async fn get_video_url(
|
|
&self,
|
|
url: String,
|
|
requester: web::types::State<Requester>,
|
|
) -> String {
|
|
if let Some(encoded) = url.strip_prefix("race/") {
|
|
return self.resolve_race(encoded).await;
|
|
}
|
|
|
|
let mut requester = requester.get_ref().clone();
|
|
let full_url = format!("https://sxyprn.com/{}", url);
|
|
let text = requester.get(&full_url, None).await.unwrap_or_default();
|
|
if text.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let cdn_paths = extract_all_cdn_paths(&text);
|
|
let candidate_urls: Vec<String> = cdn_paths
|
|
.iter()
|
|
.filter_map(|p| transform_cdn_path(p))
|
|
.collect();
|
|
|
|
if candidate_urls.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let joined = candidate_urls.join("|");
|
|
let encoded = general_purpose::URL_SAFE_NO_PAD.encode(joined.as_bytes());
|
|
format!("/proxy/sxyprn/race/{}", encoded)
|
|
}
|
|
|
|
async fn resolve_race(&self, encoded: &str) -> String {
|
|
let bytes = match general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()) {
|
|
Ok(b) => b,
|
|
Err(_) => return String::new(),
|
|
};
|
|
let decoded = match String::from_utf8(bytes) {
|
|
Ok(s) => s,
|
|
Err(_) => return String::new(),
|
|
};
|
|
|
|
let urls: Vec<String> = decoded
|
|
.split('|')
|
|
.filter(|s| !s.is_empty())
|
|
.map(|s| s.to_string())
|
|
.collect();
|
|
|
|
race_cdn_urls(urls).await
|
|
}
|
|
}
|