mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-10 11:37:21 +00:00
feat(media): support explicit fetch media intent
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ExtensionDownload = { urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, };
|
||||
export type ExtensionDownload = { urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, media: boolean, };
|
||||
|
||||
@@ -57,6 +57,7 @@ export const AddDownloadsModal = () => {
|
||||
pendingAddFilename,
|
||||
pendingAddHeaders,
|
||||
pendingAddCookies,
|
||||
pendingAddMediaUrls,
|
||||
toggleAddModal,
|
||||
addDownload,
|
||||
queues
|
||||
@@ -136,6 +137,7 @@ export const AddDownloadsModal = () => {
|
||||
pendingAddReferer,
|
||||
pendingAddHeaders,
|
||||
pendingAddCookies,
|
||||
pendingAddMediaUrls,
|
||||
baseDownloadFolder,
|
||||
perServerConnections
|
||||
]);
|
||||
@@ -173,10 +175,17 @@ export const AddDownloadsModal = () => {
|
||||
}, [saveLocation, isAddModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const forcedMediaUrls = new Set(pendingAddMediaUrls.map(url => {
|
||||
try {
|
||||
return new URL(url).href;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}));
|
||||
setParsedItems(current =>
|
||||
reconcileDownloadRows(urls, current, pendingAddFilename || undefined)
|
||||
reconcileDownloadRows(urls, current, pendingAddFilename || undefined, forcedMediaUrls)
|
||||
);
|
||||
}, [urls, pendingAddFilename]);
|
||||
}, [urls, pendingAddFilename, pendingAddMediaUrls]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const row of parsedItems) {
|
||||
@@ -205,6 +214,7 @@ export const AddDownloadsModal = () => {
|
||||
const mediaData = await fetchMediaMetadataDeduped({
|
||||
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,
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ type CommandMap = {
|
||||
result: MetadataResponse;
|
||||
};
|
||||
fetch_media_metadata: {
|
||||
args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null; headers: string | null; cookies: string | null; proxy: string | null };
|
||||
args: { url: string; cookieBrowser: string | null; userAgent: 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 };
|
||||
|
||||
@@ -64,6 +64,7 @@ describe('useDownloadStore', () => {
|
||||
pendingAddFilename: '',
|
||||
pendingAddHeaders: '',
|
||||
pendingAddCookies: '',
|
||||
pendingAddMediaUrls: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -487,7 +488,8 @@ describe('useDownloadStore', () => {
|
||||
silent: false,
|
||||
filename: 'file.bin',
|
||||
headers: 'X-Test: value',
|
||||
cookies: 'session=secret'
|
||||
cookies: 'session=secret',
|
||||
media: false
|
||||
});
|
||||
|
||||
const state = useDownloadStore.getState();
|
||||
@@ -495,53 +497,115 @@ describe('useDownloadStore', () => {
|
||||
expect(state.pendingAddUrls).toBe('https://example.com/file.bin');
|
||||
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
||||
expect(state.pendingAddFilename).toBe('file.bin');
|
||||
expect(state.pendingAddHeaders).toBe('X-Test: value');
|
||||
expect(state.pendingAddCookies).toBe('session=secret');
|
||||
});
|
||||
expect(state.pendingAddHeaders).toBe('X-Test: value');
|
||||
expect(state.pendingAddCookies).toBe('session=secret');
|
||||
expect(state.pendingAddMediaUrls).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not reuse stale extension metadata for a later single-link handoff', async () => {
|
||||
useDownloadStore.setState({
|
||||
isAddModalOpen: true,
|
||||
pendingAddUrls: '',
|
||||
pendingAddReferer: 'https://old.example/page',
|
||||
pendingAddFilename: '7aae36e6-00ec-4e7d-8dec-f14ace170bdb',
|
||||
pendingAddHeaders: 'X-Old: value',
|
||||
pendingAddCookies: 'old=session'
|
||||
});
|
||||
pendingAddFilename: '7aae36e6-00ec-4e7d-8dec-f14ace170bdb',
|
||||
pendingAddHeaders: 'X-Old: value',
|
||||
pendingAddCookies: 'old=session',
|
||||
pendingAddMediaUrls: []
|
||||
});
|
||||
|
||||
await useDownloadStore.getState().handleExtensionDownload({
|
||||
urls: ['https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg'],
|
||||
referer: 'https://github.com/center2055/OnionHop/releases/tag/v3.5',
|
||||
silent: false,
|
||||
filename: null,
|
||||
headers: 'User-Agent: Firefox Test',
|
||||
cookies: null
|
||||
});
|
||||
filename: null,
|
||||
headers: 'User-Agent: Firefox Test',
|
||||
cookies: null,
|
||||
media: false
|
||||
});
|
||||
|
||||
const state = useDownloadStore.getState();
|
||||
expect(state.pendingAddUrls).toBe('https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg');
|
||||
expect(state.pendingAddReferer).toBe('https://github.com/center2055/OnionHop/releases/tag/v3.5');
|
||||
expect(state.pendingAddFilename).toBe('');
|
||||
expect(state.pendingAddHeaders).toBe('User-Agent: Firefox Test');
|
||||
expect(state.pendingAddCookies).toBe('');
|
||||
});
|
||||
expect(state.pendingAddHeaders).toBe('User-Agent: Firefox Test');
|
||||
expect(state.pendingAddCookies).toBe('');
|
||||
expect(state.pendingAddMediaUrls).toEqual([]);
|
||||
});
|
||||
|
||||
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',
|
||||
silent: true,
|
||||
filename: 'report.pdf',
|
||||
headers: 'User-Agent: Test',
|
||||
cookies: 'session=secret'
|
||||
});
|
||||
filename: 'report.pdf',
|
||||
headers: 'User-Agent: Test',
|
||||
cookies: 'session=secret',
|
||||
media: false
|
||||
});
|
||||
|
||||
const state = useDownloadStore.getState();
|
||||
expect(state.isAddModalOpen).toBe(true);
|
||||
expect(state.pendingAddUrls).toBe('https://example.com/downloads/report.pdf');
|
||||
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
||||
expect(state.pendingAddFilename).toBe('report.pdf');
|
||||
expect(state.pendingAddHeaders).toBe('User-Agent: Test');
|
||||
expect(state.pendingAddCookies).toBe('session=secret');
|
||||
});
|
||||
expect(state.pendingAddHeaders).toBe('User-Agent: Test');
|
||||
expect(state.pendingAddCookies).toBe('session=secret');
|
||||
expect(state.pendingAddMediaUrls).toEqual([]);
|
||||
});
|
||||
|
||||
it('preserves explicit extension media intent for non-allow-listed pages', async () => {
|
||||
await useDownloadStore.getState().handleExtensionDownload({
|
||||
urls: ['https://adult.example/watch/123'],
|
||||
referer: 'https://adult.example/watch/123',
|
||||
silent: false,
|
||||
filename: null,
|
||||
headers: 'User-Agent: Firefox Test',
|
||||
cookies: 'session=secret',
|
||||
media: true
|
||||
});
|
||||
|
||||
const state = useDownloadStore.getState();
|
||||
expect(state.isAddModalOpen).toBe(true);
|
||||
expect(state.pendingAddUrls).toBe('https://adult.example/watch/123');
|
||||
expect(state.pendingAddMediaUrls).toEqual(['https://adult.example/watch/123']);
|
||||
});
|
||||
|
||||
it('deduplicates forced media URLs and drops stale media intent when opening fresh', async () => {
|
||||
useDownloadStore.setState({
|
||||
isAddModalOpen: true,
|
||||
pendingAddUrls: 'https://adult.example/watch/123',
|
||||
pendingAddMediaUrls: ['https://adult.example/watch/123']
|
||||
});
|
||||
|
||||
await useDownloadStore.getState().handleExtensionDownload({
|
||||
urls: ['https://adult.example/watch/123'],
|
||||
referer: 'https://adult.example/watch/123',
|
||||
silent: false,
|
||||
filename: null,
|
||||
headers: 'User-Agent: Firefox Test',
|
||||
cookies: 'session=secret',
|
||||
media: true
|
||||
});
|
||||
|
||||
expect(useDownloadStore.getState().pendingAddMediaUrls).toEqual([
|
||||
'https://adult.example/watch/123'
|
||||
]);
|
||||
|
||||
useDownloadStore.setState({
|
||||
isAddModalOpen: false,
|
||||
pendingAddMediaUrls: ['https://stale.example/watch']
|
||||
});
|
||||
|
||||
await useDownloadStore.getState().handleExtensionDownload({
|
||||
urls: ['https://example.com/file.bin'],
|
||||
referer: 'https://example.com/page',
|
||||
silent: false,
|
||||
filename: 'file.bin',
|
||||
headers: 'User-Agent: Firefox Test',
|
||||
cookies: null,
|
||||
media: false
|
||||
});
|
||||
|
||||
expect(useDownloadStore.getState().pendingAddMediaUrls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -252,6 +252,7 @@ interface DownloadState {
|
||||
pendingAddFilename: string;
|
||||
pendingAddHeaders: string;
|
||||
pendingAddCookies: string;
|
||||
pendingAddMediaUrls: string[];
|
||||
selectedPropertiesDownloadId: string | null;
|
||||
toggleAddModal: (isOpen: boolean) => void;
|
||||
openAddModalWithUrls: (
|
||||
@@ -259,7 +260,8 @@ interface DownloadState {
|
||||
referer?: string | null,
|
||||
filename?: string | null,
|
||||
headers?: string | null,
|
||||
cookies?: string | null
|
||||
cookies?: string | null,
|
||||
media?: boolean
|
||||
) => void;
|
||||
handleExtensionDownload: (request: ExtensionDownloadRequest) => Promise<void>;
|
||||
deleteModalState: DeleteModalState;
|
||||
@@ -383,6 +385,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
pendingAddFilename: '',
|
||||
pendingAddHeaders: '',
|
||||
pendingAddCookies: '',
|
||||
pendingAddMediaUrls: [],
|
||||
selectedPropertiesDownloadId: null,
|
||||
deleteModalState: { isOpen: false },
|
||||
openDeleteModal: (downloadIds) => set({
|
||||
@@ -398,18 +401,27 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
pendingAddReferer: '',
|
||||
pendingAddFilename: '',
|
||||
pendingAddHeaders: '',
|
||||
pendingAddCookies: ''
|
||||
pendingAddCookies: '',
|
||||
pendingAddMediaUrls: []
|
||||
}),
|
||||
openAddModalWithUrls: (urls, referer, filename, headers, cookies) => set((state) => {
|
||||
openAddModalWithUrls: (urls, referer, filename, headers, cookies, media = false) => set((state) => {
|
||||
const existingUrls = state.isAddModalOpen && state.pendingAddUrls ? state.pendingAddUrls : '';
|
||||
const mergedUrls = existingUrls ? `${existingUrls}\n${urls}` : urls;
|
||||
const existingMediaUrls = state.isAddModalOpen ? state.pendingAddMediaUrls : [];
|
||||
const pendingAddMediaUrls = media
|
||||
? [...new Set([
|
||||
...existingMediaUrls,
|
||||
...urls.split('\n').map(url => url.trim()).filter(Boolean)
|
||||
])]
|
||||
: existingMediaUrls;
|
||||
return {
|
||||
isAddModalOpen: true,
|
||||
pendingAddUrls: mergedUrls,
|
||||
pendingAddReferer: referer?.trim() || '',
|
||||
pendingAddFilename: filename?.trim() || '',
|
||||
pendingAddHeaders: headers?.trim() || '',
|
||||
pendingAddCookies: cookies?.trim() || ''
|
||||
pendingAddCookies: cookies?.trim() || '',
|
||||
pendingAddMediaUrls
|
||||
};
|
||||
}),
|
||||
handleExtensionDownload: async (request) => {
|
||||
@@ -421,7 +433,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
request.referer,
|
||||
urls.length === 1 ? request.filename : null,
|
||||
request.headers,
|
||||
request.cookies
|
||||
request.cookies,
|
||||
request.media === true
|
||||
);
|
||||
},
|
||||
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('add download metadata workflow', () => {
|
||||
'https://example.com/file.zip\nhttps://example.com/new.zip',
|
||||
[existing],
|
||||
undefined,
|
||||
new Set(),
|
||||
() => `new-${nextId++}`
|
||||
);
|
||||
|
||||
@@ -47,12 +48,55 @@ describe('add download metadata workflow', () => {
|
||||
'https://example.com/a\nhttps://example.com/a\nfile:///tmp/private\nnot-a-url',
|
||||
[],
|
||||
undefined,
|
||||
new Set(),
|
||||
() => `row-${nextId++}`
|
||||
);
|
||||
|
||||
expect(rows.map(item => item.status)).toEqual(['loading', 'invalid', 'invalid']);
|
||||
});
|
||||
|
||||
it('forces explicit extension media fetches through media metadata for any http page', () => {
|
||||
const rows = reconcileDownloadRows(
|
||||
'https://adult.example/watch/123',
|
||||
[],
|
||||
undefined,
|
||||
new Set(['https://adult.example/watch/123'])
|
||||
);
|
||||
|
||||
expect(rows[0]).toMatchObject({
|
||||
sourceUrl: 'https://adult.example/watch/123',
|
||||
isMedia: true,
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
||||
it('upgrades an existing normal row when the user explicitly fetches it as media', () => {
|
||||
const existing = row({
|
||||
sourceUrl: 'https://adult.example/watch/123',
|
||||
downloadUrl: 'https://adult.example/watch/123',
|
||||
file: '123',
|
||||
status: 'ready',
|
||||
generation: 2,
|
||||
isMedia: false
|
||||
});
|
||||
|
||||
const rows = reconcileDownloadRows(
|
||||
'https://adult.example/watch/123',
|
||||
[existing],
|
||||
undefined,
|
||||
new Set(['https://adult.example/watch/123'])
|
||||
);
|
||||
|
||||
expect(rows[0]).toMatchObject({
|
||||
sourceUrl: 'https://adult.example/watch/123',
|
||||
isMedia: true,
|
||||
status: 'loading',
|
||||
generation: 3,
|
||||
formats: undefined,
|
||||
selectedFormat: undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes only failed metadata and preserves successful format selection', () => {
|
||||
const ready = row({
|
||||
id: 'ready',
|
||||
|
||||
@@ -71,6 +71,7 @@ export const reconcileDownloadRows = (
|
||||
rawText: string,
|
||||
currentRows: AddDownloadDraftRow[],
|
||||
pendingFilename?: string,
|
||||
forceMediaUrls: ReadonlySet<string> = new Set(),
|
||||
createId: () => string = () => crypto.randomUUID()
|
||||
): AddDownloadDraftRow[] => {
|
||||
const inputs = parseInputLines(rawText);
|
||||
@@ -78,7 +79,19 @@ export const reconcileDownloadRows = (
|
||||
|
||||
return inputs.map(input => {
|
||||
const preserved = existing.get(input.sourceUrl);
|
||||
if (preserved) return preserved;
|
||||
if (preserved) {
|
||||
if (input.valid && forceMediaUrls.has(input.sourceUrl) && !preserved.isMedia) {
|
||||
return {
|
||||
...preserved,
|
||||
status: 'loading',
|
||||
generation: preserved.generation + 1,
|
||||
isMedia: true,
|
||||
formats: undefined,
|
||||
selectedFormat: undefined
|
||||
};
|
||||
}
|
||||
return preserved;
|
||||
}
|
||||
|
||||
const fallback = canonicalizeDownloadFileName(
|
||||
inputs.length === 1 && pendingFilename
|
||||
@@ -93,7 +106,7 @@ export const reconcileDownloadRows = (
|
||||
file: fallback,
|
||||
status: input.valid ? 'loading' : 'invalid',
|
||||
generation: input.valid ? 1 : 0,
|
||||
isMedia: input.valid && isMediaUrl(input.sourceUrl)
|
||||
isMedia: input.valid && (forceMediaUrls.has(input.sourceUrl) || isMediaUrl(input.sourceUrl))
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +18,12 @@ let MEDIA_DOMAINS = [
|
||||
'v.redd.it',
|
||||
'soundcloud.com',
|
||||
'facebook.com',
|
||||
'fb.watch'
|
||||
'fb.watch',
|
||||
'pornhub.com',
|
||||
'redtube.com',
|
||||
'xhamster.com',
|
||||
'xnxx.com',
|
||||
'xvideos.com'
|
||||
];
|
||||
|
||||
const ACTIVE_DOWNLOAD_STATUSES: ReadonlySet<DownloadStatus> = new Set([
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MediaMetadata } from '../bindings/MediaMetadata';
|
||||
type FetchMediaMetadataArgs = {
|
||||
url: string;
|
||||
cookieBrowser: string | null;
|
||||
userAgent: string | null;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
headers: string | null;
|
||||
@@ -17,6 +18,7 @@ const metadataKey = (args: FetchMediaMetadataArgs) =>
|
||||
JSON.stringify([
|
||||
args.url,
|
||||
args.cookieBrowser,
|
||||
args.userAgent,
|
||||
args.username,
|
||||
args.password,
|
||||
args.headers,
|
||||
|
||||
Reference in New Issue
Block a user