feat: queue UX (Phase M) + media editing (split-chapters, trim)
Phase L (media editing): - Split by chapters (--split-chapters) as a DownloadOptions toggle - Trim/cut by time range: parseTrimSections -> --download-sections "*A-B" + --force-keyframes-at-cuts (deduped vs SponsorBlock), per-download Trim panel Phase M (queue & daily-use UX), all 7: - Pause/resume: main-side killTree + paused flag (silent close), download:pause IPC, store pause/resume + paused status, QueueItem controls - Reorder via "Download next" (prioritize) - Aggregate progress strip (pure summarizeQueue) + combined speed/ETA - Drag-and-drop links / .url files onto the download card - Retry all failed - Duplicate detection (sameVideo) with "Download anyway" guard - Per-download scheduling (datetime-local) + save-for-later (saved status, 15s promotion ticker); session-only (queue not persisted) New pure modules unit-tested (queueStats); buildArgs trim/split tested. typecheck + test green (178 passed). Roadmap updated: Phase M COMPLETE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-10
@@ -38,6 +38,7 @@ import {
|
||||
interface ActiveDownload {
|
||||
child: ChildProcess
|
||||
canceled: boolean
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
const active = new Map<string, ActiveDownload>()
|
||||
@@ -295,7 +296,7 @@ export function startDownload(
|
||||
return { ok: false, error: (e as Error).message }
|
||||
}
|
||||
|
||||
const rec: ActiveDownload = { child, canceled: false }
|
||||
const rec: ActiveDownload = { child, canceled: false, paused: false }
|
||||
active.set(opts.id, rec)
|
||||
|
||||
// Title/channel/duration are kept locally so the completion notification and
|
||||
@@ -344,7 +345,8 @@ export function startDownload(
|
||||
if (settled) return
|
||||
settled = true
|
||||
active.delete(opts.id)
|
||||
if (!rec.canceled) {
|
||||
// A paused download was killed on purpose — stay silent, like a cancel.
|
||||
if (!rec.canceled && !rec.paused) {
|
||||
send(wc, { type: 'error', id: opts.id, error: err.message })
|
||||
logFailure(opts, resolvedTitle, err.message)
|
||||
notify(wc, resolvedTitle ?? 'Download failed', err.message)
|
||||
@@ -355,7 +357,9 @@ export function startDownload(
|
||||
if (settled) return
|
||||
settled = true
|
||||
active.delete(opts.id)
|
||||
if (rec.canceled) return // renderer already showed 'canceled' optimistically
|
||||
// Canceled: renderer already showed 'canceled'. Paused: renderer showed
|
||||
// 'paused' and keeps the .part for a later resume. Either way, no event.
|
||||
if (rec.canceled || rec.paused) return
|
||||
if (code === 0) {
|
||||
send(wc, { type: 'done', id: opts.id, filePath })
|
||||
notify(wc, resolvedTitle ?? 'Download complete', 'Finished downloading.')
|
||||
@@ -370,15 +374,12 @@ export function startDownload(
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
export function cancelDownload(id: string): void {
|
||||
const rec = active.get(id)
|
||||
if (!rec) return
|
||||
rec.canceled = true
|
||||
// Kill the whole process tree (/T) so the spawned ffmpeg child dies too. Resolve
|
||||
// taskkill from System32 by absolute path, never the bare name, so a planted
|
||||
// taskkill.exe on PATH / in the CWD can't run in its place. (audit F3)
|
||||
function killTree(rec: ActiveDownload): void {
|
||||
const pid = rec.child.pid
|
||||
if (pid != null) {
|
||||
// Kill the whole tree (/T) so the spawned ffmpeg child dies too. Resolve
|
||||
// taskkill from System32 by absolute path, never the bare name, so a planted
|
||||
// taskkill.exe on PATH / in the CWD can't run in its place. (audit F3)
|
||||
execFile(
|
||||
getSystem32Path('taskkill.exe'),
|
||||
['/pid', String(pid), '/T', '/F'],
|
||||
@@ -389,3 +390,24 @@ export function cancelDownload(id: string): void {
|
||||
rec.child.kill()
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelDownload(id: string): void {
|
||||
const rec = active.get(id)
|
||||
if (!rec) return
|
||||
rec.canceled = true
|
||||
killTree(rec)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a running download: kill the process tree but flag it `paused` (not
|
||||
* `canceled`) so the close handler stays silent — no error event, no error log,
|
||||
* no notification. yt-dlp leaves the partial `.part` file in place, so resuming
|
||||
* is just a fresh startDownload with the same options: yt-dlp's default
|
||||
* `--continue` picks the partial file back up.
|
||||
*/
|
||||
export function pauseDownload(id: string): void {
|
||||
const rec = active.get(id)
|
||||
if (!rec) return
|
||||
rec.paused = true
|
||||
killTree(rec)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user