From fbb89cde8ecf8656f169a97c07db6fe907431162 Mon Sep 17 00:00:00 2001 From: NimBold Date: Thu, 9 Jul 2026 23:21:16 +0330 Subject: [PATCH] fix(downloads): harden capture and media flows Scope extension request context to each Add modal row, refresh stale metadata handoffs, and align yt-dlp format and retry behavior with Firelink's transfer contract. --- src-tauri/src/lib.rs | 47 ++++- src/components/AddDownloadsModal.tsx | 237 ++++++++++++++------------ src/store/useDownloadStore.test.ts | 49 +++++- src/store/useDownloadStore.ts | 63 ++++++- src/utils/addDownloadMetadata.test.ts | 53 ++++++ src/utils/addDownloadMetadata.ts | 25 ++- 6 files changed, 342 insertions(+), 132 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2938e79..7274a91 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -536,7 +536,7 @@ fn build_media_format_options( let mut options = Vec::new(); if has_video { - let available_heights: Vec = [2160_u64, 1440, 1080, 720, 480, 360] + let available_heights: Vec = [4320_u64, 2160, 1440, 1080, 720, 480, 360, 240, 144] .into_iter() .filter(|height| { clean_formats @@ -2876,9 +2876,15 @@ pub(crate) async fn start_media_download_internal( .arg("--socket-timeout") .arg("20") .arg("--retries") - .arg(max_retries.to_string()) + // Firelink owns the retry budget so `maxAutomaticRetries` means + // the same thing for aria2 and media downloads. Letting yt-dlp + // also consume that value would multiply the configured retry + // count on every outer restart. + .arg("0") .arg("--extractor-retries") - .arg("3") + .arg("0") + .arg("--fragment-retries") + .arg("0") .arg("--ffmpeg-location") .arg(&ffmpeg_path) .arg("--js-runtimes") @@ -2943,7 +2949,15 @@ pub(crate) async fn start_media_download_internal( } else if safe_filename.ends_with(".webm") { cmd = cmd.arg("--merge-output-format").arg("webm"); } else { - cmd = cmd.arg("--merge-output-format").arg("mkv"); + // `--merge-output-format` only affects split video/audio + // selections. A progressive MP4/WebM stream already includes + // audio, so also request remuxing to keep an MKV option from + // producing a mismatched file extension and container. + cmd = cmd + .arg("--merge-output-format") + .arg("mkv") + .arg("--remux-video") + .arg("mkv"); } } @@ -4984,6 +4998,31 @@ mod tests { assert_eq!(option.filesize_approx, Some(1_468_000_000)); } + #[test] + fn keeps_low_and_ultra_high_available_video_qualities() { + let formats = vec![ + json!({ + "format_id": "401", + "ext": "webm", + "height": 4320, + "vcodec": "av01.0.17M.08", + "acodec": "none" + }), + json!({ + "format_id": "17", + "ext": "mp4", + "height": 144, + "vcodec": "avc1.42E01E", + "acodec": "mp4a.40.2" + }) + ]; + + let options = build_media_format_options(&formats, Some(60.0)); + + assert!(options.iter().any(|format| format.resolution == "4320p")); + assert!(options.iter().any(|format| format.resolution == "144p")); + } + #[test] fn returns_no_media_options_for_webpage_only_metadata() { let formats = vec![json!({ diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 914ef9e..9e299f0 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -3,7 +3,8 @@ import { useDownloadStore, getSiteLogin, getProxyArgs, - type AddDownloadAction + type AddDownloadAction, + type PendingAddRequestContext } from '../store/useDownloadStore'; import { useSettingsStore } from '../store/useSettingsStore'; import { FolderPlus, Settings, Shield, RefreshCw, FileText, HardDrive, Database, Link, ArrowRight, Play, ChevronDown, ChevronRight, Video, Film, Music, type LucideIcon } from 'lucide-react'; @@ -39,16 +40,6 @@ const formatBytes = (bytes: number) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; -const isCrossHostRedirect = (sourceUrl: string, downloadUrl: string) => { - try { - const sourceHost = new URL(sourceUrl).hostname; - const downloadHost = new URL(downloadUrl).hostname; - return downloadHost !== sourceHost && !downloadHost.endsWith(`.${sourceHost}`); - } catch { - return false; - } -}; - const normalizeComparableUrl = (rawUrl: string) => { try { return new URL(rawUrl).href; @@ -57,6 +48,11 @@ const normalizeComparableUrl = (rawUrl: string) => { } }; +const extensionHeaders = (context: PendingAddRequestContext | undefined) => [ + context?.referer ? `Referer: ${context.referer.replace(/[\r\n]/g, '')}` : '', + context?.headers +].filter(Boolean).join('\n'); + export const AddDownloadsModal = () => { const { addToast } = useToast(); const { @@ -67,6 +63,9 @@ export const AddDownloadsModal = () => { pendingAddHeaders, pendingAddCookies, pendingAddMediaUrls, + pendingAddRequestContexts, + pendingAddRequestVersion, + pendingAddLatestUrls, toggleAddModal, addDownload, queues @@ -106,43 +105,76 @@ export const AddDownloadsModal = () => { const [checksumValue, setChecksumValue] = useState(''); const [headers, setHeaders] = useState(''); const [cookies, setCookies] = useState(''); - const headersFromExtensionRef = useRef(false); - const cookiesFromExtensionRef = useRef(false); + const headersManuallyEditedRef = useRef(false); + const cookiesManuallyEditedRef = useRef(false); + const modalSessionRef = useRef(false); + const observedRequestVersionRef = useRef(0); const [mirrors, setMirrors] = useState(''); + const requestContextForUrl = (url: string) => + pendingAddRequestContexts[normalizeComparableUrl(url)]; + const hasExtensionRequestContext = Object.keys(pendingAddRequestContexts).length > 0; + const headersForRow = (sourceUrl: string) => { + if (headersManuallyEditedRef.current) return headers.trim(); + const context = requestContextForUrl(sourceUrl); + if (context) return extensionHeaders(context).trim(); + return hasExtensionRequestContext ? '' : headers.trim(); + }; + const cookiesForRow = (sourceUrl: string) => { + if (cookiesManuallyEditedRef.current) return cookies.trim(); + const context = requestContextForUrl(sourceUrl); + if (context) return context.cookies.trim(); + return hasExtensionRequestContext ? '' : cookies.trim(); + }; + const suggestedFilenameForRow = (sourceUrl: string) => { + const context = requestContextForUrl(sourceUrl); + if (context?.filename) return context.filename; + return hasExtensionRequestContext ? '' : pendingAddFilename; + }; + useEffect(() => { - if (isAddModalOpen) { - setSaveLocation(baseDownloadFolder); - setIsSaveLocationManual(false); - setUrls(pendingAddUrls || ''); - setParsedItems([]); - setSelectedItemIndex(null); - setPendingUseSharedDestination(false); - setPendingDestinationOverrides({}); - setConnections(perServerConnections); - setSpeedLimitEnabled(false); - setSpeedLimit('1024'); - setUseAuth(false); - setUsername(''); - setPassword(''); - setAdvancedExpanded(false); - setChecksumEnabled(false); - setChecksumAlgo('SHA-256'); - setChecksumValue(''); - const extensionHeaders = [ - pendingAddReferer ? `Referer: ${pendingAddReferer.replace(/[\r\n]/g, '')}` : '', - pendingAddHeaders - ].filter(Boolean).join('\n'); - setHeaders(extensionHeaders); - headersFromExtensionRef.current = Boolean(extensionHeaders.trim()); - setCookies(pendingAddCookies); - cookiesFromExtensionRef.current = Boolean(pendingAddCookies?.trim()); - setMirrors(''); - setIsQueueMenuOpen(false); - setIsSubmitting(false); - } else { + if (!isAddModalOpen) { + modalSessionRef.current = false; setUrls(''); + return; } + + if (modalSessionRef.current) return; + modalSessionRef.current = true; + const initialUrls = pendingAddUrls || ''; + const initialUrlLines = initialUrls.split('\n').map(url => url.trim()).filter(Boolean); + observedRequestVersionRef.current = pendingAddRequestVersion; + const initialContext = initialUrlLines.length === 1 + ? requestContextForUrl(initialUrlLines[0]) + : undefined; + + setSaveLocation(baseDownloadFolder); + setIsSaveLocationManual(false); + setUrls(initialUrls); + setParsedItems([]); + setSelectedItemIndex(null); + setPendingUseSharedDestination(false); + setPendingDestinationOverrides({}); + setConnections(perServerConnections); + setSpeedLimitEnabled(false); + setSpeedLimit('1024'); + setUseAuth(false); + setUsername(''); + setPassword(''); + setAdvancedExpanded(false); + setChecksumEnabled(false); + setChecksumAlgo('SHA-256'); + setChecksumValue(''); + setHeaders(initialContext ? extensionHeaders(initialContext) : [ + pendingAddReferer ? `Referer: ${pendingAddReferer.replace(/[\r\n]/g, '')}` : '', + pendingAddHeaders + ].filter(Boolean).join('\n')); + headersManuallyEditedRef.current = false; + setCookies(initialContext?.cookies || pendingAddCookies); + cookiesManuallyEditedRef.current = false; + setMirrors(''); + setIsQueueMenuOpen(false); + setIsSubmitting(false); }, [ isAddModalOpen, pendingAddUrls, @@ -154,6 +186,18 @@ export const AddDownloadsModal = () => { perServerConnections ]); + useEffect(() => { + if (!isAddModalOpen || !modalSessionRef.current + || observedRequestVersionRef.current === pendingAddRequestVersion) return; + observedRequestVersionRef.current = pendingAddRequestVersion; + const additions = pendingAddLatestUrls + .split('\n') + .map(url => url.trim()) + .filter(Boolean); + if (additions.length === 0) return; + setUrls(current => current.trim() ? `${current.trim()}\n${additions.join('\n')}` : additions.join('\n')); + }, [isAddModalOpen, pendingAddRequestVersion, pendingAddLatestUrls]); + useEffect(() => { if (!isQueueMenuOpen) return; const closeMenu = (event: PointerEvent) => { @@ -194,10 +238,33 @@ export const AddDownloadsModal = () => { return url; } })); - setParsedItems(current => - reconcileDownloadRows(urls, current, pendingAddFilename || undefined, forcedMediaUrls) + const requestFilenames = Object.fromEntries( + Object.entries(pendingAddRequestContexts) + .filter(([, context]) => Boolean(context.filename)) + .map(([url, context]) => [url, context.filename]) ); - }, [urls, pendingAddFilename, pendingAddMediaUrls]); + const requestContextVersions = Object.fromEntries( + Object.entries(pendingAddRequestContexts) + .map(([url, context]) => [url, context.version]) + ); + setParsedItems(current => + reconcileDownloadRows( + urls, + current, + hasExtensionRequestContext ? undefined : pendingAddFilename || undefined, + forcedMediaUrls, + undefined, + requestFilenames, + requestContextVersions + ) + ); + }, [ + urls, + pendingAddFilename, + pendingAddMediaUrls, + pendingAddRequestContexts, + hasExtensionRequestContext + ]); useEffect(() => { for (const row of parsedItems) { @@ -223,67 +290,19 @@ export const AddDownloadsModal = () => { } } + const rowHeaders = headersForRow(row.sourceUrl); + const rowCookies = cookiesForRow(row.sourceUrl); const mediaMetadataArgs = { url: row.sourceUrl, cookieBrowser: browserArg, userAgent: settingsStore.customUserAgent.trim() || null, username: useAuth ? username.trim() || null : login?.username || null, password: useAuth ? password || null : keychainPassword, - headers: headers?.trim() || null, - cookies: cookies?.trim() || null, + headers: rowHeaders || null, + cookies: rowCookies || null, proxy }; - const cleanMediaMetadataArgs = { - ...mediaMetadataArgs, - headers: null, - cookies: null - }; - const isExtensionMediaRow = pendingAddMediaUrls - .map(normalizeComparableUrl) - .includes(normalizeComparableUrl(row.sourceUrl)); - const hasExtensionContext = - headersFromExtensionRef.current || cookiesFromExtensionRef.current; - let mediaData; - if (isExtensionMediaRow && hasExtensionContext) { - try { - mediaData = await fetchMediaMetadataDeduped(cleanMediaMetadataArgs); - if (headersFromExtensionRef.current) { - setHeaders(''); - headersFromExtensionRef.current = false; - } - if (cookiesFromExtensionRef.current) { - setCookies(''); - cookiesFromExtensionRef.current = false; - } - } catch (cleanError) { - console.warn( - 'Media metadata failed without extension browser context; retrying with extension headers/cookies', - cleanError - ); - mediaData = await fetchMediaMetadataDeduped(mediaMetadataArgs); - } - } else { - try { - mediaData = await fetchMediaMetadataDeduped(mediaMetadataArgs); - } catch (error) { - if (!hasExtensionContext) { - throw error; - } - console.warn( - 'Media metadata failed with extension browser context; retrying without extension headers/cookies', - error - ); - mediaData = await fetchMediaMetadataDeduped(cleanMediaMetadataArgs); - if (headersFromExtensionRef.current) { - setHeaders(''); - headersFromExtensionRef.current = false; - } - if (cookiesFromExtensionRef.current) { - setCookies(''); - cookiesFromExtensionRef.current = false; - } - } - } + const mediaData = await fetchMediaMetadataDeduped(mediaMetadataArgs); if (mediaData && mediaData.formats.length > 0) { const mappedFormats = mediaData.formats.map(f => { const quality = f.resolution || 'Video'; @@ -337,15 +356,11 @@ export const AddDownloadsModal = () => { userAgent: settingsStore.customUserAgent.trim() || null, username: useAuth ? username.trim() || null : login?.username || null, password: useAuth ? password || null : keychainPassword, - headers: headers?.trim() || null, - cookies: cookies?.trim() || null, + headers: headersForRow(row.sourceUrl) || null, + cookies: cookiesForRow(row.sourceUrl) || null, proxy }); const nextDownloadUrl = meta.url || row.sourceUrl; - if (cookiesFromExtensionRef.current && isCrossHostRedirect(row.sourceUrl, nextDownloadUrl)) { - setCookies(''); - cookiesFromExtensionRef.current = false; - } setParsedItems(current => updateRowIfCurrent( current, row.id, @@ -355,8 +370,8 @@ export const AddDownloadsModal = () => { ...currentRow, downloadUrl: nextDownloadUrl || currentRow.downloadUrl, file: canonicalizeDownloadFileName( - current.length === 1 && pendingAddFilename - ? pendingAddFilename + current.length === 1 && suggestedFilenameForRow(row.sourceUrl) + ? suggestedFilenameForRow(row.sourceUrl) : meta.filename ), size: meta.size_bytes ? meta.size : undefined, @@ -690,11 +705,11 @@ export const AddDownloadsModal = () => { speedLimit: speedLimitEnabled ? `${speedLimit}K` : '0', username: useAuth ? username.trim() : undefined, password: useAuth ? password.trim() : undefined, - headers: headers.trim() || undefined, + headers: headersForRow(item.sourceUrl) || undefined, checksum: checksumEnabled && checksumValue.trim() ? `${checksumAlgo}=${checksumValue.trim()}` : undefined, - cookies: cookies.trim() || undefined, + cookies: cookiesForRow(item.sourceUrl) || undefined, mirrors: mirrors.trim() || undefined, destination: useSharedDestination ? finalLocation @@ -1089,7 +1104,7 @@ export const AddDownloadsModal = () => {