Commit Graph

427 Commits

Author SHA1 Message Date
xRangerDE
832b606701 ui: VOD sort dropdown with persisted key + locale labels
Adds a sort selector next to the existing filter input. Five modes:
newest first (default), oldest first, most viewed, longest first,
shortest first. Concrete user pain — long archives previously had no
way to find the longest stream, the most-watched, or to scroll back
to the start chronologically.

- vodSortKey state persisted to localStorage as
  twitch-vod-manager:vod-sort and validated against an enum on load,
  so an unknown stored value falls back to date_desc
- renderVodGridFromCurrentState now applies sortVods before
  filterVodsByQuery so the filter sees the sort and the match counter
  is consistent
- sortVods uses created_at timestamps for date sorts, view_count for
  views, and a tiny vodDurationToSeconds parser (XhYmZs) for duration
- DE + EN labels for both the "Sort:" prefix and the five option
  texts; refreshVodSortSelectLabels re-runs on language switch
- syncVodSortSelect on init preselects the persisted value before
  any VOD load so the dropdown reflects state immediately

Browser-default keyboard nav (arrows, type-ahead) covers keyboard
access for the select.

docs/IMPROVEMENT_LOG.md: Cycle 4 dated section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:54:53 +02:00
xRangerDE
020f3dacf1 harden: GQL retry on transient errors + consolidate shutdown cleanup
Two server-side changes touching different paths.

1. fetchPublicTwitchGql now retries on transient HTTP (408/429/5xx) and
   network-layer failures (no response). Up to 3 attempts with
   exponential backoff + jitter (400ms * 2^(n-1)). The previous
   catch (e) { return null; } swallowed network blips on the public
   fallback path, which is what every user without a client_id hits
   on each VOD list load — a single TCP RST produced an empty list
   and the user had to click refresh. GraphQL errors[] are still
   returned without retry (application-level query rejections).
   Recovery is logged via appendDebugLog so we can later see whether
   the retries actually pay off in production.

2. shutdownCleanup() consolidates window-all-closed and before-quit.
   The two handlers ran nearly identical cleanup blocks but had
   drifted: only window-all-closed killed children and was
   platform-aware. The helper kills activeDownloads + activeClipProcesses
   + currentEditorProcess with try/catch, persists config + queue,
   then stops timers (debug-log flush moved AFTER persistence so any
   save error reaches the log before the timer is gone). An idempotent
   shutdownCleanupDone flag makes a follow-on event a no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:54:40 +02:00
xRangerDE
81a1f914b4 release: 4.5.10 clip hardening, VOD filter, editor proc decoupling
- download-clip: sanitize broadcaster name + title, ensure unique
  filename, post-download size + integrity check, track in
  activeClipProcesses so window-close cleans up
- VOD list: persistent filter input with Ctrl+F focus, Esc clear,
  match counter (DE + EN strings)
- currentProcess split into currentEditorProcess (cutter/merger/
  splitter only) so cancel-download no longer accidentally kills a
  separate video cut

See docs/IMPROVEMENT_LOG.md (Cycle 3, 2026-05-03) for the dated
rationale and regression run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:43:30 +02:00
xRangerDE
23d0dd5829 ui: VOD list filter with persistence + Ctrl+F focus + Esc clear
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>
2026-05-03 15:43:16 +02:00
xRangerDE
31e6671e65 harden: download-clip integrity + cancel tracking + decouple editor procs
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>
2026-05-03 15:43:01 +02:00
xRangerDE
9d57c03e74 release: 4.5.9 release pipeline + defensive parsing
- 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>
2026-05-03 15:29:37 +02:00
xRangerDE
379048f191 harden: defensive parsing for config + queue, normalize stale downloading
- 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>
2026-05-03 15:29:28 +02:00
xRangerDE
b4faf67db7 infra: release_gitea.mjs --skip-build + playwright in devDeps
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>
2026-05-03 15:29:17 +02:00
xRangerDE
707c98e19d release: 4.5.8 fsync atomic writes, parallel filename race, UI persistence
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>
2026-05-03 15:11:14 +02:00
xRangerDE
feebfc86a1 ui: data-id queue lookup + persisted active tab + Esc/Ctrl+N shortcuts
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>
2026-05-03 15:10:28 +02:00
xRangerDE
8d0cb4cefd harden: atomic fsync writes + per-item filename claims
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>
2026-05-03 15:10:15 +02:00
xRangerDE
54197af863 release: 4.5.7 fix notification app name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 04:20:21 +02:00
xRangerDE
37d75fac24 fix: Windows notification shows 'Twitch VOD Manager' instead of 'electron.app.Electron'
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>
2026-03-29 04:18:34 +02:00
xRangerDE
f9a0fdcf3d release: 4.5.6 guard formatDuration against NaN/Infinity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 15:27:52 +01:00
xRangerDE
da1d14d458 fix: guard formatDuration against NaN/Infinity input
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 15:23:21 +01:00
xRangerDE
18940d0640 chore: add ESLint with security plugin, fix code quality warnings
- Install eslint, typescript-eslint, eslint-plugin-security
- Add eslint.config.mjs with project-tuned rules
- Fix redundant catch assignment in cutVideo
- Fix let→const for promise dedup patterns
- No security bugs found — all regex warnings are false positives (anchored patterns)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:55:35 +01:00
xRangerDE
d9bdf744fd release: 4.5.5 fix ETA showing video duration instead of actual remaining time
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:54:44 +01:00
xRangerDE
d8f0836165 fix: ETA calculation was using video duration instead of download progress
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>
2026-03-21 15:47:19 +01:00
xRangerDE
a07ec1f958 release: 4.5.4 hardening — clip validation, cutter check, pagination guard, atomic writes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:39:55 +01:00
xRangerDE
5f2e85e455 fix: clip time validation, cutter 0-byte check, pagination guard, atomic config write
H1: Add NaN/negative/zero-duration validation to clip dialog before IPC call
H2: Reject cut video output <= 256 bytes as effectively empty
H3: Add paginated VOD fetching with MAX_VOD_PAGES=50 safety guard
H4: Atomic write (tmp+rename) for config and queue persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:33:31 +01:00
xRangerDE
39fa5065d2 release: 4.5.3 bugfixes — selector overflow, drag safety, filename claims, details persist, progress stability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:20:11 +01:00
xRangerDE
2b379e5e6a fix: correct dragstart cancel method and release claimed filenames after download
- 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>
2026-03-21 15:19:30 +01:00
xRangerDE
cf9d7b8334 fix: selector overflow for 10+ items, drag-drop status guard, filename claim set for parallel safety
- 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>
2026-03-21 15:15:25 +01:00
xRangerDE
6c47c63fa8 fix: clamp ETA bounds, store stats interval, add activeDownloads finally-cleanup, prevent progress backward jump
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:04:59 +01:00
xRangerDE
4607ba9cc6 fix: persist expanded details across re-renders, guard drag-drop init against duplicates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:03:20 +01:00
xRangerDE
1e81b889f9 release: 4.5.2 compact queue buttons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:30:50 +01:00
xRangerDE
00d35f1b1c fix(ui): reduce queue action button height for compact layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:10:02 +01:00
xRangerDE
8132e062fa release: 4.5.1 fix numbered selector visual update
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:07:51 +01:00
xRangerDE
0133569104 fix: include selection state in queue render fingerprint so numbered selectors update visually
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:06:42 +01:00
xRangerDE
6b97039471 release: 4.5.0 ETA, shortcuts, stats, drag&drop, expandable items, light theme, parallel downloads, code split
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:02:32 +01:00
xRangerDE
66afaba0ea refactor: extract tool discovery functions to src/tools.ts
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>
2026-03-20 10:00:51 +01:00
xRangerDE
54d04d4f73 feat: support parallel downloads (up to 2 simultaneous)
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>
2026-03-20 09:54:20 +01:00
xRangerDE
63aafae85d feat: add light theme with toggle in settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:46:26 +01:00
xRangerDE
424b312551 refactor: extract shared interfaces to src/types.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:44:37 +01:00
xRangerDE
fbcf3935d0 feat: add drag & drop queue reordering and expandable queue item details
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:42:35 +01:00
xRangerDE
2481230983 feat: add keyboard shortcuts (Del/S) and download statistics bar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:40:39 +01:00
xRangerDE
c96fd13aff feat: add ETA calculation for download progress display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:39:27 +01:00
xRangerDE
6a32387add release: 4.4.0 performance optimizations, collision detection, notifications
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:28:42 +01:00
xRangerDE
3537d28eb2 test: add filename collision detection unit tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:28:06 +01:00
xRangerDE
a4ca410641 feat: filename collision detection, queue JSON validation, download-complete notification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:27:19 +01:00
xRangerDE
76ecbc652d perf: parallel init with Promise.all, targeted DOM updates for download progress
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:26:19 +01:00
xRangerDE
b7499c87a3 release: 4.3.4 fix queue buttons overflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 09:06:52 +01:00
xRangerDE
00cf7781d3 fix(ui): prevent queue buttons from being pushed off-screen by long queue
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>
2026-03-20 09:06:10 +01:00
xRangerDE
46dc29a226 release: 4.3.3 numbered merge selection, user-defined order
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 08:57:26 +01:00
xRangerDE
0e6b219455 feat(merge-split): numbered selection instead of checkboxes, user-defined merge order
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>
2026-03-20 08:55:35 +01:00
xRangerDE
674041a603 release: 4.3.2 bugfix — config flush on quit, processQueue concurrency guard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 08:47:15 +01:00
xRangerDE
86c80acf28 fix: flush config on quit, verify temp cleanup, add missing spawn error handlers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:42:35 +01:00
xRangerDE
33730fd372 fix: guard processQueue against concurrent invocations, fix stale process kill reference
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:41:11 +01:00
xRangerDE
3af159f8e7 release: 4.3.0 add VOD merge+split feature
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:23:40 +01:00
xRangerDE
6c082a87ab fix(merge-split): fix premature 100% progress, add ffmpeg preflight, clean partial splits, use locale metaLabel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:12:50 +01:00