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
72 lines
2.3 KiB
Rust
72 lines
2.3 KiB
Rust
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);
|
|
}
|
|
}
|