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
+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))
};
});