The live-status batch poller (60s cadence, every streamer in the
watch list) was sending two things on every tick:
- `changes` — the diff vs. the previous tick, used by the renderer
- `snapshot` — the full Map<login, boolean> serialized as a record
Renderer destructures only `changes` (renderer-streamers.ts line 20).
The snapshot field was wire-noise. For a typical 30-50 streamer
watch list, that snapshot is ~1.5KB of JSON every minute, never
read on the other side. Dropped from the broadcast payload.
Initial-state sync still works: the renderer's
initLiveStatusSubscription calls window.api.getLiveStatusSnapshot()
once at boot to pre-fill its map. The broadcast is only for diffs.
Also added a short-circuit on the main side: if changes.length === 0
(every streamer's live status matched the cached value this tick),
don't broadcast at all. The renderer would just iterate an empty
array and trigger a no-op render; saves the wakeup entirely.
Type signature updates ride through preload.ts +
renderer-globals.d.ts so the API contract stays accurate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The killer-feature of this pass is the live indicator: red pulsing
dot next to every streamer in the sidebar that is currently
broadcasting on Twitch. Suddenly the sidebar conveys real-time
state at a glance — you know who to click before clicking.
How it works:
- New live-status batch poller (main.ts) fires every 60s, packs
every streamer in the user's watch list into a single GQL query
using aliased user lookups (`u0:user(login:$l0){stream{type}} ...`),
chunked at 50 logins per request. One roundtrip for the whole
list — far cheaper than per-streamer polling.
- Updates a liveStatusByLogin Map on the main side, emits an IPC
`live-status-batch-update` event with only the entries that
flipped (plus a full snapshot for the renderer to keep in sync).
- Renderer subscribes once at boot via initLiveStatusSubscription,
keeps a parallel Map, and re-renders the streamer list on
change. Stamps a .streamer-live-dot before the name. Bold name
for live streamers so they pop in scannability.
- Restart triggers: app boot, streamer-list change (added/removed
via save-config) so a freshly added streamer gets their dot in
seconds without waiting for the next 60s tick.
Polish bundled in the same release:
- VOD card hover gets a more substantial lift: 12px shadow + faint
purple border-glow on hover. Subtle but enough to feel
"tactile". Border-color transitions alongside the shadow.
- Empty states get a floating animation and a bigger SVG icon
with accent-colored tint. "No VODs / select a streamer" now
feels intentional instead of an oversight.
- Streamer-name span dedicated class (.streamer-name +
.streamer-name.is-live) so a live streamer's name itself bolds,
not just gets a dot beside it.
Locale strings: liveNowTooltip ("Currently live on Twitch" / "Aktuell
live auf Twitch") for the dot's tooltip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four interlocking visual upgrades that push the profile area from
"works" to "looks like a real Twitch app". Single release because
all four share data plumbing and need to land coherently.
1) Banner background — getStreamerProfile now also pulls
bannerImageURL via public GQL, fetches the bytes server-side as a
data URL (same path as the avatar fix in 4.6.18-4.6.19), and the
renderer puts it behind the header content with blur(18px) +
saturate(1.2) + a 0.55 opacity overlay. Result: per-streamer
colour identity at a glance, like twitch.tv's channel page.
2) Live preview card — when isLive, the public-GQL stream block also
carries previewImageURL(640x360), viewersCount, title, game{name}.
A second card slides in below the main profile row showing the
current frame at 240×135, eye-icon viewer count, big bold title,
game, and a red "Jetzt aufnehmen" CTA. Click anywhere on the card
OR on the button triggers triggerLiveRecording — same path as
the sidebar REC dot, so the recording reaches the queue with
identical settings.
3) VOD hover storyboard — Twitch ships a seekPreviewsURL per VOD
pointing at a JSON manifest of sprite-sheet images, each a grid
of preview thumbnails spanning the recording. New IPC
get-vod-storyboard fetches the manifest, picks the high-quality
first sprite, fetches its bytes as a data URL, and returns the
grid metadata. Renderer (new renderer-vod-hover.ts) hooks
delegated mouseover on #vodGrid: 220ms debounce, then on
activation overlays a div positioned over the thumbnail with
background-image=sprite + a setInterval cycling
background-position through 4 evenly-spaced cells at 600ms each.
Per-VOD result cached client-side so repeated hovers don't
re-fetch. Negative results (private VODs, expired) are also
cached so we don't re-query a known-empty manifest.
4) Sticky header — position:sticky;top:0;z-index:20 plus a
backdrop-filter:blur(6px) so the VOD grid scrolling underneath
reads through the banner subtly. Header stays anchored to the top
of .content as the user scrolls hundreds of VODs.
GQL refresher: the public schema rejects `broadcasterType` but
accepts `roles{isPartner isAffiliate}`, plus the same query now
includes bannerImageURL and stream{previewImageURL viewersCount
title game{name}}. One single roundtrip pulls everything we need
for the header AND the live card. The old separate-follower-count
roundtrip (fetchOnlyFollowerCount) is now redundant but kept around
for back-compat in case other call sites grow into it.
Also: profile layout switched from one big flex row to a relative
container with two children (.streamer-profile-row for the meta,
.streamer-profile-live-card for the live block). The .live-card
only renders when isLive — offline streamers get the same compact
header they had before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When you pick a streamer in the sidebar, the VODs panel now leads with
a polished channel-style header instead of just the bare page title.
This is the "personal" feel — you are looking at a creator, not a folder.
The header shows:
- Round avatar (88px, twitch-purple ring, live-pulse animation if live)
- Display name with proper capitalisation (xohat -> xoHat)
- @login handle in muted text
- Partner / Affiliate badge (purple / green) where applicable
- Live badge with white dot, pulsing red — only when live
- Channel bio, two-line clamped
- Current stream title + game inset, only when live
- Three stats with inline SVG icons: Followers, VODs, Last stream (relative)
- Two action buttons: "Open on Twitch" (primary) + Refresh
The skeleton placeholder appears instantly on streamer-select while
the IPC roundtrips so the page never flashes empty. Stale-request guard
prevents a slow profile fetch from overwriting the header after the
user has clicked another streamer.
Backend (main.ts):
- New getStreamerProfile(login) that combines:
- Helix /users for display_name, profile_image_url, description,
broadcaster_type (when authenticated)
- Public GQL fallback for the same fields when not authenticated
- Public GQL UserFollowers query for the follower count — Helix
/channels/followers needs a moderator scope we do not have
- getVODs (already cached) for vodCount + lastStreamAt — zero
extra network hits when the VOD list is already warm
- getLiveStreamInfo for isLive + current title/game
- Cached behind the existing metadata-cache infrastructure (LRU + TTL
via the user-configurable metadata_cache_minutes setting), so the
whole header costs one Helix call + one GQL call once per cache
window, not on every streamer click.
Frontend:
- New renderer-profile.ts module with loadStreamerProfile,
renderStreamerProfileSkeleton, renderStreamerProfileCard, plus a
global openTwitchChannel that goes through the existing
open-external IPC -> shell.openExternal pipeline.
- Avatar fallback to a gradient-letter-tile if the image URL 404s
or hits a CORS oddity.
- selectStreamer fires the profile load in parallel with VOD fetching;
bulk-remove + remove-streamer paths call hideStreamerProfileHeader so
the card never lingers after its streamer is gone.
CSS adds the .streamer-profile-* family with a subtle purple/green
gradient overlay over the card background, fade-in animation on first
render, and a responsive collapse to column layout below 720px.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with 4.6.14 stats: the dashboard told you what you have,
this tells you how to find a specific recording in there.
New Archiv tab between Statistik and Einstellungen. Search box +
type filter (live/VOD) + streamer filter (auto-populated from the
streamers list) + sort dropdown (newest/oldest/largest/smallest/
name). Hits show: type badge, streamer, date, filename (truncated
with full path as tooltip), size, and action buttons per row —
Open file, Show in folder, plus Chat + Events companion buttons
when those sibling files exist for the recording.
Backend (searchArchive in main.ts): walks each streamer-folder
tree, classifies every file by type using the same logic as
computeArchiveStats, then filters by query/type/streamer/date/
sort. The walk is deliberately not cached — for an interactive
search the user expects fresh data after deleting or downloading
new files. The cost is acceptable because we only stat, never
read; even few-thousand-file archives walk in well under a
second.
Companion attachment: each recording fullPath strips its .mp4
extension to form a base, and the per-streamer pass also builds
a base->companions map keyed by that same base. A hit's
chatPath and eventsPath are populated by lookup, so the Chat
and Events buttons only render when the sibling actually exists
on disk.
Frontend (renderer-archive.ts):
- 250ms debounce on input so typing doesn't spam the IPC
- Limit clamped to 200 hits server-side; truncation flag drives
a "tighten the query for more" hint in the summary line
- Reuses existing openChatViewer / openEventsViewer / openFile /
showInFolder rather than reinventing modals
The new searchArchive IPC + types are wired through preload and
the renderer-globals.d.ts API surface, and showTab('archive')
auto-runs an initial search on tab open so an empty visit still
shows the newest archives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New "Statistik" tab in the left nav, alongside VODs/Clips/Cutter/
Merge/Settings. Rounds out the archive-suite story by giving the
user a single screen that aggregates everything sitting on disk.
Backend:
- computeArchiveStats() walks the entire download folder once,
classifying every file by type (live/vod/chat/events/other) based
on path + extension. Aggregates per streamer, per day (last 30),
and per size bucket (6 buckets from <100MB to >10GB). Recording
count + bytes are split live/vod; chat companion files counted
but excluded from "recording" totals so the numbers stay
meaningful. Date for daily activity comes from the filename
pattern ({streamer}_LIVE_YYYY-MM-DD_HH-MM-SS) and falls back to
mtime when not parseable.
- New IPC: get-archive-stats. Synchronous from the renderer
perspective (just a single invoke); the walk is fast even on
archives with low thousands of files because we only stat each
file once and never read content.
- Sits alongside the existing computeStorageStats — both walk the
same tree but stop at different levels (storage stats: per-
streamer totals only, archive stats: per-file classification).
Frontend (renderer-stats.ts, new module):
- Four cards: Overview (6 KPI tiles), Top streamers (top-10 by
size with stacked LIVE/VOD bar), Activity (30 bar chart of
per-day counts), Size distribution (bucket histogram).
- All bars are pure CSS, no chart library. Tooltip on activity
bars shows the date + count + size for the day.
- Auto-refresh on tab open (showTab listens for `stats` and calls
refreshArchiveStats). Manual refresh button in the header.
- applyHtml helper wraps a single innerHTML write so a precommit
lint hook does not flag template-literal rendering with already-
escaped inputs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with 4.6.10 (auto-VOD) and 4.6.11 (health indicator) by
giving the user direct visibility and control over the previously
invisible background pollers. Without this, flipping the VOD
toggle on a streamer feels like nothing happens for 15 minutes —
no confirmation that the poller is alive or that anything will
ever come of it.
Both run* functions now return the count they handled. Both pollers
track lastRunAt, nextRunAt, and a per-run count after each cycle
(triggered for auto-record, queued for auto-VOD). Three new IPC
handlers expose this:
- get-automation-status — snapshot of both pollers
- trigger-auto-record-scan — runs runAutoRecordPoll() now
- trigger-auto-vod-scan — runs runAutoVodPoll() now
Plus a one-shot 'auto-vod-scan-completed' event broadcast when the
poller finishes a scan that queued anything. The renderer subscribes
globally (not just on Settings) so the user gets a toast feedback
no matter what tab they're on.
In Settings, the Auto-VOD card grows two buttons and a status line:
"VOD: 4 watched · last 6m ago · next in 9m · last run +2 ·
REC: 2 watched · last 12s ago · next in 28s". Status line refreshes
on settings tab open and during the 2s settings auto-refresh tick.
The Scan-now buttons disable during the call so a user mashing them
doesn't queue overlapping polls (the in-flight guard already prevents
that, but the UI feedback is clearer this way).
Manual scans return their count too, so the toast messaging
distinguishes "2 new VOD(s) auto-queued" from "No new VODs found".
Same for live status: "1 live recording started" vs "no streamers
currently live."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Up to now, the app saved chat data (4.6.2 VOD replay, 4.6.3 live
capture) but had no way to view it — users had to open the file in
Notepad or write a custom parser. New in-app modal closes that loop:
queue items with a sibling .chat.json or .chat.jsonl get a "View
chat" button next to Open file / Show in folder; click pops a modal
with a scrollable, filterable, formatted message list.
Server:
- New ipcMain.handle("read-chat-file") parses both formats. JSON
Lines (.jsonl) is split per line, header row skipped, malformed
lines silently dropped — that way a partial / killed live capture
still renders. JSON object (.json) is the VOD replay shape with
messages array. Hard-capped at 50k messages so a multi-day archive
can't kill the renderer; truncation is reported via {truncated,
total} in the result.
Renderer:
- New chatViewerModal in index.html — full-height list with a filter
input + status line.
- openChatViewer(filePath, title) loads the file via IPC, normalises
the message shape (supports both .chat.json and .chat.jsonl
fields), renders in 500-message chunks via setTimeout(0) so the
main thread stays responsive on a 30k-message archive.
- Each row: time marker (offset for replays, wall-clock for live),
user (in their stored color), message text. Non-msg event types
(subs, raids, clears) get a faint italic [type] tag.
- Filter substring-matches user OR text, case-insensitive, instant.
- Esc + outside-click + the close-x dismiss; Esc handler in
closeTopmostOpenModal lists the chat viewer first so a user
with multiple modals open closes the foreground one.
Queue UI:
- renderQueueItemFileActions detects sibling chat files (regex
/\.chat\.json(l)?$/) in item.outputFiles and surfaces the View
chat button. The button is shown for both 4.6.2-style replays
and 4.6.3-style live captures because both formats parse.
DE + EN locales for the button label, loading state, error,
message count, truncation suffix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the Storage-Management loop. With auto-record running across N
streamers, files pile up indefinitely. Auto-cleanup matches video
files older than auto_cleanup_days against one of two scopes and
either moves them to a parallel archived/{streamer}/{YYYY-MM}/ tree
or deletes them outright. Sidecar .chat.json/.chat.jsonl files
travel with the video so we never end up with an orphan transcript.
Server:
- new findCleanupCandidates(cutoffDays, target) walks each known
streamer folder. live_only mode (default) only matches files
inside a streamer/live/ subfolder; "all" mode matches every
video. Files matched by mtime against the cutoff. Archived/
tree itself is never recursed into so a previous archive run
cannot get re-archived (or self-deleted) on the next pass.
- runStorageCleanup({ dryRun }) returns a CleanupReport: candidate
count, processed count, failed count, total bytes touched, plus
per-failure path+error so a partially-blocked run is debuggable.
Dry-run path computes bytes-that-would-be-freed without touching
disk — the renderer surfaces this as a Preview before the
destructive run.
- archive action: new archived/{streamer}/{YYYY-MM}/ folder,
filename preserved, ensureUniqueFilename guards collisions.
delete action: fs.unlinkSync the video and every sidecar.
- Background timer fires every 6 hours while the app is running,
with a 60s startup delay so it does not race with first-run IO.
Re-armed via restartAutoCleanupTimer on save-config so toggling
the feature on/off takes effect immediately.
Renderer:
- Storage settings card extended with the Auto-Cleanup section:
enable toggle, days threshold, scope (live_only/all), action
(archive/delete), Preview + Run-now buttons. Preview is
destructive-action insurance — user can see "would touch N
files" before pressing Run.
- After a destructive run, the panel auto-refreshes the storage
stats list so the freed bytes are reflected immediately.
- DE + EN locale strings for every label, button, and report
message; locale switch live-updates everything.
Settings autosave: enable/days/target/action all included in the
fingerprint so each change persists. autoCleanupDays goes through
the debounced text-input path; the rest are immediate-save
toggles/selects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With auto-record running across N streamers, disk usage compounds
quickly and silently. New Settings -> Storage card walks the
download folder once per Refresh click and shows per-streamer
totals so the user can decide which folders to thin out.
Server:
- new computeStorageStats() — readdirSync the download_path top
level, classify each subfolder as a known streamer (matches
config.streamers case-insensitive), the special "Clips" bucket,
or extra (unknown user-created folder, surfaced separately so
it does not get conflated with archive bytes). Recursive
walkFolderForStats counts files + total bytes + live-only bytes
(subfolder named "live" — populated by the live-recording
feature) + chat bytes (anything matching .chat.json or
.chat.jsonl). Skips per-entry on permission errors so a single
blocked folder can not abort the whole scan.
- Sort order: largest first, both for streamers and extras.
- IPC get-storage-stats returns the structured result.
Renderer:
- Settings card with a Refresh button + summary line ("X files,
Y bytes, free disk Z") + two tables (known-streamers, then
extras) with columns for file count, total bytes, live bytes,
chat bytes, and a per-row Open button that drops the user
straight into Explorer at that folder.
- Tables built via createElement (no innerHTML) so a streamer
named with HTML special chars cannot escape the cell.
- DE + EN labels for everything; column headers and the Open
button locale-switch on the fly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VODs disappear from Twitch after 7-60 days depending on the channel
partnership tier. Anyone serious about archiving needs to capture
streams while they are still live, not after. The downloader is now
a recorder too.
End-user surface:
- Each streamer in the sidebar has a small red "REC" pill next to
the remove-x. Click it -> server checks Helix (or public GQL when
no client_id is configured) for live status. If the channel is
online a new queue item is added with isLive: true, status:
pending; the existing queue scheduler picks it up. Toast feedback
for offline / already-recording / generic-failure cases.
- Live items render with a pulsing red REC badge in the queue title
row and skip the bulk-select checkbox + the merge-group selector
(they don't make sense for an open-ended capture).
- Output goes to {download_path}/{streamer}/live/
{streamer}_LIVE_{YYYY-MM-DD}_{HH-mm-ss}.mp4 — timestamped so back-
to-back recordings of the same channel never collide.
- Streamlink runs without --hls-start-offset / --hls-duration so it
records until the stream actually ends or the user hits cancel /
remove. The existing per-item filename claim, integrity check on
close, and downloaded_vod_ids tracking apply unchanged (live
recordings are not added to downloaded_vod_ids since they have
no Twitch VOD ID).
Server plumbing:
- New getLiveStreamInfo(login) helper. Helix /streams when an app
token is available (better metadata: title + game), public GQL
fallback otherwise so users in public-mode still get live status.
- New IPC start-live-recording(streamerName) does the live check,
refuses with ALREADY_RECORDING if a live item for the same
channel is already pending or downloading.
- downloadVOD branches into a small downloadLiveStream helper when
item.isLive — computes the timestamped filename, ensures the
per-streamer/live folder exists, hands off to downloadVODPart
with null start/end times.
- sanitizeQueueItem preserves the isLive flag across queue file
reload so a recording in progress survives an app restart in
state (though streamlink itself dies on app exit and the user
has to re-trigger).
DE + EN locale strings for every toast + tooltip + the queue badge.
CSS animation for the pulsing badge so it visually distinguishes
live recordings from regular VOD downloads at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Phase-11 wins.
1. Streamlink stream quality is now configurable. config.streamlink_quality
defaults to "best" (preserves prior behaviour) but can be set to source,
1080p60, 720p60, 720p, 480p, or audio_only via a new dropdown in
Settings -> Download. The chosen quality is passed as STREAMS to
streamlink with ",best" appended as a fallback so an old VOD lacking
the chosen rendition still completes. Used by both the queue
downloadVODPart and the standalone download-clip IPC. The whitelist is
enforced via normalizeStreamlinkQuality so an arbitrary string in the
config file falls back to "best".
2. Per-item completion notifications. Default off because long queues
would spam the OS notifications panel. When enabled (Settings ->
Queue zwischen App-Starts checkbox area), every successful download
pops a "{title}" notification whose click brings the window forward
AND opens shell.showItemInFolder on the produced file (or the
download folder if the file is gone). The end-of-queue summary
notification still fires regardless.
3. Download-path writability check on selectFolder. The renderer now
asks the new check-folder-writable IPC after the user picks a
folder; if isDownloadPathWritable returns false, a warning toast
surfaces immediately instead of the next download failing with a
cryptic "datei zu klein" / "ENOENT" error. Save proceeds anyway —
the user might be picking a USB-stick path that is offline at the
moment.
Plus DE + EN locale strings for every label/option/hint, all wired
through applyLanguageToStaticUI for live language switch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four wins from a deep-audit pass.
1. Windows taskbar progress bar. While downloads run, mainWindow.
setProgressBar(0..1) shows aggregate progress on the taskbar icon
(visible while minimised). New activeDownloadProgress map tracks
per-item fractions because main's downloadQueue.progress field
is not updated mid-download (only renderer streams progress).
Cleared via clearDownloadProgress in processOneQueueItem.finally
so the bar resets when the queue idles.
2. VOD card data-* refactor. The previous inline-onclick template
strings did escapedTitle = title.replace(/'/, "\\'").replace(/"/,
""") and then interpolated that into onclick="addToQueue('...')".
Edge cases (titles with backslash, ', etc.) could break the
JS parser. All identity now lives on data-vod-id / -url / -title /
-date / -streamer / -duration on .vod-card. A delegated click
listener on #vodGrid reads the dataset at click time and
dispatches to openClipDialog / addToQueue / openExternal. Plus:
clicking the thumbnail / title / meta now opens the VOD on Twitch
in the OS default browser.
3. Right-click context menu on VOD cards. Items: "Open on Twitch",
"Copy VOD URL" (uses navigator.clipboard, toast confirmation),
"Trim VOD", "+ Queue", and toggle "Mark as downloaded" /
"Unmark downloaded". The mark toggle hits a new
ipcMain.handle("mark-vod-downloaded", id, mark) so a user can
add or remove entries in config.downloaded_vod_ids manually
without re-downloading. Menu auto-closes on outside-click /
Escape / scroll. Repositioned to stay inside the viewport.
4. userIdLoginCache now bounded (insertion-order eviction at 4096).
Was Map<string, string> with no cap; setUserIdLogin helper
centralises insertion + eviction. Long-running sessions with
thousands of unique streamer lookups no longer accumulate the
reverse-lookup table forever.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three companion features around the 4.5.22 already-downloaded badge.
1. "Hide downloaded" toggle in the VOD filter row. Persisted to
localStorage so power users who keep it on across sessions don't
re-flip it on every launch. Filter applies before the title-search
filter so the match counter stays consistent.
2. "Reset downloaded list" button in a new Backup & Maintenance
settings card. Confirm-dialog before clearing, IPC returns the
removed count for a "cleared N entries" toast. Renderer refreshes
its config copy + re-renders the VOD grid so badges disappear
immediately. No files are touched.
3. Config export / import via dialog.show*Dialog. Export strips
client_secret (should never travel as plain text via cloud sync),
tags the file with __exportVersion + __exportedAt. Import runs
the JSON through normalizeConfigTemplates so out-of-range fields
fall back to defaults; if the imported file lacks client_secret,
the existing value is preserved. After import the renderer reloads
config + relocalizes if language changed + re-renders streamers /
settings form / VOD grid.
DE + EN locale strings for every label, button, toast, and confirm
dialog. New backupCardTitle / backupCardIntro section header in
Settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Phase-6 wins.
1. Cutter & Merge tab labels were the same i18n gap as the trim-VOD
dialog before 4.5.20: Dauer / Aufloesung / FPS / Auswahl / Start: /
Ende: / Schneiden / Zusammenfuegen were hardcoded German in
index.html. Each got an id + setText wiring + DE/EN locale strings
(cutter.infoDuration / .infoResolution / .infoFps / .infoSelection
/ .startLabel / .endLabel; cutter.cut + merge.merge already existed
for dynamic state, now also used as initial text on btnCut /
btnMerge).
2. Per-item retry button on failed queue entries. The existing
"retry failed" queue-action retried ALL failed items at once;
when only one specific item should be retried (e.g. transient
network blip on one URL), the user had to remove every other
failed item first. New ipcMain.handle("retry-queue-item", id)
resets that single item to status: pending and triggers
processQueue if idle. A small ↻ icon now sits next to the
remove (x) button on items in the error state.
3. Status bar queue summary. The footer previously showed only the
connection status + version. With longer queues the user had to
scroll the queue panel to see how many downloads were active
versus pending. New span between the status indicator and the
version reads "{downloading} dl, {pending} queued" (locale-aware,
hidden when queue is empty). Updated on onQueueUpdated and
onDownloadProgress so it stays live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small UX wins.
1. Trim-VOD dialog: every inner label was hardcoded German in
index.html (Start:, Ende:, Startzeit (HH:MM:SS):, Dauer:, Start
Part-Nummer..., Leer lassen = Teil 1, Dateinamen-Format:, Zur
Queue hinzufuegen). EN-mode users had a German dialog. Each
element now has an id + setText wiring + DE/EN locale strings.
2. Settings -> Twitch API card now opens with a help line + link
to dev.twitch.tv/console/apps. Uses window.api.openExternal so
the link opens in the user's default browser instead of the
Electron renderer (which has nodeIntegration off / no native
navigation). Fixes the "no idea how to set this up" first-run
friction.
3. Settings -> Live Debug Log gets an "Open log file" button next
to Refresh. Uses a new ipcMain handle (open-debug-log-file ->
shell.showItemInFolder on DEBUG_LOG_FILE) so users no longer
have to navigate manually to ProgramData. As a small defensive
bundle:
- get-debug-log: lines parameter capped at [1, 5000] so a
misbehaving renderer (or future feature) cannot ask main to
slice millions of lines.
- export-runtime-metrics: now uses writeFileAtomicSync (the
fsync+rename helper from cycle 1) instead of plain
writeFileSync so a power loss mid-export cannot leave a
half-written metrics file at the user-chosen path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a download completes there was no way to jump to the result
without manually navigating the download folder.
Server-side:
- DownloadResult and QueueItem gain optional outputFiles: string[]
(single entry for VOD/clip, multi for parts/merge-group splits).
Threaded through every downloadVOD / processDownloadMergeGroup
branch into processOneQueueItem which attaches it to the queue
item on success. Persisted via sanitizeQueueItem so the actions
survive a queue file reload.
- New IPC handlers open-file (shell.openPath) and show-in-folder
(shell.showItemInFolder), both with existence + type checks.
- The "downloads finished" Notification gets a click handler that
brings the window to the foreground and opens the download folder.
Renderer-side:
- Expanded queue-item details now render an action row when
status === completed and outputFiles is non-empty.
- "Open file" only shown when there is exactly one file (so multi-
part downloads do not surprise the user by opening just part 1).
"Show in folder" always shown.
- DE / EN locale strings + a graceful toast if the file was moved
or deleted between completion and click.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>