rule34video: drop per-video enrichment; add on-demand page resolver proxy (off)

Listings no longer fetch every video page for formats/tags/artist, which
ran into the site's 429 limit. /proxy/rule34video/{id}/{slug} resolves the
page at play time and 302s to the signed mp4; the resolver lives in
util/rule34video_resolve.rs. Handing out proxy URLs is gated behind
USE_MEDIA_PROXY, currently false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEp724zDpCWNaSeCrMSvjx
This commit is contained in:
Simon
2026-11-05 20:26:50 +00:00
parent cf0ae15f93
commit 4f83dae3fa
6 changed files with 170 additions and 211 deletions

View File

@@ -0,0 +1,117 @@
//! rule34video.com video-page resolution for the `/proxy/rule34video/` redirect.
//!
//! Lives here (always compiled) because `HOT_TUB_PROVIDER=<id>` single-provider
//! builds only compile the selected provider, while the proxy routes are
//! always registered.
//!
//! yt-dlp's Rule34Video extractor no longer resolves the page URL ("No video
//! formats found"), so the signed mp4 URLs (360p/480p/720p, valid ~12h, no
//! cookie/Referer needed) come from the page's `var flashvars`.
use crate::util::requester::Requester;
const BASE_URL: &str = "https://rule34video.com";
/// Reads a single-quoted JS string value (`key: '...'`) from the
/// `flashvars` object, honouring `\'` escapes.
fn flashvar(block: &str, key: &str) -> Option<String> {
let marker = format!("{}: '", key);
let start = block.find(&marker)? + marker.len();
let mut out = String::new();
let mut chars = block[start..].chars();
while let Some(c) = chars.next() {
match c {
'\\' => out.push(chars.next()?),
'\'' => return Some(out),
other => out.push(other),
}
}
None
}
/// `(height, url)` for every quality on the page, best first.
pub fn parse_formats(html: &str) -> Vec<(u32, String)> {
let Some(start) = html.find("var flashvars") else {
return vec![];
};
let block = &html[start..];
let block = &block[..block.len().min(6000)];
let mut formats: Vec<(u32, String)> = Vec::new();
for (url_key, text_key) in [
("video_url", "video_url_text"),
("video_alt_url", "video_alt_url_text"),
("video_alt_url2", "video_alt_url2_text"),
("video_alt_url3", "video_alt_url3_text"),
] {
let Some(url) = flashvar(block, url_key).filter(|url| url.starts_with("http")) else {
continue;
};
let height = flashvar(block, text_key)
.and_then(|quality| quality.trim_end_matches('p').parse::<u32>().ok())
.unwrap_or(0);
formats.push((height, url));
}
formats.sort_by(|a, b| b.0.cmp(&a.0));
formats
}
/// `{id}/{slug}[/]` -> the video page URL. Only digits, letters, `-` and `_`
/// are accepted so the endpoint cannot steer the request elsewhere.
pub fn page_url(endpoint: &str) -> Option<String> {
let mut parts = endpoint.trim_matches('/').split('/');
let id = parts.next().filter(|id| !id.is_empty() && id.chars().all(|c| c.is_ascii_digit()))?;
let slug = parts.next().filter(|slug| {
!slug.is_empty() && slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
})?;
match parts.next() {
None => Some(format!("{BASE_URL}/video/{id}/{slug}/")),
Some(_) => None,
}
}
/// The best signed mp4 URL (or the one of `height`) for a video page.
pub async fn resolve_media_url(
requester: &mut Requester,
page_url: &str,
height: Option<u32>,
) -> Option<String> {
let headers = vec![("Referer".to_string(), format!("{BASE_URL}/"))];
let html = requester.get_probe(page_url, headers).await.ok().flatten()?;
let formats = parse_formats(&html);
height
.and_then(|h| formats.iter().find(|(height, _)| *height == h))
.or_else(|| formats.first())
.map(|(_, url)| url.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_flashvars_formats_best_first() {
let html = r#"var flashvars = { video_id: '1', video_url: 'https://rule34video.com/get_file/a_360.mp4/?v-acctoken=1', video_url_text: '360p', video_alt_url: 'https://rule34video.com/get_file/a_480p.mp4/?v-acctoken=2', video_alt_url_text: '480p', video_alt_url2: 'https://rule34video.com/get_file/a_720p.mp4/?v-acctoken=3', video_alt_url2_text: '720p', video_title: 'It\'s' };"#;
let formats = parse_formats(html);
assert_eq!(formats.len(), 3);
assert_eq!(formats[0].0, 720);
assert!(formats[0].1.contains("a_720p"));
assert!(formats[2].1.contains("a_360"));
}
#[test]
fn page_without_flashvars_yields_nothing() {
assert!(parse_formats("<html></html>").is_empty());
}
#[test]
fn builds_page_url_from_safe_endpoints_only() {
assert_eq!(
page_url("123/some-slug/").as_deref(),
Some("https://rule34video.com/video/123/some-slug/")
);
assert_eq!(page_url("123/../x"), None);
assert_eq!(page_url("abc/slug"), None);
assert_eq!(page_url("123/slug/extra"), None);
}
}