eporner: play through a redirect proxy; the signed CDN links expire after 2h
Expired eporner mp4 links are answered with a 302 to static.eporner.com/
na.mp4, a one-minute "watch it on eporner.com" clip. The links expire
exactly two hours after the /xhr/video call, so anything played from a
favourite, history entry or a list left open past that hit the decoy.
New /proxy/eporner/{alnum_id}/{height} re-resolves the hash/xhr chain at
play time and 302s to the matching (else best) quality. The eporner
provider and melonstube's eporner destinations now emit these proxy URLs;
verified app clients get the best quality as video.url via the existing
redirect-proxy rule. Verified live: proxy -> fresh CDN link -> 206 video/mp4
for app and non-app clients.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
65
src/proxies/eporner.rs
Normal file
65
src/proxies/eporner.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use ntex::web;
|
||||
|
||||
use crate::util::eporner_resolve::resolve_ranked;
|
||||
use crate::util::requester::Requester;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EpornerProxy {}
|
||||
|
||||
impl EpornerProxy {
|
||||
pub fn new() -> Self {
|
||||
EpornerProxy {}
|
||||
}
|
||||
|
||||
/// `{alnum_id}[/{height}]`.
|
||||
fn parse_endpoint(endpoint: &str) -> Option<(String, Option<u32>)> {
|
||||
let mut parts = endpoint.trim_matches('/').split('/');
|
||||
let alnum_id = parts.next().filter(|id| {
|
||||
!id.is_empty() && id.chars().all(|c| c.is_ascii_alphanumeric())
|
||||
})?;
|
||||
let height = parts.next().and_then(|h| h.parse::<u32>().ok());
|
||||
Some((alnum_id.to_string(), height))
|
||||
}
|
||||
|
||||
/// Resolves a fresh signed mp4 at play time. The CDN URLs expire two hours
|
||||
/// after the `/xhr/video` call, and an expired one is answered with a 302
|
||||
/// to `static.eporner.com/na.mp4`, a one-minute "watch it on eporner.com"
|
||||
/// clip; so a stale listing, favourite or history entry plays that clip.
|
||||
pub async fn get_video_url(
|
||||
&self,
|
||||
endpoint: String,
|
||||
requester: web::types::State<Requester>,
|
||||
) -> String {
|
||||
let Some((alnum_id, height)) = Self::parse_endpoint(&endpoint) else {
|
||||
return String::new();
|
||||
};
|
||||
let page_url = format!("https://www.eporner.com/video-{alnum_id}/");
|
||||
let mut requester = requester.get_ref().clone();
|
||||
let Some(ranked) = resolve_ranked(&mut requester, &alnum_id, &page_url).await else {
|
||||
return String::new();
|
||||
};
|
||||
height
|
||||
.and_then(|h| ranked.iter().find(|(height, _)| *height == h))
|
||||
.or_else(|| ranked.first())
|
||||
.map(|(_, format)| format.url.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::EpornerProxy;
|
||||
|
||||
#[test]
|
||||
fn parses_id_and_optional_height() {
|
||||
assert_eq!(
|
||||
EpornerProxy::parse_endpoint("exfeTXHuL1x/720"),
|
||||
Some(("exfeTXHuL1x".to_string(), Some(720)))
|
||||
);
|
||||
assert_eq!(
|
||||
EpornerProxy::parse_endpoint("/exfeTXHuL1x/"),
|
||||
Some(("exfeTXHuL1x".to_string(), None))
|
||||
);
|
||||
assert_eq!(EpornerProxy::parse_endpoint("../etc"), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user