feat(m4): persist the download queue across restarts

The queue/scheduler lived only in renderer memory, so saved/scheduled and other
pending downloads were silently lost on quit (audit M4). Persist them:

- main: queue.ts store (proven cached-atomic createJsonStore, queue.json) +
  queue:list / queue:save IPC; QUEUE_MAX cap.
- shared: PersistedQueueItem = the durable subset of DownloadItem (runtime-only
  progress/speed/eta/sizeLabel/finishing dropped).
- renderer: mirror the persistable items on every queue change (a signature over
  the subset means progress ticks don't re-save; main's jsonStore coalesces the
  writes) and rehydrate on launch via an App useEffect. Pure mappers extracted
  to store/queuePersist.ts and unit-tested.

Restore semantics: downloading -> queued (yt-dlp continues the .part), saved +
scheduledFor survives so the promoter fires when due, paused/error return
actionable; completed/canceled are never persisted. A queueHydrated gate stops a
launch-time store change from wiping queue.json before it's read.

Reconciles ROADMAP.md. typecheck + 268 tests + eslint + prettier green.
(OS-level quit/relaunch cycle is worth a manual smoke test.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:34:55 -04:00
parent 63687aaec3
commit 0a681f864f
13 changed files with 320 additions and 10 deletions
+38
View File
@@ -46,6 +46,9 @@ export const IpcChannels = {
ytdlpAutoUpdateStatus: 'ytdlp:auto-update-status',
errorLogList: 'errorlog:list',
errorLogClear: 'errorlog:clear',
/** Persisted download queue (M4): saved/scheduled/pending items survive a quit. */
queueList: 'queue:list',
queueSave: 'queue:save',
backupExport: 'backup:export',
backupImport: 'backup:import',
systemThemeGet: 'system-theme:get',
@@ -501,6 +504,41 @@ export type DownloadEvent =
| { type: 'done'; id: string; filePath?: string }
| { type: 'error'; id: string; error: string }
/** The subset of a persisted queue item's status worth surviving a quit. */
export type PersistedQueueStatus = 'queued' | 'downloading' | 'paused' | 'saved' | 'error'
/**
* A queue item persisted across restarts (M4) so saved/scheduled and other pending
* downloads return after a quit. This is the *durable* subset of the renderer's
* `DownloadItem`: runtime-only fields (progress/speed/eta/sizeLabel/finishing) are
* dropped since a restored item re-derives them. Transient results ('completed' /
* 'canceled') are never persisted; on load a 'downloading' item is normalized back
* to 'queued' because its process died with the previous session.
*/
export interface PersistedQueueItem {
id: string
url: string
title: string
channel?: string
durationLabel?: string
thumbnail?: string
kind: MediaKind
quality: string
status: PersistedQueueStatus
filePath?: string
error?: string
formatId?: string
formatHasAudio?: boolean
options?: DownloadOptions
extraArgs?: string
trim?: string
scheduledFor?: number
incognito?: boolean
probedMeta?: DownloadMeta
collection?: CollectionContext
mediaItemId?: string
}
/** Persisted user settings (electron-store). */
export interface Settings {
/** where video downloads are saved; empty string = the default Documents\Video folder */