The source repo is public now, so the separate releases-only repo
(AeroFetch-releases) is no longer needed to keep the updater tokenless.
Retarget UPDATE_REPO from AeroFetch-releases back to AeroFetch and refresh the
config doc comment. The dedicated read-only baked-token service account
(aerofetch-updater) has been deleted and its token revoked, so nothing reads a
token any more; the .gitignore comment for the retired .update-token file is
updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Windows daily-sync task launches `AeroFetch.exe --sync`, but the single-
instance lock routes that relaunch to the `second-instance` handler whenever
AeroFetch is already open. The handler only focused the window and looked for a
link -- it never checked `isSyncLaunch(argv)` -- so the sync never ran. The daily
auto-download of new watched-source uploads therefore did nothing for minimize-
to-tray / always-on users (and the relaunch stole window focus each day).
Add a `runWatchedSync` main->renderer push: on a `--sync` relaunch the handler
now nudges the renderer to run syncWatched() instead of focusing, so the daily
sync fires whether AeroFetch was closed or already running.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ship a smaller installer that no longer carries ffmpeg.exe/ffprobe.exe
(the bulk of its size). On first run they are downloaded from a pinned
upstream archive (gyan 8.1.2), verified against a pinned SHA-256, and
unpacked with the OS tar.exe into the managed userData/bin dir -- the
same place as the self-updating yt-dlp, so they survive app updates and
portable re-extraction. A hard onboarding gate blocks the app until they
are present, with a "locate existing ffmpeg" folder-picker fallback for
offline machines and a Repair action in Settings > About.
The updater's streaming/checksum/redirect/idle-timeout download loop is
extracted to lib/verifiedDownload.streamVerifiedFile and shared by both
the app-installer download and the ffmpeg fetch; the updater's public
behaviour and error strings are unchanged (its boundary tests still pass).
No new npm dependency: extraction uses the System32 bsdtar resolved by
absolute path (audit F3).
Note: this commit also carries pre-existing, in-progress aria2c/network
and updater-token work that was already uncommitted in the working tree
and is entangled with the above in shared files (updater.ts, ipc.ts,
preload/index.ts, mockApi.ts, shared/ipc.ts, plus the settings/network
files and aria2c.exe). It was not cleanly separable by path, so it is
included here rather than split out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
describeDownloadError condenses a postprocessing failure to one line for
the queue/notification, discarding the rest of yt-dlp's stderr. The
persisted error log now also keeps the fuller non-noise transcript
(stderrDetail) as an optional `detail` field, and the Diagnostics card
gets a collapsible "Show details" expander per row so a failure like
"Postprocessing: Conversion failed!" can be inspected -- and copied
into a bug report -- without re-running the download from a terminal.
yt-dlp collapses an ffmpeg postprocessing failure to a bare
"Postprocessing: Conversion failed!" with no indication of which step
died, and without --verbose the underlying ffmpeg reason is never
emitted at all. describeDownloadError() appends the last non-noise
trailing lines (e.g. the [ModifyChapters] SponsorBlock re-encode) so
the error log and completion notification point at the actual cause
instead of a dead end.
The release repo is private, so the auto-updater previously required each
user to paste a Gitea token into Settings before it could see or download
updates. Inject a read-only token at build time (AEROFETCH_UPDATE_TOKEN env
var or a gitignored .update-token file) via electron.vite's `define`, and
fall back to it in authHeader() when the user hasn't set their own. The token
value lives only in the built bundle, never in source or git.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ROADMAP/ROADMAP-PINCHFLAT docs were removed; this drops the now-dead
"see ROADMAP-PINCHFLAT.md" breadcrumbs (and one "the roadmap" mention)
from source comments, preserving the surrounding Pinchflat-style context.
Comment-only; typecheck (node+web) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-implements the sleepRequests/sleepInterval/maxSleepInterval feature
(--sleep-requests / --sleep-interval / --max-sleep-interval) from the
stale feat/binary-management-and-library-scale branch, ported onto
current main's structure (zod settingsSchema, decomposed NetworkCard).
All default to 0/off, preserving current behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-demand maintenance panel (SourceRetention) on the source detail —
not a persisted schedule (roadmap's niche-for-desktop scope).
Prune: keep the newest N downloaded files, delete the rest's files
(download archive prevents silent re-download); dry-run count + two-step
confirm before any deletion. Upgrade: re-enqueue items whose recorded
downloadedQuality ranks below the source's current target (profile or
global). Selection is pure + injected-now in @shared/retention.ts
(selectPruneCandidates/selectUpgradeCandidates + qualityRank), unit-
tested; pruneMediaItems (main) deletes best-effort and clears state.
MediaItem.downloadedQuality now recorded through the completion path.
Live-verified the panel renders. 366 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A MediaProfile bundles { kind, quality, options, outputTemplate, extraArgs }
that a Source points at via Source.profileId, overriding the global defaults
field-by-field for that source's downloads.
Backend mirrors CommandTemplate: profiles.ts (createJsonStore, PROFILES_MAX,
isProfileLike + sanitize), CRUD IPC (profiles:list/save/remove) + preload +
useProfiles store; Source.profileId + sources:set-profile. Pure resolution
in @shared/profileResolve.ts (profile → global, per field; unit-tested).
Applied in the sources-store enqueueItems; a per-download collectionOutput-
Template override threads to buildArgs so a profile's outputTemplate wins.
UI: ProfileManager (mirrors TemplateManager) in a new Settings → Advanced →
Media profiles card, plus a per-source Profile picker in the Library source
detail. Both live-verified in the preview; 354 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Command preview + incognito toggle were already wired end-to-end in the
DownloadBar (Show command button, Incognito checkbox) — flipped the two
stale ROADMAP.md checkboxes with notes; verified live in the preview.
Output-path setting (PINCHFLAT): new Settings.collectionOutputTemplate — a
raw yt-dlp -o template overriding the default Channel/Playlist/NNN-Title
layout for Library downloads (empty = default). collectionOutputTemplate()
takes an optional custom template (joined under outDir, left for yt-dlp to
expand); download.ts passes the setting; zod schema validates it as a safe
relative template; Settings -> Filenames field. Tests in buildArgs +
settingsSchema; live-verified the field renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zod field schemas in settingsSchema.ts replace setSettings' bespoke
per-key switch: parse → skip-on-fail → store, side effects (secret
encryption, launch-at-startup sync) kept in settings.ts. sanitizeOptions
extracted to settingsOptions.ts to break the import cycle; backup import
flows through the same schema. Field-by-field parity pinned in
test/settingsSchema.test.ts (15 tests); full suite unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CC1: customCommandEnabled→enableCustomCommands, downloadArchive→
useDownloadArchive, clipboardWatch→watchClipboard, sidebarCollapsed→
isSidebarCollapsed, hardwareAcceleration→useHardwareAcceleration,
videoDir/audioDir→videoFolder/audioFolder (+ chooseDir/clearDir store
actions → chooseFolder/clearFolder). Rename map in settingsMigration.ts
(pure, unit-tested incl. pre-rename backup fixture), applied as an
idempotent on-disk shim at store open and on backup import so old
settings.json and old backups both keep working.
CC11: update host/owner/repo + release API moved to src/main/config.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 pinning tests lock downloadAppUpdate's security invariants (redirect
re-validation per hop, checksum refusal paths, SHA-256 verify + cleanup,
truncation, idle-timeout, cancel in both phases, progress) with a
scripted net.request fake; then the nested streaming block is extracted
verbatim into streamInstallerToFile. downloadAppUpdate is now a linear
pre-flight; B6 cancel-slot ownership stays with its module state via
onCancelReady/onSettled. Pins green before and after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI23: cookie sign-in + PO-token windows open with the app's resolved
theme background (passed from the IPC layer), no more white flash.
W12: multi-resolution fallback tray icon (16/24/32/48 via
addRepresentation dataURL, embedded in trayFallbackIcons.ts); decode +
scale factors verified in an Electron probe.
W15: hardwareAcceleration setting (default false = software rendering);
index.ts gates disableHardwareAcceleration on it after the portable
redirect; Appearance card switch with restart note; boot path verified.
L131: resolved by-design — clearing the queue acknowledges failures;
flashFrame (W10) covers attention, Diagnostics persists the record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two small self-contained features from the audit's deferred list:
- L51: the Task Scheduler daily sync is no longer pinned to 09:00. New
Settings.syncTime (24h HH:MM) with a native time input beside the Library
"Daily sync" switch — persists on change, re-registers the task on blur
(schtasks /Create /F overwrite is the time-change mechanism). The value
reaches the schtasks /ST argv, so a shared isValidSyncTime guards it at
both the settings write and in schedule.ts (unit-tested incl.
injection-shaped input). Live-verified against the machine's real task.
- L64: aria2c connection count (-x/-s) is user-tunable. New
Settings.aria2cConnections (1-16, aria2c's own ceiling) exposed as a
SpinButton in Settings -> Network while the aria2c toggle is on.
ARIA2C_ARGS became the pure aria2cArgs(n?) with a defensive clamp;
threaded through AccessOptions. The -k 1M split floor stays fixed — a
free-text field would bypass the custom-command consent gate.
typecheck + 309 tests + eslint + production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The watched live session for the six deferred lifecycle items, each verified
against real yt-dlp/Electron behavior on Windows:
- R4: cancel now deletes the download's orphaned partials. The engine captures
the final output path via a new `--print before_dl:dest|%(filename)s`
(empirically %(filepath)s is still 'NA' that early) and the close handler
sweeps only unambiguous intermediates (.part / .part-Frag* / .ytdl /
<stem>.fNNN.<ext>) via the pure, unit-tested lib/partials.ts — never a bare
<stem>.<ext> or another download's files. Live-verified with decoys.
- L65: verified-acceptable, no code change — a taskkill /F pause leaves the
.part byte-intact and yt-dlp --continue resumes at the exact byte offset.
- L139: spawn↔self-update interlock (new ytdlpLock.ts). The updater refuses
while any yt-dlp spawn is live; download/terminal IPC handlers hold (await)
behind an in-progress exe rewrite instead of failing.
- B2: source indexing is cancellable — AbortSignal through the probe walk
(kills the in-flight child, live-verified via tasklist), sources:index-cancel
IPC, and a Cancel button in the Library add-source row.
- B6: app-update download is cancellable — app:update-cancel routes through
the existing finish() teardown (abort + destroy + unlink, live-verified on
Electron net.request); Cancel button under the update progress bar. The
checksum phase is cancelable too.
- L143: closing the window mid-download (non-tray mode) now offers a native
tray-independent "Quit anyway / Keep downloading" dialog, replacing the
passive "use the tray to quit" notification.
Peer-review pass caught and fixed: a non-reentrant update lock (concurrent
begin clobbered the settle promise), a dead Cancel window during the B6
checksum fetch + a canceller slot-ownership bug (L140 shape), and a
persist-after-cancel hole in the index walk.
typecheck + 304 tests + eslint + production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new in-app toast surface is the batch's spine, making the silent-failure and
global-status gaps fixable:
- Toast infra: store/toasts.ts (useToasts + toast(); auto-dismiss) + ui/Toaster.tsx
(non-portal bottom-center stack, avoids the Fluent-portal GPU flicker). Unit-
tested (test/toasts.test.ts). Mounted at the app root beside LiveRegion.
- UX6: shared renderer reveal.ts revealFile('open'|'folder') awaits the IPC result
and toasts the main-process reason instead of silently no-oping on a moved/typed-
out file. safeShowInFolder upgraded to return an error string like safeOpenPath
(preload/mock/Api types updated).
- UX9 / UI25: a Fluent CounterBadge on the sidebar Downloads nav item shows the
active (downloading + queued) count from any tab, via a count-only App selector
that doesn't re-render on progress ticks.
- UX15: cancelAll store action + a "Cancel all (N)" header button.
- UX24: the disabled "Check watched" button uses disabledFocusable + a Hint that
explains it needs a watched source.
- UX12: the Terminal gate gains an inline "Enable custom commands" button, so it's
no longer a dead-end pointing at another screen.
- L144: the DownloadBar duplicate guard now also checks history — a URL downloaded
earlier warns "Already downloaded: …" (dupInHistory) vs "Already in your queue: …".
Deferred with rationale (CODE-AUDIT.md): UX7/UX8 (Settings IA redesign — visual),
UX18/UX26 (subjective/visual polish), L131 (arguably by-design, needs live taskbar),
L142 (history/templates/sources IPC-return reconciliation — a focused own PR).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contained consistency consolidations; the sweeping ones are deferred with
their standard documented (they're the audit's own post-1.0 refactors, and
several would be reckless to do unattended).
Landed:
- src/main/lib/lineBuffer.ts (createLineBuffer): one newline line-splitter for
streamed child-process output, replacing the duplicated buffer loop in
download.ts and terminal.ts (CC3/CC4/SIMP5). Pure + unit-tested
(test/lineBuffer.test.ts, 7 cases: partial lines, CRLF, empty lines, flush,
multi-line chunks). download.ts keeps byte-identical marker parsing and no
close-flush (yt-dlp template lines are always newline-terminated).
- ytdlp.ts: getYtdlpVersion/updateYtdlp now run stderr through cleanError, the
same `cleanError(r.stderr) || r.error?.message || ''` idiom already used by
download/probe/indexer (CC6) — a failed check shows the trailing error line.
- CL6 resolved: the one real redundancy was L15; clearDir/openFile/showInFolder
are deliberate view-model wrappers (CC13), left as-is.
Deferred with documented standard + rationale (CODE-AUDIT.md): CC1 (breaking
persisted-key rename → migration), CC5+CL4 (security-critical updater
net.request, not live-verifiable here), CC7 (cosmetic arg-order), CC9 (zod dep),
CC10 (electron-store↔jsonStore split is deliberate for DPAPI), CC11 (config.ts
move), CC12 (file-tree reorg), CC13→L93, CC14→L142.
Verified: typecheck (node+web) + 275 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- W2/UX19: windowState.ts persists the window's normal bounds + maximized flag to
<userData>/window-state.json (no new dep); createWindow restores from it with a
screen.getAllDisplays() visibility guard so it never reopens off a disconnected
monitor. Debounced save on resize/move + save on close (which may hide to tray).
- W10: notify() flashFrame(true)s the taskbar when a completion/failure lands while
the window is unfocused/minimized (Windows clears the flash on focus).
- W11: already implemented (badge.ts overlay dot + setOverlayIcon) — ticked.
typecheck + 268 tests + eslint + prettier green. (Window restore + flash want a
live quit/relaunch confirm.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The queue/scheduler lived only in renderer memory, so saved/scheduled and other
pending downloads were silently lost on quit (audit M4). Persist them:
- main: queue.ts store (proven cached-atomic createJsonStore, queue.json) +
queue:list / queue:save IPC; QUEUE_MAX cap.
- shared: PersistedQueueItem = the durable subset of DownloadItem (runtime-only
progress/speed/eta/sizeLabel/finishing dropped).
- renderer: mirror the persistable items on every queue change (a signature over
the subset means progress ticks don't re-save; main's jsonStore coalesces the
writes) and rehydrate on launch via an App useEffect. Pure mappers extracted
to store/queuePersist.ts and unit-tested.
Restore semantics: downloading -> queued (yt-dlp continues the .part), saved +
scheduledFor survives so the promoter fires when due, paused/error return
actionable; completed/canceled are never persisted. A queueHydrated gate stops a
launch-time store change from wiping queue.json before it's read.
Reconciles ROADMAP.md. typecheck + 268 tests + eslint + prettier green.
(OS-level quit/relaunch cycle is worth a manual smoke test.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
schedule.ts keeps its own schtasks wrapper because it needs the numeric exit
code (getScheduledSync checks r.code === 0), which execFileAsync deliberately
doesn't surface. The 5 migrated sites are probe/ytdlp(x2)/ffmpeg/indexer/
download.probeMeta. Comment-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shouldAutoCheckYtdlp compared (now - lastCheck) >= interval; a backward system-
clock correction leaves lastCheck in the future so the difference is negative and
the daily check never comes due again. Treat a future lastCheck as skewed and run
the check. The renderer scheduled-item promoter is inherently fire-once (promotion
flips the item out of 'saved'), so it can't double-fire on a backward jump — a
forward jump firing early is documented and accepted as minor.
Unit-tested. typecheck + 263 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incognito lived only in the renderer (history skip), so a "private" download
still leaked in main: errorlog entries (title+URL), OS toasts showing the title,
and the signed-in cookies attached — the UI promised "no logging, no history, no
cookies" but only history held.
Plumb `incognito` through StartDownloadOptions and enforce it in main:
- logFailure skips the errorlog write (and the pre-spawn errorlog in ipc.ts).
- notify uses a generic outcome title and drops the detail body, so no title/URL
lands in the Windows Action Center.
- both cookie sources (--cookies login jar, --cookies-from-browser) are withheld,
so a private download can't be tied to the user's identity.
typecheck + 262 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hand-rolled `new Promise((resolve) => execFile(…, cb))` wrappers in
probe / ytdlp (×2) / ffmpeg / indexer.probeFlat / download.probeMeta with one
lib/exec.ts execFileAsync returning a normalized
{ok,stdout,stderr,timedOut,error} that never rejects. Each caller keeps its own
error mapping (timedOut -> "Timed out …" vs stderr) and JSON parsing; behaviour
is unchanged. windowsHide defaults on; per-call timeout/maxBuffer preserved.
Foundation for CC4/CC5 — the stdout line-buffer half lands with SIMP5, the
net.request half with SIMP16, at which point those checkboxes close.
Unit-tested (exec.test.ts) against the node binary: ok / stderr+nonzero-exit /
missing-binary / timeout. typecheck + 262 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnostics were invisible in a packaged build: main catches did a bare
console.error (DevTools suppressed in prod, M31) and the renderer's logError
(M29) wrote to an unreachable console. Add a small leveled logger
(src/main/logger.ts) appending to <userData>/logs/aerofetch.log with size-capped
rotation; all app/fs access is lazy+guarded so it's a safe no-op under tests.
- Route the 5 main-process console.* catch sites (jsonStore write-fail, settings
write-fail + plaintext-secret warn, cookie loadURL, yt-dlp auto-update)
through the logger.
- Add a fire-and-forget log:write IPC channel + preload logError() so the
renderer's logError forwards failures to the main file sink (closes the M29
"sink" half the code comments deferred to CC8). mockApi + Api type updated.
- Unit-test pure formatLine/composeMessage + no-op safety; update the R6
jsonStore test to assert against the logger.
The user-facing toast half of CC8 ties to the global status surface (UI25/UX9).
typecheck + 258 tests + lint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prior commit kept a pass-through `export { fmtBytes, fmtSpeed, fmtEta }` in
main/lib/formatters.ts purely so existing importers wouldn't have to change —
which re-exported fmtSpeed (zero importers, dead) and left download.ts re-
exporting fmtBytes/fmtEta with no consumer at all, undercutting the "one home"
goal.
Now the real consumers import from the canonical module directly:
- probe.ts and the download.test import from @shared/format.
- formatters.ts keeps only its internal `import { fmtBytes }` (for parseProgress)
and no longer re-exports.
- download.ts re-exports only parseProgress (its own function); the dead
fmtBytes/fmtEta re-export is removed.
No behavior change. typecheck + 248 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes H4 and the remaining formatter half of H2.
- New @shared/format.ts is the single home for fmtBytes/fmtSpeed/fmtEta, imported
by both main and renderer. main/lib/formatters.ts re-exports them.
- DownloadProgress now carries raw speedBytesPerSec/etaSeconds instead of
pre-formatted speed/eta strings. main's parseProgress emits the raw numbers.
- The renderer stores the raw numbers on DownloadItem; QueueItem formats them for
per-item display, and summarizeQueue sums/maxes them directly. The lossy
string->number->string round-trip in queueStats (parseSpeed / parseEtaSeconds /
a local formatSpeed / formatEta) is deleted, along with a duplicate fmtEta in
store/downloads.ts.
- Per-item and aggregate speed now use the same 1024-based scale; the aggregate
previously used a 1000-based formatter (a latent inconsistency).
Tests updated for the raw-number contract (parseProgress, summarizeQueue).
typecheck + 248 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Critical:
- C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS
in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all
derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx
shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts.
- C2: break the downloads<->sources circular import via a typed event bus
(store/coordinator.ts). App eagerly imports sources for side-effects so startup
load + scheduled --sync + downloadCompleted subscription still run.
Reliability:
- L140/L148: cancel/pause release the active slot synchronously; identity-guarded
releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected
by a stale entry nor run over the concurrency cap.
- L141: renderer history capped at 500 + url de-dup to match main.
- R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry
instead of an empty catch.
- R7: encryptSecret warns before the plaintext fallback.
typecheck + 243 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-best format option claimed ext:'mp4', but the real output container
for the best pick depends on the user's videoContainer setting (mp4/mkv/webm),
so it mislabeled mkv/webm outputs. ext is optional on FormatOption and the
option's label is the fixed "Best available", so the field is simply omitted.
hasAudio:true is kept (the best pick always merges an audio track).
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
writeJsonAtomic always pretty-printed (JSON.stringify(value, null, 2)), which
needlessly inflates size and write time for the media-items store (up to
MAX_ITEMS=20000 rows). Added an optional `pretty` flag (default true, so
history/sources/templates/errorlog stay hand-inspectable) threaded through
createJsonStore, and set it false for media-items.json.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M23: replaceMediaItems wrote [...newItems, ...others] and the JSON store sliced
the combined list to MAX_ITEMS, so re-indexing one large source could
silently evict ANOTHER source's tail. Now other sources are always kept in
full and only the source being (re)indexed is capped to the remaining
budget (MAX_ITEMS - others.length).
M27: enqueueItems forced settings.defaultKind for every item, so a channel
couldn't be queued as audio without flipping the global default. Added an
optional `kind` override (falls back to the default) and a video/audio
SegmentedControl in the Library action row, seeded from defaultKind so
existing behavior is unchanged until toggled.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audio quality preset "Best (MP3)" named a format that's wrong whenever the
audio format is opus/flac/wav. Renamed it to "Best" so the label never
contradicts the chosen audioFormat (which is picked separately). Bitrate presets
stay as bitrates and are already ignored for lossless formats (M21).
- AUDIO_QUALITY_OPTIONS[0]: 'Best (MP3)' -> 'Best' (buildArgs already had a
`case 'Best'` returning '0', so arg generation is unchanged).
- Updated the main + renderer defaults and the preview/mock/test fixtures.
- Added a one-line migration in getSettings() that rewrites a stored legacy
'Best (MP3)' to 'Best' so existing users' dropdowns match.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addressing issues found reviewing this session's audit commits:
REGRESSIONS FIXED
- updater.ts (L52): reverted the spawn(detached) installer launch. Raw
CreateProcess/spawn fails with ERROR_ELEVATION_REQUIRED if the NSIS build
flips to perMachine (the config comment invites this), and it silently
dropped shell.openPath's launch-error detection. Restored shell.openPath
(ShellExecute honors the elevation manifest) and kept the L52 goal by
replacing the 1500ms timer with setImmediate(app.quit).
- LibraryView.tsx (M15): native <button> doesn't inherit color (UA sets
ButtonText), so the source-card chevron (currentColor) would render wrong
in dark mode. Added color: colorNeutralForeground1 to cardHead.
INEFFECTIVE CHANGE REVERTED
- base.css (W18): forced-color-adjust:auto on * is the CSS default (no-op),
and [class*="backdrop"] never matches Fluent/Griffel's hashed atomic class
names. Reverted; W18 unmarked (needs real High-Contrast testing).
WEAK FIX REVERTED
- Onboarding.tsx (L67): "Skip" and "Get started" called the identical handler
on a single-screen onboarding. Removed the duplicate button; L67 unmarked
(real ask is a "show tips again" revisit affordance).
STYLE / CORRECTNESS
- Stripped UTF-8 BOMs accidentally added to Select/CommandPalette/App/
Onboarding by the PowerShell sanitizer; left pre-existing BOMs untouched.
- DownloadBar.tsx: merged the duplicate @shared/ipc import; removed a stray
double blank line.
- qualityOptions.ts (L29): restored the original `satisfies Record<MediaKind,
readonly string[]>` constraint + the noUncheckedIndexedAccess rationale
comment (the extraction had dropped both for a weaker `as const`).
- downloads.ts: removed double blank line left by the QUALITY_OPTIONS move.
- binaries.ts: corrected YTDLP_MISSING_MSG doc comment ("at startup" -> the
actual call sites).
typecheck + 242 tests + eslint all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The window title was always 'AeroFetch' regardless of activity, so screen
readers and taskbar previews couldn't tell whether downloads were running.
The taskbarProgress IPC handler (which already fires on each progress tick)
now calls mainWindow.setTitle() alongside setProgressBar/setOverlayIcon:
- Idle: 'AeroFetch'
- Downloading: 'AeroFetch -- N downloads active'
- Error: 'AeroFetch -- Error'
This reuses the same overlay badge label string so the text is consistent
between the taskbar tooltip and the title bar.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both DownloadBar (renderer, drag-drop) and deeplink.ts (main, argv) had their
own regex to extract URL= from a Windows Internet Shortcut file -- with subtly
different patterns (/^\s*URL\s*=\s*(\S+)/ vs /^URL=(.+)$/).
Added parseUrlShortcutContent() to @shared/ipc. Both callers now delegate to
it and apply their own http/https guard (looksLikeUrl in renderer, asHttpUrl
in main). The local parseUrlFile wrapper in DownloadBar is kept as the
validation compositing point; it's now a one-liner.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts,
coupling views to the store. Moved to src/renderer/src/qualityOptions.ts,
which assembles it directly from @shared/ipc. store/downloads.ts no longer
imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView)
updated to import from the new module.
L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a
detached spawn. The installer is spawned with stdio:'ignore' and unref'd so
it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate
so the IPC response returns to the renderer before the process exits.
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L46: Introduced YTDLP_MISSING_MSG constant in binaries.ts. All four callers
(download, indexer, probe, ytdlp) now emit the same user-facing string:
"yt-dlp.exe is missing. Open Settings -> Software update to re-download it."
This removes newline-embedded messages and four divergent phrasings.
L71: Removed readOnly from videoDir/audioDir folder inputs in SettingsView.
Added onChange handler so users can paste or type a path directly without
being forced to use the Browse button. The path still flows through
applySettings() in main, which validates it.
L74: Accent color swatches were triple-labeled (aria-pressed + aria-label +
title). Replaced with a single aria-label that embeds "(selected)" state,
plus title for the hover tooltip. Removed the incorrect aria-pressed
(these are selection buttons, not toggles).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L22: DownloadOptionsForm accepts optional `kind` prop. When set, hides
audio-specific fields (Audio format) for video downloads and vice versa
(Video container, Preferred codec) for audio downloads. Settings passes
defaultKind so the defaults card shows only relevant options.
L24: Replace lone window.open('htmlUrl', '_blank') with a new openUrl IPC
channel (shell:open-url) that calls shell.openExternal after validating
the protocol is http/https. Consistent with how all other external opens
work in the app. Preload + main handler + mock updated.
L27: DownloadBar URL-field Enter is now smart -- if formats/playlist are
already loaded (or probe errored), Enter triggers download; otherwise
it probes. Eliminates the keyboard dead-end where Enter never downloads.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sidebar collapsed state was an orphan in localStorage -- not backed up,
lost on portable installs, and inconsistent with every other preference.
Added sidebarCollapsed: boolean to the Settings type and DEFAULT_SETTINGS,
wired it through applySettings' boolean branch, and updated the renderer
store / main.tsx mock defaults. App.tsx now drives the toggle via
updateSettings({ sidebarCollapsed }) instead of localStorage.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L54: DownloadBar preview image now tracks failed src in state (same pattern as
MediaThumb); on load error shows the kind icon fallback instead of broken img.
L61: Convert taskbarProgress from ipcMain.on/ipcRenderer.send to ipcMain.handle/
ipcRenderer.invoke so all IPC channels use the same request-response idiom.
App.tsx calls void to explicitly discard the resolved promise.
L63: audioQuality() now has an explicit 'Best' case alongside the bitrate strings;
unknown labels still fall back to '0' but the default branch is documented.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L49: Remove newline from missing-binary error message in download.ts; inline
error spans render it as literal newline, now one sentence with period.
L53: backup.ts exportBackup/importBackup/showMessageBox no longer use win!
non-null assertions -- each uses a guarded ternary (same pattern index.ts
already used for chooseFolder).
L59: Extract PAGE_BACKGROUND to src/shared/theme.ts; main/index.ts and
renderer/src/theme.ts both import from it -- no more "keep in sync" comment.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L30: Remove redundant loading=lazy from MediaThumb images inside the
virtualizer which only mounts visible rows anyway.
L44: electron-builder.yml now excludes the actual eslint.config.js and
.prettierrc rather than non-existent legacy .eslintrc/.prettierrc.yaml.
L60: IpcChannels.externalUrl renamed from external-url to app:external-url
to match every other channel name category prefix.
L90: settings.ts sanitizeOptions now uses (ARR as readonly string[]).includes
pattern consistently with setSettings; removes the as-never hack.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H3: probe.ts now imports fmtBytes from lib/formatters instead of download.ts,
breaking the circular probe->download dependency direction.
M33: ipc.ts comment corrected to reflect that no batch cap exists; notes the
known M33 issue for future implementation.
L25: App.tsx taskbar subscriber skips IPC when fraction/mode/badge unchanged,
eliminating redundant roundtrips on every progress tick.
L32: paletteActions wrapped in useMemo so CommandPalette sees a stable array
reference; only rebuilds when isDark changes.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
H5: HistoryEntry now stores formatId+formatHasAudio; redownload passes them
back to addFromUrl so the original yt-dlp format is reused, not guessed.
Compound quality labels stripped to the preset prefix for old history.
L72: thumbnail now passed in redownload so re-queued rows keep their art.
H6: TerminalView log capped at MAX_LOG_LINES=2000 to prevent unbounded growth.
M21: --audio-quality omitted for lossless audioFormats (flac/wav).
L166: fmtEta hour rollover fixed in all 3 locations -- 2h00:00 not 120:00.
L165: queueStats formatSpeed precision aligned with fmtBytes.
L55: QueueItem suppresses sizeLabel when already in probed-format quality label.
L47: probeMeta null sends empty meta event so Resolving clears via SR6.
L68: notifiedBackground resets on window show for subsequent close-while-downloading.
L76: dup warning falls back to URL when title is still a placeholder.
L91: SettingsView onFormatSelect uses indexOf instead of unsafe tuple cast.
L98: Embed chapters field gets a hint text.
L108: BrowserWindow created with explicit title AeroFetch.
L37: pure formatters extracted to src/main/lib/formatters.ts and unit-tested.
L38: buildArgs test covers webm thumbnail suppression.
L39: CROP_SQUARE_PPA test is structural not exact-string.
L40: looksLikeUrl/looksLikeSingleVideo extracted to src/renderer/src/lib/urlHelpers.ts.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encrypt proxy / youtubePoToken / updateToken on disk via safeStorage (DPAPI on Windows), with a one-time launch migration for legacy plaintext. Decrypted before reaching callers/renderer; backup export still writes clear, as documented. Harden updater token handling to refuse cross-origin redirects on the authenticated REST check.
Extract the clipboard-link logic from DownloadBar into the shared useClipboardLink hook: add an optional filter (library skips single-video links), an offer() for external aerofetch:// / .url links, and a source field driving banner wording. Add looksLikeSingleVideo plus its unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closing the window no longer kills in-progress downloads, the library gains the
same copied-link suggestion the downloads tab has, and the in-app updater can now
authenticate to a sign-in-required Gitea.
Background / tray:
- The window's close handler now hides to the tray (instead of quitting) whenever
a download is in flight, even if "Keep running in the tray" is off — quitting
was killing the spawned yt-dlp processes. A one-time notification explains the
app is still running. (download.ts exposes hasActiveDownloads().)
- tray.ts now falls back to an embedded icon when no build/icon.ico ships, so the
tray actually appears — previously an empty icon meant the tray was skipped and
a minimized window could be stranded with no way back.
- New "Start with Windows" setting (launchAtStartup) wired to
app.setLoginItemSettings, synced at startup and on toggle. Useful with
auto-download so watched channels stay current in the background.
- The "Keep running in the tray" hint now explains it also enables background
auto-download of new uploads.
Library clipboard detection:
- Extracted the downloads tab's clipboard watcher into a shared useClipboardLink
hook and used it in the library's add-source field, so a copied channel/playlist
link is offered there too.
Updater fix (works on a private / sign-in-required instance):
- Added an optional updateToken setting. When set, the updater sends it as a Gitea
Authorization header on the release check, the checksum fetch, and the installer
download — so "Check for updates" works where anonymous access is blocked (the
previous "could not reach the update server" case). Blank = anonymous, unchanged.
No token is ever shipped; it's only ever sent to the host-pinned update host.
Settings gains a masked "Update access token" field.
Typecheck clean; 187 tests pass; electron-vite build clean. New UI verified in the
browser preview (tray/startup toggles, token field, library copied-link banner).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>