0a681f864f
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>
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
import { type PersistedQueueItem, type PersistedQueueStatus } from '@shared/ipc'
|
|
import { type DownloadItem, type DownloadStatus } from './downloadTypes'
|
|
|
|
/**
|
|
* Pure mapping between the live `DownloadItem` and the durable `PersistedQueueItem`
|
|
* main stores across restarts (M4). Kept free of any store/zustand import so it can
|
|
* be unit-tested in isolation (mirrors queueStats / formatters). Only stable fields
|
|
* persist — runtime-only progress/speed/eta/sizeLabel/finishing are dropped and
|
|
* re-derived on restore.
|
|
*/
|
|
|
|
// Statuses worth surviving a quit. 'completed'/'canceled' are transient results
|
|
// (completions already live in history) and are never persisted.
|
|
export const PERSISTED_STATUSES: readonly DownloadStatus[] = [
|
|
'queued',
|
|
'downloading',
|
|
'paused',
|
|
'saved',
|
|
'error'
|
|
]
|
|
|
|
export function toPersisted(i: DownloadItem): PersistedQueueItem {
|
|
return {
|
|
id: i.id,
|
|
url: i.url,
|
|
title: i.title,
|
|
channel: i.channel,
|
|
durationLabel: i.durationLabel,
|
|
thumbnail: i.thumbnail,
|
|
kind: i.kind,
|
|
quality: i.quality,
|
|
status: i.status as PersistedQueueStatus,
|
|
filePath: i.filePath,
|
|
error: i.error,
|
|
formatId: i.formatId,
|
|
formatHasAudio: i.formatHasAudio,
|
|
options: i.options,
|
|
extraArgs: i.extraArgs,
|
|
trim: i.trim,
|
|
scheduledFor: i.scheduledFor,
|
|
incognito: i.incognito,
|
|
probedMeta: i.probedMeta,
|
|
collection: i.collection,
|
|
mediaItemId: i.mediaItemId
|
|
}
|
|
}
|
|
|
|
export function fromPersisted(p: PersistedQueueItem): DownloadItem {
|
|
return {
|
|
...p,
|
|
// A download in flight when we quit lost its process; re-queue it (yt-dlp
|
|
// continues the .part on relaunch). Progress restarts from 0.
|
|
status: p.status === 'downloading' ? 'queued' : p.status,
|
|
progress: 0
|
|
}
|
|
}
|
|
|
|
/** The persistable subset of the live queue, in durable form. */
|
|
export function persistableItems(items: DownloadItem[]): PersistedQueueItem[] {
|
|
return items
|
|
.filter((i) => (PERSISTED_STATUSES as readonly string[]).includes(i.status))
|
|
.map(toPersisted)
|
|
}
|