From 30271b9b7c8de617ba39f047de87106b1c27cb45 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 18 Oct 2026 19:30:55 +0000 Subject: [PATCH] archivebate: app clients get resolved media URLs with headers, resolved concurrently Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi --- src/providers/archivebate.rs | 55 ++++++++++++++++ src/proxies/archivebate.rs | 119 +++++++++++++++++++++-------------- 2 files changed, 128 insertions(+), 46 deletions(-) diff --git a/src/providers/archivebate.rs b/src/providers/archivebate.rs index ffb0809..d6527c3 100644 --- a/src/providers/archivebate.rs +++ b/src/providers/archivebate.rs @@ -815,6 +815,53 @@ impl ArchivebateProvider { item } + /// For Hot Tub app clients: resolve every item's real media URL at the same + /// time (one task per item, no concurrency cap) and hand the app that URL + /// with the headers the CDN is played with, instead of the lazy + /// `/proxy/archivebate` redirect. Items that fail or time out keep the + /// proxied format, which re-resolves at playback time. + async fn resolve_direct_formats( + videos: Vec, + options: &ServerOptions, + ) -> Vec { + let tasks: Vec<_> = videos + .iter() + .map(|video| { + let detail_url = video.url.clone(); + let mut requester = + requester_or_default(options, CHANNEL_ID, "archivebate.resolve_direct"); + tokio::spawn(async move { + timeout( + StdDuration::from_secs(25), + crate::proxies::archivebate::resolve_media(&mut requester, &detail_url), + ) + .await + .ok() + .flatten() + }) + }) + .collect(); + + let resolved = futures::future::join_all(tasks).await; + videos + .into_iter() + .zip(resolved) + .map(|(mut video, resolved)| match resolved.ok().flatten() { + Some(media) => { + video.formats = Some(vec![Self::video_format_with_headers( + VideoFormat::new(media.media_url, "source".to_string(), "mp4".to_string()), + vec![ + ("Referer".to_string(), media.embed_url), + ("User-Agent".to_string(), FIREFOX_UA.to_string()), + ], + )]); + video + } + None => Self::with_default_format(video, options), + }) + .collect() + } + async fn enrich_video(&self, item: VideoItem, options: &ServerOptions) -> VideoItem { let page_url = item.url.clone(); let format_url = Self::proxied_video(options, &page_url); @@ -1208,6 +1255,14 @@ impl Provider for ArchivebateProvider { if videos.is_empty() { return videos; } + let is_app_client = options + .client_version + .as_ref() + .map(ClientVersion::is_verified_hottub) + .unwrap_or(false); + if is_app_client { + return Self::resolve_direct_formats(videos, &options).await; + } stream::iter(videos.into_iter().map(|video| { let provider = self.clone(); let options = options.clone(); diff --git a/src/proxies/archivebate.rs b/src/proxies/archivebate.rs index f1c19a6..0841ed4 100644 --- a/src/proxies/archivebate.rs +++ b/src/proxies/archivebate.rs @@ -229,63 +229,90 @@ impl ArchivebateProxy { embed_url: &str, requester: &mut Requester, ) -> Option { - let response = requester - .get_raw_with_headers_timeout( - embed_url, - Self::html_headers(detail_url), - Some(StdDuration::from_secs(8)), - None, - ) - .await - .ok()?; - if !response.status().is_success() { - return None; + // The embed host is sometimes slow on a first (cold) request and fast on + // the next, so one timed-out attempt is retried. + for _ in 0..2 { + let Ok(response) = requester + .get_raw_with_headers_timeout( + embed_url, + Self::html_headers(detail_url), + Some(StdDuration::from_secs(10)), + None, + ) + .await + else { + continue; + }; + if !response.status().is_success() { + return None; + } + let html = response.text().await.ok()?; + return Self::extract_mixdrop_media_url(&html); } - let html = response.text().await.ok()?; - Self::extract_mixdrop_media_url(&html) + None } } +/// A resolved Mixdrop media file plus the embed page it came from (the CDN is +/// played with that page as `Referer`). +#[derive(Debug, Clone)] +pub struct ResolvedMedia { + pub media_url: String, + pub embed_url: String, +} + +/// Resolves an archivebate watch page (`https://archivebate.com/watch/{id}`) +/// to the real media URL: watch page -> Mixdrop embed -> unpacked +/// `MDCore.wurl`. Shared by the lazy `/proxy/archivebate` redirect and by the +/// provider, which hands the app the media URL directly. +pub async fn resolve_media(requester: &mut Requester, detail_url: &str) -> Option { + let detail_html = requester + .get_with_headers( + detail_url, + ArchivebateProxy::html_headers("https://archivebate.com/"), + Some(Version::HTTP_11), + ) + .await + .unwrap_or_default(); + if detail_html.is_empty() { + return None; + } + + let mut embeds: Vec = Vec::new(); + if let Some(iframe_url) = ArchivebateProxy::first_iframe_source_from_html(&detail_html) + .map(|value| ArchivebateProxy::absolute_url(&value)) + { + if ArchivebateProxy::is_mixdrop_host(&iframe_url) { + embeds.push(iframe_url); + } + } + if let Some(embed_url) = ArchivebateProxy::download_fid_from_detail_html(&detail_html) + .map(|value| ArchivebateProxy::absolute_url(&value)) + .and_then(|value| ArchivebateProxy::mixdrop_embed_url_from_download_url(&value)) + { + embeds.push(embed_url); + } + + for embed_url in embeds { + if let Some(media_url) = + ArchivebateProxy::resolve_mixdrop_media_from_embed(detail_url, &embed_url, requester).await + { + return Some(ResolvedMedia { media_url, embed_url }); + } + } + None +} + impl crate::proxies::Proxy for ArchivebateProxy { async fn get_video_url(&self, url: String, requester: web::types::State) -> String { let Some(detail_url) = Self::normalize_detail_request(&url) else { return String::new(); }; - let mut requester = requester.get_ref().clone(); - let detail_html = requester - .get_with_headers( - &detail_url, - Self::html_headers("https://archivebate.com/"), - Some(Version::HTTP_11), - ) + resolve_media(&mut requester, &detail_url) .await - .unwrap_or_default(); - if detail_html.is_empty() { - return String::new(); - } - - if let Some(iframe_url) = Self::first_iframe_source_from_html(&detail_html).map(|value| Self::absolute_url(&value)) { - if Self::is_mixdrop_host(&iframe_url) { - if let Some(media_url) = - Self::resolve_mixdrop_media_from_embed(&detail_url, &iframe_url, &mut requester).await - { - return media_url; - } - } - } - - if let Some(download_fid) = Self::download_fid_from_detail_html(&detail_html).map(|value| Self::absolute_url(&value)) { - if let Some(embed_url) = Self::mixdrop_embed_url_from_download_url(&download_fid) { - if let Some(media_url) = - Self::resolve_mixdrop_media_from_embed(&detail_url, &embed_url, &mut requester).await - { - return media_url; - } - } - } - - String::new() + .map(|resolved| resolved.media_url) + .unwrap_or_default() } }