(({
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)}%`
diff --git a/src/components/PropertiesModal.tsx b/src/components/PropertiesModal.tsx
index 67a4506..7581059 100644
--- a/src/components/PropertiesModal.tsx
+++ b/src/components/PropertiesModal.tsx
@@ -206,8 +206,14 @@ export const PropertiesModal = () => {
Category{item.category}
Last try-
- Date added{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
- Destination{saveLocation || baseDownloadFolder}
+ Date added{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
+ Destination{saveLocation || baseDownloadFolder}
+ {item.lastError && (item.status === 'failed' || item.status === 'retrying') && (
+
+ Last error
+ {item.lastError}
+
+ )}
diff --git a/src/components/SpeedLimiterView.tsx b/src/components/SpeedLimiterView.tsx
index 620fd44..0b9e68d 100644
--- a/src/components/SpeedLimiterView.tsx
+++ b/src/components/SpeedLimiterView.tsx
@@ -144,7 +144,7 @@ export default function SpeedLimiterView() {
Global Speed Limit
- 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.
diff --git a/src/store/downloadStore.ts b/src/store/downloadStore.ts
index d4c9348..1f324fb 100644
--- a/src/store/downloadStore.ts
+++ b/src/store/downloadStore.ts
@@ -69,8 +69,12 @@ export async function initDownloadListener() {
const progress = useDownloadProgressStore.getState().progressMap[payload.id];
const updates: Partial = {
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);
diff --git a/src/store/useDownloadStore.test.ts b/src/store/useDownloadStore.test.ts
index 14cb4ab..3466ab2 100644
--- a/src/store/useDownloadStore.test.ts
+++ b/src/store/useDownloadStore.test.ts
@@ -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 () => {
diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts
index 429eace..b7e53fb 100644
--- a/src/store/useDownloadStore.ts
+++ b/src/store/useDownloadStore.ts
@@ -17,6 +17,9 @@ export type { DownloadCategory } from '../utils/downloads';
const backendDispatchPromises = new Map>();
+const errorMessage = (error: unknown): string =>
+ error instanceof Error ? error.message : String(error);
+
export async function dispatchItem(id: string): Promise {
if (backendDispatchPromises.has(id)) return backendDispatchPromises.get(id)!;
@@ -73,10 +76,14 @@ export async function dispatchItem(id: string): Promise {
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((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((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
)
}));