//! rule34video.com video-page resolution for the `/proxy/rule34video/` redirect. //! //! Lives here (always compiled) because `HOT_TUB_PROVIDER=` 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 { 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::().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 { 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, ) -> Option { 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("").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); } }