Move redirecting-proxy formats to video.url for verified app clients; document the rule
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
@@ -26,6 +26,7 @@ These rules cut across every step below. They are intentionally grouped at the t
|
|||||||
|
|
||||||
### Output Shape
|
### Output Shape
|
||||||
|
|
||||||
|
- **Never put a redirecting URL in `formats[].url`**: the Hot Tub app cannot follow redirects there. Redirect proxies (`REDIRECT_PROXIES` in `src/proxy.rs`) are for `video.url`; `api.rs` moves such formats to `url` for verified app clients as a safety net.
|
||||||
- Use `formats` for real media URLs (HLS, multi-quality, signed token), and keep `video.url` on a stable page URL. Never set `video.url` to a tokenized stream that needs a `Referer` to play — the `url` field has no header contract.
|
- Use `formats` for real media URLs (HLS, multi-quality, signed token), and keep `video.url` on a stable page URL. Never set `video.url` to a tokenized stream that needs a `Referer` to play — the `url` field has no header contract.
|
||||||
- If thumbnails need a `Referer` and the player has no per-thumb header mechanism, proxy them via `/proxy/<id>-thumb/...`. The same applies to manifests with relative URIs.
|
- If thumbnails need a `Referer` and the player has no per-thumb header mechanism, proxy them via `/proxy/<id>-thumb/...`. The same applies to manifests with relative URIs.
|
||||||
- Keep filter `id` values stable and machine-targeted. Display `title`s change; ids route.
|
- Keep filter `id` values stable and machine-targeted. Display `title`s change; ids route.
|
||||||
|
|||||||
82
src/api.rs
82
src/api.rs
@@ -714,6 +714,15 @@ async fn videos_post(
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Hot Tub app cannot follow a redirect in `formats[].url`: for verified
|
||||||
|
// app clients, formats that point at a redirecting `/proxy/...` become the
|
||||||
|
// item's `url` instead.
|
||||||
|
if clientversion.is_verified_hottub() {
|
||||||
|
for video in video_items.iter_mut() {
|
||||||
|
move_redirect_formats_to_url(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(literal_query) = literal_query.as_deref() {
|
if let Some(literal_query) = literal_query.as_deref() {
|
||||||
#[cfg(feature = "debug")]
|
#[cfg(feature = "debug")]
|
||||||
let before = video_items.len();
|
let before = video_items.len();
|
||||||
@@ -794,6 +803,30 @@ async fn videos_post(
|
|||||||
Ok(web::HttpResponse::Ok().json(&videos))
|
Ok(web::HttpResponse::Ok().json(&videos))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves formats that point at a redirecting proxy to `video.url`.
|
||||||
|
///
|
||||||
|
/// If direct (non-redirecting) formats remain they are kept and `url` is left
|
||||||
|
/// alone; otherwise `url` becomes the first (best-listed) redirecting proxy URL
|
||||||
|
/// and `formats` is dropped entirely.
|
||||||
|
pub(crate) fn move_redirect_formats_to_url(video: &mut VideoItem) {
|
||||||
|
let Some(formats) = video.formats.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (redirecting, direct): (Vec<_>, Vec<_>) = formats
|
||||||
|
.into_iter()
|
||||||
|
.partition(|format| crate::proxy::is_redirect_proxy_url(&format.url));
|
||||||
|
if redirecting.is_empty() {
|
||||||
|
video.formats = Some(direct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if direct.is_empty() {
|
||||||
|
video.url = redirecting[0].url.clone();
|
||||||
|
video.formats = None;
|
||||||
|
} else {
|
||||||
|
video.formats = Some(direct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn uploaders_post(
|
async fn uploaders_post(
|
||||||
uploader_request: web::types::Json<UploadersRequest>,
|
uploader_request: web::types::Json<UploadersRequest>,
|
||||||
cache: web::types::State<VideoCache>,
|
cache: web::types::State<VideoCache>,
|
||||||
@@ -1082,4 +1115,53 @@ mod tests {
|
|||||||
assert_eq!(item.uploader.as_deref(), Some("FreeusePorn"));
|
assert_eq!(item.uploader.as_deref(), Some("FreeusePorn"));
|
||||||
assert_eq!(item.formats.as_ref().map(|formats| formats.len()), Some(2));
|
assert_eq!(item.formats.as_ref().map(|formats| formats.len()), Some(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn item_with_formats(urls: &[&str]) -> VideoItem {
|
||||||
|
let mut item = VideoItem::new(
|
||||||
|
"id".to_string(),
|
||||||
|
"t".to_string(),
|
||||||
|
"https://site.example/watch/1".to_string(),
|
||||||
|
"c".to_string(),
|
||||||
|
"https://thumb".to_string(),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
item.formats = Some(
|
||||||
|
urls.iter()
|
||||||
|
.map(|u| crate::videos::VideoFormat::new(u.to_string(), "q".to_string(), "mp4".to_string()))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn redirect_only_formats_move_to_url() {
|
||||||
|
let mut item = item_with_formats(&[
|
||||||
|
"http://h/proxy/hqporner/1080",
|
||||||
|
"http://h/proxy/hqporner/720",
|
||||||
|
]);
|
||||||
|
move_redirect_formats_to_url(&mut item);
|
||||||
|
assert_eq!(item.url, "http://h/proxy/hqporner/1080");
|
||||||
|
assert!(item.formats.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_formats_are_kept_and_redirect_ones_dropped() {
|
||||||
|
let mut item = item_with_formats(&[
|
||||||
|
"http://h/proxy/hqporner/1080",
|
||||||
|
"https://cdn.example/v.mp4",
|
||||||
|
]);
|
||||||
|
move_redirect_formats_to_url(&mut item);
|
||||||
|
assert_eq!(item.url, "https://site.example/watch/1");
|
||||||
|
let formats = item.formats.expect("direct format kept");
|
||||||
|
assert_eq!(formats.len(), 1);
|
||||||
|
assert_eq!(formats[0].url, "https://cdn.example/v.mp4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_proxy_formats_are_untouched() {
|
||||||
|
let mut item = item_with_formats(&["http://h/proxy/noodlemagazine/x.mp4"]);
|
||||||
|
move_redirect_formats_to_url(&mut item);
|
||||||
|
assert_eq!(item.url, "https://site.example/watch/1");
|
||||||
|
assert_eq!(item.formats.map(|f| f.len()), Some(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/proxy.rs
70
src/proxy.rs
@@ -20,6 +20,40 @@ use crate::proxies::thaiporntv::ThaipornTvProxy;
|
|||||||
use crate::proxies::*;
|
use crate::proxies::*;
|
||||||
use crate::util::requester::Requester;
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
/// Proxies that answer with a `302` to the real media instead of streaming it.
|
||||||
|
/// The Hot Tub app cannot follow a redirect inside `formats[].url`, so for app
|
||||||
|
/// clients a format pointing at one of these is moved to `video.url` (see
|
||||||
|
/// `move_redirect_formats_to_url`). Keep in sync with the `proxy2redirect`
|
||||||
|
/// routes below; `redirect_proxy_list_matches_routes` enforces it.
|
||||||
|
pub const REDIRECT_PROXIES: &[&str] = &[
|
||||||
|
"archivebate",
|
||||||
|
"clapdat",
|
||||||
|
"doodstream",
|
||||||
|
"lulustream",
|
||||||
|
"sxyprn",
|
||||||
|
"javtiful",
|
||||||
|
"spankbang",
|
||||||
|
"porndish",
|
||||||
|
"hqporner",
|
||||||
|
"heavyfetish",
|
||||||
|
"vjav",
|
||||||
|
"pornhd3x",
|
||||||
|
"vidara",
|
||||||
|
"pimpbunny",
|
||||||
|
"thaiporntv",
|
||||||
|
"tube8",
|
||||||
|
"supjav",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// True for a `/proxy/<redirect proxy>/...` URL (absolute or host-relative).
|
||||||
|
pub fn is_redirect_proxy_url(url: &str) -> bool {
|
||||||
|
let Some(rest) = url.split_once("/proxy/").map(|(_, rest)| rest) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let name = rest.split('/').next().unwrap_or_default();
|
||||||
|
REDIRECT_PROXIES.contains(&name)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::resource("/archivebate/{endpoint}*")
|
web::resource("/archivebate/{endpoint}*")
|
||||||
@@ -239,3 +273,39 @@ fn get_proxy(proxy: &str) -> Option<AnyProxy> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod redirect_proxy_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recognises_redirect_proxy_urls() {
|
||||||
|
assert!(is_redirect_proxy_url("http://h/proxy/archivebate/archivebate.com/watch/1"));
|
||||||
|
assert!(is_redirect_proxy_url("/proxy/hqporner/x"));
|
||||||
|
assert!(!is_redirect_proxy_url("http://h/proxy/noodlemagazine/x"));
|
||||||
|
assert!(!is_redirect_proxy_url("https://cdn.example/video.mp4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn redirect_proxy_list_matches_routes() {
|
||||||
|
let source = include_str!("proxy.rs");
|
||||||
|
let config_body = source
|
||||||
|
.split("pub fn config(")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|rest| rest.split("async fn proxy2redirect(").next())
|
||||||
|
.expect("config body");
|
||||||
|
let mut from_routes: Vec<String> = Vec::new();
|
||||||
|
for block in config_body.split("web::resource(\"/").skip(1) {
|
||||||
|
let name = block.split("/{endpoint}").next().unwrap_or_default();
|
||||||
|
let until_next = block.split("web::resource(").next().unwrap_or(block);
|
||||||
|
if until_next.contains("proxy2redirect") {
|
||||||
|
from_routes.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut listed: Vec<String> = REDIRECT_PROXIES.iter().map(|s| s.to_string()).collect();
|
||||||
|
from_routes.sort();
|
||||||
|
from_routes.dedup();
|
||||||
|
listed.sort();
|
||||||
|
assert_eq!(listed, from_routes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user