From e30eff4d2e6435210ddbb016766a6d228b89e4c5 Mon Sep 17 00:00:00 2001 From: NimBold Date: Tue, 7 Jul 2026 00:17:35 +0330 Subject: [PATCH] feat(media): support extension media fetch Thread extension-provided headers and cookies into Firelink media metadata loading so authenticated page fetches show the correct formats. Include those auth inputs in both metadata and yt-dlp info-json cache keys to prevent cross-session collisions, and advance the Firefox extension submodule to the published media-fetch commit. --- Extensions/Firefox | 2 +- src-tauri/src/lib.rs | 133 ++++++++++++++++++++++++--- src/components/AddDownloadsModal.tsx | 2 + src/ipc.ts | 2 +- src/utils/mediaMetadata.ts | 12 ++- 5 files changed, 136 insertions(+), 15 deletions(-) diff --git a/Extensions/Firefox b/Extensions/Firefox index 6f051ea..19e4b1b 160000 --- a/Extensions/Firefox +++ b/Extensions/Firefox @@ -1 +1 @@ -Subproject commit 6f051ea178c981a851c5f1d4024a30a0c0c08839 +Subproject commit 19e4b1b6d1a8a67b547bbcfe8edd0e7ab9329392 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3a6d13f..a61af29 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,8 +17,21 @@ fn get_metadata_cache() -> &'static std::sync::Mutex> { CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new())) } -fn metadata_info_cache_key(url: &str, cookie_source: Option<&str>, proxy: Option<&str>) -> String { - serde_json::json!([url, cookie_source.unwrap_or(""), proxy.unwrap_or("")]).to_string() +fn metadata_info_cache_key( + url: &str, + cookie_source: Option<&str>, + headers: Option<&str>, + cookies: Option<&str>, + proxy: Option<&str>, +) -> String { + serde_json::json!([ + url, + cookie_source.unwrap_or(""), + headers.unwrap_or(""), + cookies.unwrap_or(""), + proxy.unwrap_or("") + ]) + .to_string() } fn sanitize_metadata_filename(filename: &str) -> Option { @@ -1264,6 +1277,8 @@ fn media_metadata_cache_key( cookie_browser: &Option, username: &Option, password: &Option, + headers: &Option, + cookies: &Option, proxy: &Option, ) -> u64 { let mut hasher = std::collections::hash_map::DefaultHasher::new(); @@ -1271,6 +1286,8 @@ fn media_metadata_cache_key( cookie_browser.hash(&mut hasher); username.hash(&mut hasher); password.hash(&mut hasher); + headers.hash(&mut hasher); + cookies.hash(&mut hasher); proxy.hash(&mut hasher); hasher.finish() } @@ -1305,10 +1322,20 @@ async fn fetch_media_metadata( cookie_browser: Option, username: Option, password: Option, + headers: Option, + cookies: Option, proxy: Option, ) -> Result { validate_url_ssrf(&url).await?; - let cache_key = media_metadata_cache_key(&url, &cookie_browser, &username, &password, &proxy); + let cache_key = media_metadata_cache_key( + &url, + &cookie_browser, + &username, + &password, + &headers, + &cookies, + &proxy, + ); let cache = MEDIA_METADATA_CACHE.get_or_init(|| tokio::sync::Mutex::new(HashMap::new())); let mut cache_guard = cache.lock().await; @@ -1345,6 +1372,8 @@ async fn fetch_media_metadata( cookie_browser.clone(), username.clone(), password.clone(), + headers.clone(), + cookies.clone(), proxy.clone(), ) .await; @@ -1357,7 +1386,10 @@ async fn fetch_media_metadata( "yt-dlp could not read browser cookies from {}; retrying media metadata without browser cookies", browser ); - fetch_media_metadata_uncached(app_handle, url, None, username, password, proxy).await + fetch_media_metadata_uncached( + app_handle, url, None, username, password, headers, cookies, proxy, + ) + .await } (result, _) => result, }; @@ -1397,9 +1429,17 @@ async fn fetch_media_metadata_uncached( cookie_browser: Option, username: Option, password: Option, + headers: Option, + cookies: Option, proxy: Option, ) -> Result { - let info_cache_key = metadata_info_cache_key(&url, cookie_browser.as_deref(), proxy.as_deref()); + let info_cache_key = metadata_info_cache_key( + &url, + cookie_browser.as_deref(), + headers.as_deref(), + cookies.as_deref(), + proxy.as_deref(), + ); // Pass bundled tools by absolute path so extraction never depends on // system Python, a user-managed PATH, or auto-detection heuristics. @@ -1465,6 +1505,7 @@ async fn fetch_media_metadata_uncached( append_ytdlp_config_option(&mut config_content, "--password", &pass); } } + append_ytdlp_http_headers(&mut config_content, headers.as_deref(), cookies.as_deref())?; use std::io::Write; config_file .write_all(config_content.as_bytes()) @@ -2776,8 +2817,13 @@ pub(crate) async fn start_media_download_internal( let mut temp_info_path = None; if let Ok(mut cache) = get_metadata_cache().lock() { - let info_cache_key = - metadata_info_cache_key(&url, effective_cookie_source.as_deref(), proxy.as_deref()); + let info_cache_key = metadata_info_cache_key( + &url, + effective_cookie_source.as_deref(), + headers.as_deref(), + cookies.as_deref(), + proxy.as_deref(), + ); if let Some(json_str) = cache.remove(&info_cache_key) { let temp_dir = std::env::temp_dir(); let path = temp_dir.join(format!("firelink_ytdlp_{}.info.json", id)); @@ -4461,11 +4507,12 @@ mod tests { aggregate_media_fraction, append_ytdlp_http_headers, build_media_format_options, collect_download_uris, filename_from_content_disposition, filename_from_url_disposition_query, filename_from_url_path, is_excluded_yt_dlp_format, - is_browser_cookie_extraction_error, json_lower, media_output_template, - media_progress_speed, normalize_speed_limit_for_aria2, parse_firelink_deep_link, - parse_ffmpeg_version, parse_media_progress_line, redact_log_line, - sanitize_ytdlp_config_value, should_cleanup_media_artifacts_after_failure, - FirelinkDeepLink, MediaProgress, MEDIA_PROGRESS_PREFIX, + is_browser_cookie_extraction_error, json_lower, media_metadata_cache_key, + metadata_info_cache_key, media_output_template, media_progress_speed, + normalize_speed_limit_for_aria2, parse_firelink_deep_link, parse_ffmpeg_version, + parse_media_progress_line, redact_log_line, sanitize_ytdlp_config_value, + should_cleanup_media_artifacts_after_failure, FirelinkDeepLink, MediaProgress, + MEDIA_PROGRESS_PREFIX, }; use serde_json::json; use std::time::{Duration, Instant}; @@ -4518,6 +4565,68 @@ mod tests { assert!(error.contains("invalid HTTP header")); } + #[test] + fn media_metadata_cache_key_includes_request_headers_and_cookies() { + let base = media_metadata_cache_key( + "https://example.com/watch?v=1", + &Some("firefox".to_string()), + &None, + &None, + &Some("User-Agent: Browser A".to_string()), + &Some("session=one".to_string()), + &None, + ); + let changed_headers = media_metadata_cache_key( + "https://example.com/watch?v=1", + &Some("firefox".to_string()), + &None, + &None, + &Some("User-Agent: Browser B".to_string()), + &Some("session=one".to_string()), + &None, + ); + let changed_cookies = media_metadata_cache_key( + "https://example.com/watch?v=1", + &Some("firefox".to_string()), + &None, + &None, + &Some("User-Agent: Browser A".to_string()), + &Some("session=two".to_string()), + &None, + ); + + assert_ne!(base, changed_headers); + assert_ne!(base, changed_cookies); + } + + #[test] + fn metadata_info_cache_key_includes_request_headers_and_cookies() { + let base = metadata_info_cache_key( + "https://example.com/watch?v=1", + Some("firefox"), + Some("User-Agent: Browser A"), + Some("session=one"), + None, + ); + let changed_headers = metadata_info_cache_key( + "https://example.com/watch?v=1", + Some("firefox"), + Some("User-Agent: Browser B"), + Some("session=one"), + None, + ); + let changed_cookies = metadata_info_cache_key( + "https://example.com/watch?v=1", + Some("firefox"), + Some("User-Agent: Browser A"), + Some("session=two"), + None, + ); + + assert_ne!(base, changed_headers); + assert_ne!(base, changed_cookies); + } + #[test] fn retryable_media_failures_preserve_resumable_artifacts() { assert!(!should_cleanup_media_artifacts_after_failure( diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 0704882..00eb84c 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -207,6 +207,8 @@ export const AddDownloadsModal = () => { cookieBrowser: browserArg, username: useAuth ? username.trim() || null : login?.username || null, password: useAuth ? password || null : keychainPassword, + headers: headers?.trim() || null, + cookies: cookies?.trim() || null, proxy }); if (mediaData && mediaData.formats.length > 0) { diff --git a/src/ipc.ts b/src/ipc.ts index e1ec018..76947da 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -21,7 +21,7 @@ type CommandMap = { result: MetadataResponse; }; fetch_media_metadata: { - args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null; proxy: string | null }; + args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null; headers: string | null; cookies: string | null; proxy: string | null }; result: MediaMetadata; }; get_aria2_engine_status: { args: undefined; result: EngineStatusItem }; diff --git a/src/utils/mediaMetadata.ts b/src/utils/mediaMetadata.ts index 5cd3cd5..a7a5326 100644 --- a/src/utils/mediaMetadata.ts +++ b/src/utils/mediaMetadata.ts @@ -6,13 +6,23 @@ type FetchMediaMetadataArgs = { cookieBrowser: string | null; username: string | null; password: string | null; + headers: string | null; + cookies: string | null; proxy: string | null; }; const inFlightMediaMetadata = new Map>(); const metadataKey = (args: FetchMediaMetadataArgs) => - JSON.stringify([args.url, args.cookieBrowser, args.username, args.password, args.proxy]); + JSON.stringify([ + args.url, + args.cookieBrowser, + args.username, + args.password, + args.headers, + args.cookies, + args.proxy + ]); export const fetchMediaMetadataDeduped = (args: FetchMediaMetadataArgs): Promise => { const key = metadataKey(args);