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.
This commit is contained in:
NimBold
2026-07-09 23:21:16 +03:30
parent cd8ab5c12b
commit fbb89cde8e
6 changed files with 342 additions and 132 deletions
+43 -4
View File
@@ -536,7 +536,7 @@ fn build_media_format_options(
let mut options = Vec::new();
if has_video {
let available_heights: Vec<u64> = [2160_u64, 1440, 1080, 720, 480, 360]
let available_heights: Vec<u64> = [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!({
+126 -111
View File
@@ -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 = () => {
<textarea
value={headers}
onChange={e => {
headersFromExtensionRef.current = false;
headersManuallyEditedRef.current = true;
setHeaders(e.target.value);
}}
className="add-download-control w-full h-12 px-3 py-1.5 text-xs font-mono resize-none"
@@ -1102,7 +1117,7 @@ export const AddDownloadsModal = () => {
type="text"
value={cookies}
onChange={e => {
cookiesFromExtensionRef.current = false;
cookiesManuallyEditedRef.current = true;
setCookies(e.target.value);
}}
placeholder="name=value; other=value"
+48 -1
View File
@@ -88,6 +88,9 @@ describe('useDownloadStore', () => {
pendingAddHeaders: '',
pendingAddCookies: '',
pendingAddMediaUrls: [],
pendingAddRequestContexts: {},
pendingAddRequestVersion: 0,
pendingAddLatestUrls: '',
});
});
@@ -616,7 +619,7 @@ describe('useDownloadStore', () => {
expect(state.pendingAddMediaUrls).toEqual([]);
});
it('routes silent extension captures to the Add Modal instead of queuing immediately', async () => {
it('routes silent extension captures to the Add Modal instead of queuing immediately', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://example.com/downloads/report.pdf'],
referer: 'https://example.com/page',
@@ -637,6 +640,50 @@ describe('useDownloadStore', () => {
expect(state.pendingAddMediaUrls).toEqual([]);
});
it('keeps each extension handoff context attached to its own URL while the Add Modal is open', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://first.example/file.zip'],
referer: 'https://first.example/page',
silent: true,
filename: 'first.zip',
headers: 'User-Agent: First Browser',
cookies: 'first=session',
media: false
});
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://second.example/file.zip'],
referer: 'https://second.example/page',
silent: true,
filename: 'second.zip',
headers: 'User-Agent: Second Browser',
cookies: 'second=session',
media: false
});
const state = useDownloadStore.getState();
expect(state.pendingAddUrls).toBe(
'https://first.example/file.zip\nhttps://second.example/file.zip'
);
expect(state.pendingAddRequestVersion).toBe(2);
expect(state.pendingAddLatestUrls).toBe('https://second.example/file.zip');
expect(state.pendingAddRequestContexts).toEqual({
'https://first.example/file.zip': {
version: 1,
referer: 'https://first.example/page',
filename: 'first.zip',
headers: 'User-Agent: First Browser',
cookies: 'first=session'
},
'https://second.example/file.zip': {
version: 2,
referer: 'https://second.example/page',
filename: 'second.zip',
headers: 'User-Agent: Second Browser',
cookies: 'second=session'
}
});
});
it('preserves explicit extension media intent for non-allow-listed pages', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://adult.example/watch/123'],
+55 -8
View File
@@ -237,6 +237,13 @@ export type AddDownloadAction =
| { type: 'start-now' }
| { type: 'add-to-queue'; queueId: string };
export type DownloadDraft = Omit<DownloadItem, 'status' | 'queueId' | 'hasBeenDispatched'>;
export type PendingAddRequestContext = {
version: number;
referer: string;
filename: string;
headers: string;
cookies: string;
};
export type DeleteModalState = {
isOpen: boolean;
@@ -261,6 +268,9 @@ interface DownloadState {
pendingAddHeaders: string;
pendingAddCookies: string;
pendingAddMediaUrls: string[];
pendingAddRequestContexts: Record<string, PendingAddRequestContext>;
pendingAddRequestVersion: number;
pendingAddLatestUrls: string;
selectedPropertiesDownloadId: string | null;
toggleAddModal: (isOpen: boolean) => void;
openAddModalWithUrls: (
@@ -394,6 +404,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
pendingAddHeaders: '',
pendingAddCookies: '',
pendingAddMediaUrls: [],
pendingAddRequestContexts: {},
pendingAddRequestVersion: 0,
pendingAddLatestUrls: '',
selectedPropertiesDownloadId: null,
deleteModalState: { isOpen: false },
openDeleteModal: (downloadIds) => set({
@@ -410,26 +423,60 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
pendingAddFilename: '',
pendingAddHeaders: '',
pendingAddCookies: '',
pendingAddMediaUrls: []
pendingAddMediaUrls: [],
pendingAddRequestContexts: {},
pendingAddLatestUrls: ''
}),
openAddModalWithUrls: (urls, referer, filename, headers, cookies, media = false) => set((state) => {
const existingUrls = state.isAddModalOpen && state.pendingAddUrls ? state.pendingAddUrls : '';
const isAppending = state.isAddModalOpen && Boolean(state.pendingAddUrls);
const existingUrls = isAppending ? state.pendingAddUrls : '';
const mergedUrls = existingUrls ? `${existingUrls}\n${urls}` : urls;
const existingMediaUrls = state.isAddModalOpen ? state.pendingAddMediaUrls : [];
const existingMediaUrls = isAppending ? state.pendingAddMediaUrls : [];
const pendingAddMediaUrls = media
? [...new Set([
...existingMediaUrls,
...urls.split('\n').map(url => url.trim()).filter(Boolean)
])]
: existingMediaUrls;
const cleanReferer = referer?.trim() || '';
const cleanFilename = filename?.trim() || '';
const cleanHeaders = headers?.trim() || '';
const cleanCookies = cookies?.trim() || '';
const requestVersion = state.pendingAddRequestVersion + 1;
const hasRequestContext = Boolean(cleanReferer || cleanFilename || cleanHeaders || cleanCookies);
const pendingAddRequestContexts = isAppending
? { ...state.pendingAddRequestContexts }
: {};
if (hasRequestContext) {
for (const rawUrl of urls.split('\n')) {
const trimmedUrl = rawUrl.trim();
if (!trimmedUrl) continue;
let key = trimmedUrl;
try {
key = new URL(trimmedUrl).href;
} catch {
// The Add modal will mark malformed input invalid; retain its original key here.
}
pendingAddRequestContexts[key] = {
version: requestVersion,
referer: cleanReferer,
filename: cleanFilename,
headers: cleanHeaders,
cookies: cleanCookies
};
}
}
return {
isAddModalOpen: true,
pendingAddUrls: mergedUrls,
pendingAddReferer: referer?.trim() || '',
pendingAddFilename: filename?.trim() || '',
pendingAddHeaders: headers?.trim() || '',
pendingAddCookies: cookies?.trim() || '',
pendingAddMediaUrls
pendingAddReferer: cleanReferer,
pendingAddFilename: cleanFilename,
pendingAddHeaders: cleanHeaders,
pendingAddCookies: cleanCookies,
pendingAddMediaUrls,
pendingAddRequestContexts,
pendingAddRequestVersion: requestVersion,
pendingAddLatestUrls: urls
};
}),
handleExtensionDownload: async (request) => {
+53
View File
@@ -71,6 +71,59 @@ describe('add download metadata workflow', () => {
});
});
it('keeps extension-provided filenames scoped to their individual URLs', () => {
const rows = reconcileDownloadRows(
'https://first.example/download\nhttps://second.example/download',
[],
undefined,
new Set(),
() => crypto.randomUUID(),
{
'https://first.example/download': 'first.zip',
'https://second.example/download': 'second.zip'
}
);
expect(rows.map(item => item.file)).toEqual(['first.zip', 'second.zip']);
});
it('refreshes an existing row when a newer extension handoff changes its request context', () => {
const existing = row({
isMedia: true,
status: 'ready',
generation: 4,
requestContextVersion: 1,
formats: [{
name: '1080p MP4',
selector: '137+140',
ext: 'mp4',
formatLabel: 'MP4',
detail: '10 MB',
type: 'Video',
bytes: 10
}],
selectedFormat: 0
});
const refreshed = reconcileDownloadRows(
existing.sourceUrl,
[existing],
undefined,
new Set(),
() => 'unused',
{},
{ [existing.sourceUrl]: 2 }
);
expect(refreshed[0]).toMatchObject({
status: 'loading',
generation: 5,
requestContextVersion: 2,
formats: undefined,
selectedFormat: undefined
});
});
it('upgrades an existing normal row when the user explicitly fetches it as media', () => {
const existing = row({
sourceUrl: 'https://adult.example/watch/123',
+17 -8
View File
@@ -26,6 +26,7 @@ export interface AddDownloadDraftRow {
sizeBytes?: number;
status: MetadataStatus;
generation: number;
requestContextVersion?: number;
isMedia: boolean;
resumable?: boolean;
formats?: AddMediaFormat[];
@@ -72,7 +73,9 @@ export const reconcileDownloadRows = (
currentRows: AddDownloadDraftRow[],
pendingFilename?: string,
forceMediaUrls: ReadonlySet<string> = new Set(),
createId: () => string = () => crypto.randomUUID()
createId: () => string = () => crypto.randomUUID(),
requestFilenames: Readonly<Record<string, string>> = {},
requestContextVersions: Readonly<Record<string, number>> = {}
): AddDownloadDraftRow[] => {
const inputs = parseInputLines(rawText);
const existing = new Map(currentRows.map(row => [row.sourceUrl, row]));
@@ -80,23 +83,28 @@ export const reconcileDownloadRows = (
return inputs.map(input => {
const preserved = existing.get(input.sourceUrl);
if (preserved) {
if (input.valid && forceMediaUrls.has(input.sourceUrl) && !preserved.isMedia) {
const forcedMedia = input.valid && forceMediaUrls.has(input.sourceUrl);
const requestContextVersion = requestContextVersions[input.sourceUrl];
const contextChanged = requestContextVersion !== undefined
&& requestContextVersion !== preserved.requestContextVersion;
if ((forcedMedia && !preserved.isMedia) || contextChanged) {
return {
...preserved,
status: 'loading',
generation: preserved.generation + 1,
isMedia: true,
formats: undefined,
selectedFormat: undefined
requestContextVersion,
isMedia: preserved.isMedia || forcedMedia,
formats: preserved.isMedia || forcedMedia ? undefined : preserved.formats,
selectedFormat: preserved.isMedia || forcedMedia ? undefined : preserved.selectedFormat
};
}
return preserved;
}
const requestedFilename = requestFilenames[input.sourceUrl]
|| (inputs.length === 1 ? pendingFilename : undefined);
const fallback = canonicalizeDownloadFileName(
inputs.length === 1 && pendingFilename
? pendingFilename
: fileNameFromUrl(input.sourceUrl)
requestedFilename || fileNameFromUrl(input.sourceUrl)
);
return {
@@ -106,6 +114,7 @@ export const reconcileDownloadRows = (
file: fallback,
status: input.valid ? 'loading' : 'invalid',
generation: input.valid ? 1 : 0,
requestContextVersion: requestContextVersions[input.sourceUrl],
isMedia: input.valid && (forceMediaUrls.has(input.sourceUrl) || isMediaUrl(input.sourceUrl))
};
});