hanime: play through a redirect proxy; the signed stream links expire
Items now carry /proxy/hanime/{slug}/{height}.m3u8, which does a fresh
handshake per play and 302s to the current playlist. The handshake moved
to util/hanime_resolve.rs so single-provider builds keep the route.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
71
src/proxies/hanime.rs
Normal file
71
src/proxies/hanime.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use ntex::http::Method;
|
||||
use ntex::http::header::LOCATION;
|
||||
use ntex::web::{self, HttpRequest};
|
||||
|
||||
use crate::util::hanime_resolve::{fetch_sources, pick_source, source_url};
|
||||
use crate::util::requester::Requester;
|
||||
|
||||
/// `{slug}[/{height}][.m3u8]`.
|
||||
fn parse_endpoint(endpoint: &str) -> Option<(String, Option<u32>)> {
|
||||
let endpoint = endpoint.trim_matches('/').trim_end_matches(".m3u8");
|
||||
let mut parts = endpoint.split('/');
|
||||
let slug = parts
|
||||
.next()
|
||||
.filter(|slug| {
|
||||
!slug.is_empty()
|
||||
&& slug
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
})?;
|
||||
let height = parts.next().and_then(|h| h.parse::<u32>().ok());
|
||||
Some((slug.to_string(), height))
|
||||
}
|
||||
|
||||
/// hanime's stream links expire, so the channel hands out this stable URL and a
|
||||
/// fresh handshake happens on every play: redirects to the current playlist.
|
||||
pub async fn redirect_to_media(
|
||||
req: HttpRequest,
|
||||
requester: web::types::State<Requester>,
|
||||
) -> Result<impl web::Responder, web::Error> {
|
||||
let endpoint = req.match_info().query("endpoint").to_string();
|
||||
let Some((slug, height)) = parse_endpoint(&endpoint) else {
|
||||
return Ok(web::HttpResponse::BadRequest().finish());
|
||||
};
|
||||
if req.method() == Method::HEAD {
|
||||
return Ok(web::HttpResponse::Ok().finish());
|
||||
}
|
||||
|
||||
let mut requester = requester.get_ref().clone();
|
||||
let Ok(sources) = fetch_sources(&mut requester, &slug).await else {
|
||||
return Ok(web::HttpResponse::BadGateway().finish());
|
||||
};
|
||||
let Some(source) = pick_source(&sources, height) else {
|
||||
return Ok(web::HttpResponse::NotFound().finish());
|
||||
};
|
||||
Ok(web::HttpResponse::Found()
|
||||
.header(LOCATION, source_url(source))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_endpoint;
|
||||
|
||||
#[test]
|
||||
fn parses_slug_and_height() {
|
||||
assert_eq!(
|
||||
parse_endpoint("natsu-zuma-2/720.m3u8"),
|
||||
Some(("natsu-zuma-2".to_string(), Some(720)))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_endpoint("/natsu-zuma-2"),
|
||||
Some(("natsu-zuma-2".to_string(), None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsafe_slugs() {
|
||||
assert_eq!(parse_endpoint("../etc/720.m3u8"), None);
|
||||
assert_eq!(parse_endpoint(""), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user