Two server-side fixes for separate clip/queue/editor crosstalk paths.
1. download-clip IPC was unsafe in three ways:
- reported success: true on exit code 0 even with empty files
(Twitch sometimes returns a manifest with no segments)
- passed clipInfo.broadcaster_name straight to path.join, so unicode
/ spaces / punctuation in display names produced odd directory
layouts on Windows
- the spawned streamlink process was tracked nowhere, so window
close orphaned it
Now: sanitize broadcaster_name + title, ensureUniqueFilename so
re-downloads do not overwrite, post-download size + integrity check
(16 KiB floor + ffprobe via validateDownloadedFileIntegrity), proc
tracked in activeClipProcesses and killed on window-all-closed.
2. currentProcess (a single ChildProcess global) was shared between
cutter/merger/splitter and downloadVODPart. The real bug: while a
queue download was running and the user kicked off a video cut,
pressing the queue's "Stop" button iterated activeDownloads (fine)
AND called currentProcess.kill() — which by then pointed at the
cutter ffmpeg, killing an unrelated cut.
Renamed to currentEditorProcess, confined to the editor pipeline.
downloadVODPart no longer touches it. The fallback kill calls in
remove-from-queue / pause-download / cancel-download are gone — the
activeDownloads loop above each was already authoritative.
window-all-closed now also kills activeClipProcesses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/release_gitea.mjs: --skip-build flag and auto-skip when
artifacts for the version already exist on disk
- playwright in devDependencies; test:e2e* scripts call node directly
- loadConfig isPlainObject shape check + loadQueue sanitizes every
entry incl. customClip/mergeGroup; stale status="downloading" is
demoted to "pending" so the queue can actually be resumed
See docs/IMPROVEMENT_LOG.md (Cycle 2, 2026-05-03) for the dated
rationale and regression run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- loadConfig now checks isPlainObject(parsed) before spreading over
defaults. Non-object JSON (array, primitive, null) is logged and the
app falls back to defaults instead of silently polluting the config
with array indices or dropping values.
- loadQueue runs every entry through sanitizeQueueItem which validates
the status enum, clamps progress to [0, 100], validates customClip
and mergeGroup shapes (with sanitizeCustomClip / sanitizeMergeGroup
helpers), and demotes stale status="downloading" entries to "pending"
with progress=0 on cold start. The previous filter only checked
typeof id/url/status === "string" and let through whatever shape
customClip / mergeGroup happened to have.
- The stale-downloading normalisation fixes a real user trap: after a
hard kill mid-download, the queue persisted status="downloading", but
no download was running on next launch and start-download only resumed
paused items, leaving "downloading" entries stuck.
- Bonus: CustomClip and MergeGroupItem imports now have call sites
(previously unused-import warnings).
docs/IMPROVEMENT_LOG.md gains a Cycle 2 dated section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two release-pipeline fixes that previously forced manual workarounds.
- scripts/release_gitea.mjs no longer unconditionally runs npm run dist:win.
New --skip-build flag, plus auto-skip when all 3 required artifacts
(Setup-<v>.exe, Setup-<v>.exe.blockmap, latest.yml) already exist for
the requested version. The previous behaviour re-ran the entire test
suite + electron-builder on every release attempt — unusable when the
test path was broken.
- playwright ^1.59.1 added to devDependencies. test:e2e / test:e2e:guide
/ test:e2e:full now invoke node scripts/smoke-test*.js directly instead
of "npm exec --yes --package=playwright -- node ...", which failed with
MODULE_NOT_FOUND when npm exec could not resolve playwright on the fly.
No browser binaries needed — the smoke tests drive Electron via
_electron, not a browser.
All test paths verified after the change: test:e2e, test:e2e:guide,
test:e2e:full, test:merge-split, test:e2e:update-logic — all pass with
the simplified scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stability + UX cycle.
- saveConfig and writeQueueToDisk now use openSync+writeSync+fsyncSync+
closeSync+renameSync via writeFileAtomicSync. Survives power loss
between write and rename (used to leave the renamed file empty and
silently reset config / queue on next launch).
- Per-item claimedFilenames map fixes the parallel-download race where
one item finishing wiped sibling claims and let a third item collide
on the same output path.
- Renderer queue lookup by [data-id] (no more index drift), active tab
persisted in localStorage, Escape closes the topmost open modal,
Ctrl/Cmd+1..5 jumps tabs.
See docs/IMPROVEMENT_LOG.md for the dated rationale and regression run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renderer-side polish bundle.
- updateQueueItemProgress now looks up items by [data-id] selector instead
of array index. Resilient against queue/DOM divergence between renders.
Determinate vs indeterminate progress logic tightened.
- Active tab persisted to localStorage on every showTab; restored on init
via loadPersistedActiveTab (whitelisted to known tab IDs so a future
rename cannot strand the user on a missing tab). Page title now only
shows the streamer name on the VODs tab — it no longer leaks into
Settings / Cutter / Merge.
- Escape closes the topmost open modal regardless of focus (clip dialog,
template guide, update modal — in that priority order).
- Ctrl+1..5 (Cmd+1..5 on macOS) jumps directly to a tab. The existing Del
(delete selected) and S (start/pause) shortcuts still work and remain
blocked while typing in inputs.
Adds docs/IMPROVEMENT_LOG.md (new, single dated section for this cycle).
Build: tsc clean. Full smoke suite green (failures: [], runtimeIssues: []).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two server-side correctness fixes for parallel downloads and crash recovery.
1. Atomic file writes survive power loss / crash mid-write.
saveConfig and writeQueueToDisk used writeFileSync + renameSync. Node's
writeFileSync does NOT fsync — a power loss between write and rename can
leave the renamed file empty or truncated, and the next launch silently
falls back to defaults / empty queue.
New writeFileAtomicSync helper: openSync + writeSync + fsyncSync +
closeSync + renameSync (with the existing Windows copy fallback). fsync
failure is non-fatal (some FS reject it) but file ordering is preserved.
2. Per-item claimed filenames fix the parallel-download race.
With max 2 parallel downloads, processOneQueueItem.finally was calling
claimedFilenames.clear() — wiping every parallel item's claims when any
one finished. In the window between an active item claiming a filename
and streamlink actually writing the first bytes, a third item could
compute the same filename and both downloads would race the same path.
New Map<itemId, Set<filename>> tracks claims per active download.
ensureUniqueFilename(path, itemId) registers per-item;
releaseClaimedFilenamesForItem(itemId) removes only that item's claims.
splitMergedFile gained an itemId parameter for the same reason. The
dead releaseClaimedFilename(path) function was removed.
Build: tsc clean. Tests: smoke + smoke-template-guide + smoke-full + merge-split
+ update-version-logic all pass. No new ESLint warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set app.setAppUserModelId('com.twitch.vodmanager') on startup so Windows
notifications display the correct app name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The old formula (avgSpeed * expectedDurationSeconds) simplified to just
(videoDuration - elapsedTime), showing 59min ETA for a 60min part after
1min of downloading. Now uses streamlink's actual progress percentage:
ETA = (elapsed / percent) * (100 - percent), which reflects real download
speed rather than video length.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use dataTransfer.effectAllowed='none' instead of preventDefault() for dragstart
(preventDefault does not cancel dragstart events per HTML spec)
- Clear claimedFilenames Set in processOneQueueItem finally block to prevent
stale claims from blocking re-downloads of same VODs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Queue selector uses min-width instead of fixed width for double-digit numbers
- Drag-start handler validates item is still pending before allowing drag
- ensureUniqueFilename uses in-memory claim set to prevent TOCTOU race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move streamlink/ffmpeg path discovery, bundled tool management,
auto-install logic, and related caches (~430 lines) into a
dedicated tools module. main.ts uses dependency injection for
debug logging and directory paths to keep the module decoupled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add parallel_downloads config option (1 or 2) with Settings UI dropdown.
Refactor processQueue to run concurrent download slots using Promise.race,
extracting per-item logic into processOneQueueItem. Add per-item process
tracking via activeDownloads Map and cancelledItemIds Set so cancel/pause
correctly terminates all active downloads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Queue section now uses flex layout with flex-shrink:0 on action buttons,
so they stay visible regardless of queue list length.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace checkboxes with numbered selectors (1, 2, 3...) that show the
merge order. Click order determines VOD sequence in the merged result.
Chronological auto-sort removed — user controls the order.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>