Filter row above the VOD grid lets the user search the loaded archive
by title. Concrete user pain: streamers commonly have hundreds of VODs
and the current UI only supported scrolling.
- vodFilterInput / vodFilterClearBtn / vodFilterCount in index.html
- localized placeholder + clear-button title (DE + EN)
- vodFilterQuery state persisted to localStorage as
twitch-vod-manager:vod-filter so the search bar survives reloads
- renderVODs split: it now caches lastLoadedVods + lastLoadedStreamer
and delegates to renderVodGridFromCurrentState which applies
filterVodsByQuery on every input event (no re-fetch)
- empty-state DOM is now built with createElement + textContent (via
setVodGridEmptyState) instead of an innerHTML template, even for
locale-only strings — defence in depth
- keyboard: Ctrl/Cmd+F focuses the filter when the VODs tab is active
(Electron has no native find bar, so the default is suppressed). Esc
clears the filter when the input has focus and content. Esc still
closes modals first if any are open.
docs/IMPROVEMENT_LOG.md: Cycle 3 dated section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
12-task step-by-step plan with exact code, file paths, and line numbers.
Reviewed and fixed 5 issues (locale schema, language-awareness, TypeScript
union types, execSync import, cleanup scope).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive design for combining multiple queue VODs into a merge group,
downloading them, merging via FFmpeg, and splitting into time-based parts.
Reviewed through two spec-review iterations fixing 14 issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace unreliable emoji flags in the native select with a custom language picker that renders real CSS flag icons, improve retry button naming/tooltips, and keep quick/full e2e coverage green after the UI change.
Introduce a longer smoke-test-full workflow for validating queue, media tools, localization, and reliability flows in one pass, and document both quick and full test commands for contributors.
Expose a single runnable end-to-end test command for fast regression sweeps and update the smoke test to validate preflight and localization-aware clip errors reliably across language modes.
After exhaustive end-to-end validation, fix clip error localization in English mode and confirm full cutter/merge reliability with regenerated local media tests, then ship a patch release for immediate auto-update rollout.
Ship a major update that adds in-app preflight diagnostics with health badge, live debug log tooling, retry management, pause/resume downloads, queue reordering support, locale polish (flags + clearer retry wording), and extensive backend hardening for day-to-day stability.
Add an in-app preflight diagnostics center with optional auto-fix, introduce backend retry handling for failed downloads, provide live debug log viewing in settings, and expand queue controls with retry-failed actions while keeping language switching instant and locale data organized.
Move DE/EN translation content into separate renderer locale files, keep instant language switching, add locale-aware date/number formatting helpers, and set English as the default language for fresh installs.
Make English the initial language for fresh installations while keeping live DE/EN switching, and split locale dictionaries into dedicated per-language files for cleaner long-term translation maintenance.
Introduce a centralized renderer text dictionary with German and English locales, add a language selector in settings, and apply translations instantly across UI sections and dynamic status labels without requiring app restart.
Polish mixed-language labels across tabs by replacing leftover English terms (views, queue, cutter naming, clip heading) with consistent German wording so the interface reads uniformly end-to-end.
Align visible queue and clip messages to German terminology, remove remaining mixed English labels, and reuse resilient streamlink command resolution for clip downloads to keep behavior consistent across environments.
Remove duplicate part-progress wording in the queue by keeping part index in the meta line and using the progress line for transferred data/percent, while labeling throughput as Geschwindigkeit for clearer download monitoring.
Improve first-phase download feedback by emitting explicit preflight/start statuses from the backend so the queue no longer appears to jump between ambiguous placeholder texts during active transfers.