archivebate: app clients get resolved media URLs with headers, resolved concurrently
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
@@ -815,6 +815,53 @@ impl ArchivebateProvider {
|
|||||||
item
|
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<VideoItem>,
|
||||||
|
options: &ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
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 {
|
async fn enrich_video(&self, item: VideoItem, options: &ServerOptions) -> VideoItem {
|
||||||
let page_url = item.url.clone();
|
let page_url = item.url.clone();
|
||||||
let format_url = Self::proxied_video(options, &page_url);
|
let format_url = Self::proxied_video(options, &page_url);
|
||||||
@@ -1208,6 +1255,14 @@ impl Provider for ArchivebateProvider {
|
|||||||
if videos.is_empty() {
|
if videos.is_empty() {
|
||||||
return videos;
|
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| {
|
stream::iter(videos.into_iter().map(|video| {
|
||||||
let provider = self.clone();
|
let provider = self.clone();
|
||||||
let options = options.clone();
|
let options = options.clone();
|
||||||
|
|||||||
@@ -229,21 +229,78 @@ impl ArchivebateProxy {
|
|||||||
embed_url: &str,
|
embed_url: &str,
|
||||||
requester: &mut Requester,
|
requester: &mut Requester,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let response = requester
|
// 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(
|
.get_raw_with_headers_timeout(
|
||||||
embed_url,
|
embed_url,
|
||||||
Self::html_headers(detail_url),
|
Self::html_headers(detail_url),
|
||||||
Some(StdDuration::from_secs(8)),
|
Some(StdDuration::from_secs(10)),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let html = response.text().await.ok()?;
|
let html = response.text().await.ok()?;
|
||||||
Self::extract_mixdrop_media_url(&html)
|
return 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<ResolvedMedia> {
|
||||||
|
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<String> = 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 {
|
impl crate::proxies::Proxy for ArchivebateProxy {
|
||||||
@@ -251,41 +308,11 @@ impl crate::proxies::Proxy for ArchivebateProxy {
|
|||||||
let Some(detail_url) = Self::normalize_detail_request(&url) else {
|
let Some(detail_url) = Self::normalize_detail_request(&url) else {
|
||||||
return String::new();
|
return String::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut requester = requester.get_ref().clone();
|
let mut requester = requester.get_ref().clone();
|
||||||
let detail_html = requester
|
resolve_media(&mut requester, &detail_url)
|
||||||
.get_with_headers(
|
|
||||||
&detail_url,
|
|
||||||
Self::html_headers("https://archivebate.com/"),
|
|
||||||
Some(Version::HTTP_11),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.map(|resolved| resolved.media_url)
|
||||||
if detail_html.is_empty() {
|
.unwrap_or_default()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user