mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-04 00:18:41 +00:00
fix(download): require aria2 for file transfers
Update yt-dlp to 2026.07.04 and refresh the bundled macOS runtime plus engine locks. Route every non-media download through aria2 by removing the native HTTP fallback path, native GID handling, and the old direct-download harness. Retry transient aria2 startup/RPC failures before failing, then show the real last error in the download row and Properties modal so Windows failures are diagnosable instead of silent. Refresh docs and tests around the aria2-only file-download contract, and advance the Firefox extension submodule to its published wording cleanup.
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
import type { DownloadCategory } from "./DownloadCategory";
|
||||
import type { DownloadStatus } from "./DownloadStatus";
|
||||
|
||||
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, };
|
||||
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, };
|
||||
|
||||
@@ -132,7 +132,9 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
key={`status-${download.status}`}
|
||||
ref={statusTextRef}
|
||||
title={
|
||||
(download.status === 'queued' || download.status === 'staged') && queueIndex !== -1
|
||||
download.lastError && (download.status === 'failed' || download.status === 'retrying')
|
||||
? download.lastError
|
||||
: (download.status === 'queued' || download.status === 'staged') && queueIndex !== -1
|
||||
? `${download.status === 'staged' ? 'In queue' : 'Queued'} #${queueIndex + 1}`
|
||||
: download.status === 'downloading'
|
||||
? `${((download.fraction || 0) * 100).toFixed(0)}%`
|
||||
|
||||
@@ -206,8 +206,14 @@ export const PropertiesModal = () => {
|
||||
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[55px] shrink-0">Category</span><span className="text-text-secondary truncate">{item.category}</span></div>
|
||||
<div className="flex gap-1.5"><span className="text-text-muted font-medium w-[50px]">Last try</span><span className="text-text-secondary truncate">-</span></div>
|
||||
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[90px]">Date added</span><span className="text-text-secondary truncate">{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}</span></div>
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[70px]">Destination</span><span className="text-text-secondary truncate" title={saveLocation}>{saveLocation || baseDownloadFolder}</span></div>
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[90px]">Date added</span><span className="text-text-secondary truncate">{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}</span></div>
|
||||
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[70px]">Destination</span><span className="text-text-secondary truncate" title={saveLocation}>{saveLocation || baseDownloadFolder}</span></div>
|
||||
{item.lastError && (item.status === 'failed' || item.status === 'retrying') && (
|
||||
<div className="flex gap-1.5 col-span-4 min-w-0">
|
||||
<span className="text-text-muted font-medium w-[90px] shrink-0">Last error</span>
|
||||
<span className="text-red-400 truncate" title={item.lastError}>{item.lastError}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function SpeedLimiterView() {
|
||||
<Gauge size={18} className="text-accent" /> Global Speed Limit
|
||||
</div>
|
||||
<p className="max-w-2xl text-[12px] leading-relaxed text-text-muted">
|
||||
Applies to new and active aria2 transfers, native fallback transfers, and yt-dlp media downloads. Per-download limits still take precedence.
|
||||
Applies to new and active aria2 transfers and yt-dlp media downloads. Per-download limits still take precedence.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
|
||||
@@ -69,8 +69,12 @@ export async function initDownloadListener() {
|
||||
const progress = useDownloadProgressStore.getState().progressMap[payload.id];
|
||||
const updates: Partial<DownloadItem> = {
|
||||
status,
|
||||
...(progress ? { fraction: progress.fraction } : {})
|
||||
...(progress ? { fraction: progress.fraction } : {}),
|
||||
...(payload.error ? { lastError: payload.error } : {})
|
||||
};
|
||||
if (!payload.error && status !== 'failed' && status !== 'retrying') {
|
||||
updates.lastError = undefined;
|
||||
}
|
||||
if (payload.fileName && payload.fileName !== current.fileName) {
|
||||
updates.fileName = payload.fileName;
|
||||
updates.category = categoryForFileName(payload.fileName);
|
||||
|
||||
@@ -268,6 +268,41 @@ describe('useDownloadStore', () => {
|
||||
|
||||
expect(added).toBe(false);
|
||||
expect(useDownloadStore.getState().downloads[0].status).toBe('failed');
|
||||
expect(useDownloadStore.getState().downloads[0].lastError).toBe('backend unavailable');
|
||||
});
|
||||
|
||||
it('preserves backend rejection reasons while auto-resuming saved queued items', async () => {
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
||||
if (cmd === 'db_get_all_queues') return [];
|
||||
if (cmd === 'db_get_all_downloads') {
|
||||
return [JSON.stringify({
|
||||
id: 'startup-failed',
|
||||
url: 'https://example.com/file.bin',
|
||||
fileName: 'file.bin',
|
||||
status: 'queued',
|
||||
category: 'Other',
|
||||
dateAdded: '',
|
||||
queueId: '00000000-0000-0000-0000-000000000001',
|
||||
hasBeenDispatched: true
|
||||
})];
|
||||
}
|
||||
if (cmd === 'enqueue_many') {
|
||||
return [{
|
||||
id: 'startup-failed',
|
||||
success: false,
|
||||
error: 'aria2 addUri failed: connection refused'
|
||||
}];
|
||||
}
|
||||
if (cmd === 'get_pending_order') return [];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await useDownloadStore.getState().initDB();
|
||||
|
||||
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
|
||||
status: 'failed',
|
||||
lastError: 'aria2 addUri failed: connection refused'
|
||||
});
|
||||
});
|
||||
|
||||
it('redownloads fallback media without requiring a format selector', async () => {
|
||||
|
||||
@@ -17,6 +17,9 @@ export type { DownloadCategory } from '../utils/downloads';
|
||||
|
||||
const backendDispatchPromises = new Map<string, Promise<boolean>>();
|
||||
|
||||
const errorMessage = (error: unknown): string =>
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
export async function dispatchItem(id: string): Promise<boolean> {
|
||||
if (backendDispatchPromises.has(id)) return backendDispatchPromises.get(id)!;
|
||||
|
||||
@@ -73,10 +76,14 @@ export async function dispatchItem(id: string): Promise<boolean> {
|
||||
const order = await invoke('get_pending_order', { queueId: item.queueId || MAIN_QUEUE_ID });
|
||||
useDownloadStore.getState().setPendingOrder(order);
|
||||
useDownloadStore.getState().registerBackendIds([id]);
|
||||
useDownloadStore.getState().updateDownload(id, { lastError: undefined });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`Failed to dispatch ${id}:`, e);
|
||||
useDownloadStore.getState().updateDownload(id, { status: 'failed' });
|
||||
useDownloadStore.getState().updateDownload(id, {
|
||||
status: 'failed',
|
||||
lastError: errorMessage(e)
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
backendDispatchPromises.delete(id);
|
||||
@@ -856,7 +863,11 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
}
|
||||
const results = await invoke('enqueue_many', { items: itemsToEnqueue });
|
||||
const registeredIds = results.filter(result => result.success).map(result => result.id);
|
||||
const failedIds = new Set(results.filter(result => !result.success).map(result => result.id));
|
||||
const failedErrors = new Map(
|
||||
results
|
||||
.filter(result => !result.success)
|
||||
.map(result => [result.id, result.error || 'Backend rejected the queued download.'])
|
||||
);
|
||||
const order = await invoke('get_pending_order', { queueId: null });
|
||||
set(state => ({
|
||||
pendingOrder: order,
|
||||
@@ -865,10 +876,14 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
...registeredIds
|
||||
]),
|
||||
downloads: state.downloads.map(download =>
|
||||
failedIds.has(download.id)
|
||||
? { ...download, status: 'failed' as const }
|
||||
failedErrors.has(download.id)
|
||||
? {
|
||||
...download,
|
||||
status: 'failed' as const,
|
||||
lastError: failedErrors.get(download.id)
|
||||
}
|
||||
: registeredIds.includes(download.id)
|
||||
? { ...download, hasBeenDispatched: true }
|
||||
? { ...download, hasBeenDispatched: true, lastError: undefined }
|
||||
: download
|
||||
)
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user