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>
- 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>
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 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>
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>
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>
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>
- Taskbar progress bar: renderer pushes summarizeQueue aggregate over a new
taskbar:progress channel (App store subscription); setProgressBar normal/error
- System tray (src/main/tray.ts) + minimize-to-tray (Settings.minimizeToTray);
close hides to tray, isQuitting guards real exits; icon via getAppIconPath()
(build/icon.ico added to extraResources)
- Jump list: app.setUserTasks "Open AeroFetch" (thumbnail toolbar deferred)
- Settings search: filters the ~11 SettingsView cards live by text match
Overlay badge deferred (needs a drawn NativeImage). typecheck + test (192) +
build all clean. Roadmap: Phase O COMPLETE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yt-dlp is now a managed, auto-updating binary rather than a static bundled
one. On launch AeroFetch seeds a writable copy under userData from the
bundled seed, self-heals it if it goes missing, and — throttled to once a
day — self-updates it to the configured channel (default: nightly). This
keeps the binary current so YouTube changes don't silently cause 403s, and
an app reinstall (or portable re-extraction) can no longer roll it back.
Settings shows an auto-update toggle, the live version, last-checked time,
and the channel selector.
Also surface the bundled ffmpeg/ffprobe versions in Settings (display only:
they have no self-update path and, unlike yt-dlp, don't break as sites
change, so there is no auto-update for them).
- binaries: split the read-only bundled seed from the managed userData copy
- ytdlp: ensureManagedYtdlp (seed + self-heal), startup auto-update runner
- ytdlpPolicy: pure once-a-day throttle decision (unit-tested)
- ffmpeg: parse ffmpeg/ffprobe -version for the Settings panel
- settings/ipc/preload/renderer: new settings, IPC channels, types, and UI
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new Settings → "Software update" card lets users update AeroFetch from inside
the app:
- src/main/updater.ts — queries the configured Gitea repo's latest release over
the public REST API, compares the tag to app.getVersion(), and (on request)
streams the installer asset to the OS temp dir with progress events, then
launches it and quits so it can replace the app.
- Security: only the trusted update host over HTTPS is ever downloaded from, and
only a freshly-downloaded .exe inside our temp dir is ever executed — the
download URL from the release JSON can't redirect us elsewhere. No token is
ever shipped; the source repo must be anonymously readable (public).
- IPC: app:update-check / -download / -run + an -progress push channel, exposed
via preload (checkForAppUpdate / downloadAppUpdate / runAppUpdate /
onAppUpdateProgress).
- UI: "Check for updates" → shows the new version and its release notes, then
"Update now" (with a download progress bar) or "View release". Up-to-date and
error states handled. Preview mock added so the flow is exercisable in `npm run ui`.
Note: the update source repo/host are constants at the top of updater.ts
(currently debont80/AeroFetch). Downloads work once the release repo is public
and the Gitea instance allows anonymous API + asset access.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seven-tier security audit of the main process, each finding fixed with a
regression test. Typecheck (node + web) clean; unit tests 106 -> 140.
- Tier 1 (command exec/argv): allowlist the yt-dlp --update-to channel
(blocks arbitrary-binary-install RCE); gate per-download extraArgs behind
the customCommandEnabled consent flag in main (blocks --exec RCE); resolve
taskkill/schtasks by absolute System32 path; validate per-download
outputDir; normalize the URL in assertHttpUrl and use it at every spawn.
- Tier 2 (input validation): fix isSafeFilenameTemplate drive-relative
('C:foo') and Windows dotted-'..' traversal bypasses; percent-encode the
untrusted id in entryUrl; catch reserved device names with extensions in
sanitizeDirSegment.
- Tier 3 (fs/backup): drop malformed template rows in importBackup;
normalize deep-link URLs via assertHttpUrl.
- Tier 4 (cookies): confine the sign-in window's navigations/popups to web
URLs (recursively) and deny all web permissions on its session.
- Tier 5 (deep-link/argv): bound the .url file read to 64 KB; match the
aerofetch:// scheme case-insensitively.
- Tier 6 (Electron window): deny camera/mic/geolocation/USB/HID/serial/
Bluetooth permissions on the app window.
- Tier 7 (network/persistence): restrict the watched-source RSS fetch to
youtube.com feed URLs (SSRF guard); complete isValidSource validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Expose the AeroFetch version over IPC (app:version → app.getVersion); the
sidebar brand now shows "v<version>" beneath the name.
- Sidebar can collapse to a 60px icon-only rail via a toggle button; nav items
and the theme control fall back to icon + tooltip when collapsed. The state is
persisted in localStorage so it survives restarts. Hint gains left/right
placements for the collapsed-rail tooltips.
- Replace the binary dark-mode Switch with an explicit 3-way Light / Dark / Auto
segmented control (Auto follows the OS). Collapsed, it becomes a single button
that cycles the three modes.
- Bump version to 0.3.2.
Verified in the browser UI preview: version label, collapse/expand, dark mode,
and the theme switch all render correctly with no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Create Documents\Video and Documents\Audio on startup (ensureMediaDirs), and
route each download into them by kind when no explicit output folder is set.
An empty outputDir now means "sort by type"; a chosen folder still overrides
for both kinds.
- Settings/Onboarding "Download folder" gains a placeholder + hint describing
the blank = route-by-type behaviour.
- Trim the home download bar to the essentials: URL box, Search and Paste
buttons, format/quality, and the Download button. Removed the private-mode
toggle, per-download Options panel, custom-command panel, command preview, and
the saving-to-folder line.
- Bump version to 0.3.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Index an entire YouTube channel or playlist once, then download it into
organized <Channel>/<Playlist>/<NNN> - <Title> folders, with incremental
re-sync. Built in six rebuild-gated phases (F-K); see ROADMAP-PINCHFLAT.md.
- F: channel-walk indexer (/playlists + /videos) -> persisted Source +
MediaItem JSON stores; pure classify/dedup logic in indexerCore.ts
- G: Windows-safe folder paths + dir sanitizer (collectionOutputTemplate)
- H: Library tab + source tree + "Download pending" into the existing queue
- I: state-preserving re-index merge + persist-downloaded-on-complete
- J: watched sources, RSS fast-check, Task Scheduler + --sync launch
(the OS-level scheduling/RSS need a real-install smoke test)
- K: .info.json / thumbnail / .description sidecars for media servers
Also gitignore .gitea-token. 106 unit tests pass; typecheck + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap Toffee/Slate/Evergreen/Lavender for rose/coral/amber/teal — four ramps
sharing one lightness/saturation curve (src/renderer/src/theme.ts). Default
accent is now teal. Updates ACCENT_COLORS (src/shared/ipc.ts), the settings
default + stale-value coercion (src/main/settings.ts), the dev/store fallbacks
(main.tsx, store/settings.ts), and the native window background to match the
new pageBackground.light (src/main/index.ts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds theme presets (Toffee/Slate/Evergreen/Lavender) with a "follow system"
mode and high-contrast awareness, a first-run welcome screen, and the
Windows analog of Android's share sheet for a Win32 app: an aerofetch://
protocol handler plus an Explorer "Send to AeroFetch" entry, both routed
through a single-instance lock so a second launch hands its link to the
already-running window instead of opening a duplicate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase C had been implemented in the working tree but never committed:
named custom-command templates (CRUD + per-download override), command
preview, and the in-app yt-dlp self-updater.
Phase D adds: history search/kind-filter/multi-select/bulk-delete/
re-download; native OS notifications on completion/failure; a private/
incognito download mode that skips history; settings+template backup/
restore via JSON; and a persistent error log surfaced as a Diagnostics
report in Settings.
See ROADMAP.md for the full per-item breakdown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the remaining Phase B (Access & networking) items from
ROADMAP.md: cookie sources (browser cookie-store import, or a built-in
sign-in window that exports a Netscape cookie file via a persisted Electron
session partition), the aria2c external downloader, proxy/rate-limit,
restrict-filenames, and a download-archive to skip repeats.
Wires download.ts to the buildArgs() module added in the previous commit
(it wasn't hooked up yet) and renames its options param
NetworkOptions -> AccessOptions to reflect the wider scope now that cookies
and filename/archive flags joined proxy/rate-limit/aria2c.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase A of the Seal feature-parity roadmap: configurable post-processing via a
shared DownloadOptions model (src/shared/ipc.ts), editable as persisted defaults
in Settings and overridable per-download in the download bar.
- audio formats (mp3/m4a/opus/flac/wav/aac), video container (mp4/mkv/webm),
preferred-codec sort (-S vcodec)
- subtitles (download/embed/langs/auto), SponsorBlock (remove/mark + categories
+ force-keyframes-at-cuts), embed chapters/metadata/thumbnail, square-crop
audio artwork
- playlist downloads: probe via `-J --flat-playlist`, inline selection UI
(checkbox list, select-all/none, live count); each entry enqueued as its own
single-video download, reusing the existing queue/concurrency/history
- reusable DownloadOptionsForm shared by Settings and the download bar so the
defaults and per-download override never drift
- ROADMAP.md tracking full Seal parity (phases A-E)
Also includes in-tree security hardening that landed alongside: preload sandbox
(CJS preload output), http(s)-only window-open + blocked renderer navigation,
argv-injection guard for URLs (src/main/url.ts), extension-allowlisted file
open/reveal (src/main/reveal.ts), yt-dlp version-check timeout, and a minimal
window.electron presence marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Electron + React (Fluent UI) desktop frontend for yt-dlp:
- Download queue with live progress, concurrency cap, cancel/retry
- Format/quality picker via yt-dlp probe; audio extraction to MP3
- Settings + history persistence (electron-store / JSON)
- Clipboard link auto-detect; persisted light/dark theme
- NSIS + portable packaging (binaries bundled at build time, not in git)
UI: "Studio" sidebar layout with a toffee-brown theme (light + neutral dark).
Dropdowns use a native <select> and tooltips a CSS-only Hint, to avoid a
dev-machine GPU overlay flicker/blank issue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>