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
+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) => {