Files
AeroFetch/ROADMAP.md
T
debont80 4c2b54a599 feat(playlist): exact-format-per-item selection
Each video row in the playlist panel gets an exact-format button that
lazily probes just that entry's URL (reusing the existing single-video
probe IPC, no new main-process code) and shows a native Select of its
real formats. The choice flows through addMany -> formatId per queue
item. Probing is on-demand per row, never all up front, since a
playlist can hold thousands of entries and per-entry probes are
rate-limited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 16:55:58 -04:00

33 KiB
Raw Blame History

AeroFetch Roadmap — feature parity with Seal

AeroFetch is the Windows/Electron counterpart to Seal, the Android Material-You frontend for yt-dlp. The core loop (probe → format pick → queue → download → history) is already in place. This document tracks the remaining gap: everything Seal does that AeroFetch doesn't yet.

Sources: Seal GitHub · Seal on F-Droid · It's FOSS overview.

See also: ROADMAP-PINCHFLAT.md — a parallel track that extends AeroFetch from a one-shot downloader into a Pinchflat-style media manager (index entire channels → organize into Channel / Playlist / Title folders → keep in sync), reusing the existing queue/history/options rather than replacing them.

Post-parity work (YTDLnis-derived editing features + Windows-native/UX polish + reliability) is tracked at the end of this document under Post-parity (Phases L onward).


Already at parity

Video + audio download · audio → mp3 with embedded thumbnail/metadata · interactive format picker (probe) · quality presets · queue + concurrency cap + cancel · download history (open / show-in-folder) · clipboard auto-detect · light/dark theme · output folder + filename template · yt-dlp version check · NSIS installer + portable build.


Phase A — Core download power COMPLETE

A shared DownloadOptions model (src/shared/ipc.ts) carries the post-processing choices end-to-end. Persisted defaults are editable in Settings → Format & post-processing via a reusable DownloadOptionsForm component. (The per-download Options panel in the download bar is plumbed end-to-end — StartDownloadOptions.options/ extraArgs — but not yet surfaced in the bar UI; see audit UX1/M5.) The main process emits the flags in buildArgs (src/main/buildArgs.ts). All items verified in the UI preview (typecheck clean). Real-download smoke test (2026-06-23): crop-to-square cover, audio re-encode (opus), video container + codec preference (mkv + vp9 merge), subtitle embed (→ mov_text), chapter embed, and SponsorBlock-remove (output duration shrinks by the cut segments) are now all verified against live yt-dlp + ffmpeg in real-download.integration.test.ts. That pass found and fixed a real shipping bug — the bundled ffprobe.exe was missing (see CODE-AUDIT C1).

  • Playlist downloads. Probe now uses yt-dlp -J --flat-playlist and returns either a single video (with formats) or a flat playlist. The download bar shows a selection panel (scrollable checkbox list, select-all/none, live count) and enqueues each chosen entry as its own single-video download — reusing the existing queue/concurrency/history.
  • Subtitles. Download + embed (--write-subs, --write-auto-subs, --sub-langs, --embed-subs, --convert-subs srt); language field + auto-caption toggle.
  • SponsorBlock. --sponsorblock-mark / --sponsorblock-remove with a category multi-select (sponsor, intro, outro, selfpromo, music_offtopic…) + --force-keyframes-at-cuts.
  • More audio formats. Was mp3-only. Now best / mp3 / m4a / opus / flac / wav / aac via --audio-format.
  • Video container + codec preference. Container choice (mp4/mkv/webm) + preferred codec (-S res,fps,vcodec:…) to nudge toward av1 / vp9 / h264 without overriding the requested resolution.
  • Embed chapters. --embed-chapters toggle. (--split-chapters shipped in Phase L.)
  • Embed thumbnail crop-to-square for audio (Seal's "crop artwork") via thumbnail post-processor args. Smoke-tested : the embedded cover comes out a perfect square (360×360) — the --ppa crop recipe survives yt-dlp's shlex split + ffmpeg's filtergraph.
  • Per-download overrides. Collapsible Options panel in the download bar (shared DownloadOptionsForm) so one download — or one playlist batch — can deviate from the persisted defaults; "Reset to defaults" clears it. Threads options through addFromUrl → DownloadItem → startDownload and is carried on retry.

Phase B — Access & networking COMPLETE

buildArgs's former NetworkOptions param is now AccessOptions (still in src/main/buildArgs.ts) — it grew past pure networking once cookies and filename/archive flags joined proxy/rate-limit/aria2c. Smoke-tested (2026-06-23): --restrict-filenames (spaces → underscores) and --download-archive (a second run of the same URL is skipped with no fresh download) are verified end-to-end. Still untested live: the sign-in window's cookie export — it needs an interactive login against a gated video, which can't run unattended, so it stays reasoning + typecheck only. aria2c (optional, binary not bundled) and proxy (needs a live proxy server) are likewise not smoke-tested.

  • Cookies. Settings → Cookies has a source picker: - From a browser's cookie store--cookies-from-browser <name> (chrome/edge/ firefox/brave/opera/vivaldi — COOKIE_BROWSERS in src/shared/ipc.ts). - Sign-in window (built into AeroFetch) → opens an Electron BrowserWindow on a persisted session partition (persist:aerofetch-login, see src/main/cookies.ts); OAuth/SSO popups are allowed through onto the same partition. Closing the window exports its cookies to a Netscape-format file (userData/cookies.txt) which buildArgs passes via --cookies. Settings → Cookies shows last-saved time + a "Clear saved cookies" button (also clears the partition's storage).
  • aria2c external downloader. Settings → Network has a "Use aria2c downloader" toggle; when on and resources/bin/aria2c.exe exists, buildArgs emits --downloader <path> --downloader-args (ARIA2C_ARGS in src/main/buildArgs.ts: -x 16 -s 16 -k 1M). Missing binary falls back silently to yt-dlp's own downloader (checked in src/main/download.ts). Binary is optional and not committed — see resources/bin/README.md.
  • Proxy (--proxy) and rate limit (--limit-rate). Settings → Network fields (Settings.proxy / Settings.rateLimit), threaded through buildArgs's AccessOptions param.
  • Restrict filenames toggle (--restrict-filenames) + download archive (--download-archive, skip already-downloaded) — both in Settings → Filenames. The archive file lives at a fixed userData/download-archive.txt (getDownloadArchivePath() in src/main/settings.ts), not user-configurable.

Phase C — Custom commands & yt-dlp management COMPLETE

Named CommandTemplates (src/shared/ipc.ts) persist to templates.json (src/main/templates.ts, mirrors history.ts's plain-JSON pattern) and are managed inline in Settings → Custom commands via TemplateManager.tsx — no modal, matching the rest of the app's no-portal-overlay convention (see the Hint/Select comments re: this dev machine's GPU/driver). A template's extra yt-dlp flags are shell-split (parseExtraArgs, src/main/buildArgs.ts) and appended to the generated argv immediately before the -- URL terminator, so a template flag can override anything chosen earlier. Settings.customCommandEnabled + defaultTemplateId apply a template to every new download; the download bar's own Custom command panel lets a single download override that default or opt out entirely (extraArgs on StartDownloadOptions, mirroring the existing per-download options override). buildCommand() (src/main/download.ts) now centralises settings/access/ options/extraArgs resolution, shared by both the real startDownload spawn and the new preview path. All items verified in the UI preview + npm run typecheck + npm run test (11 new unit tests covering parseExtraArgs, formatCommandLine, and extraArgs ordering). Smoke-tested (2026-06-23): parseExtraArgs('--write-info-json --no-mtime') splits into discrete tokens and the flags reach a live yt-dlp run — the .info.json sidecar is written, confirming custom-command args take effect end-to-end.

  • Custom command templates. Named, editable templates of extra yt-dlp args (CommandTemplate), CRUD'd inline via TemplateManager.tsx; persisted to templates.json. Pick a default via Settings.defaultTemplateId (a Select in the Custom commands card). The "run custom command" mode is Settings.customCommandEnabled, applied to every new download unless overridden per-download in the download bar's Custom command panel.
  • Command preview (surfaced). IpcChannels.commandPreviewpreviewCommand() (src/main/download.ts) builds the exact argv via the same buildCommand() path a real download would take, then renders it with formatCommandLine. Now surfaced: the DownloadBar's "Show command" toggle (useDownloadBar.toggleCommandPreviewwindow.api.previewCommand(...)) renders the exact argv in an inline panel that reflects the current URL/format/options/trim/schedule. (Checkbox was stale — the button has shipped; re-verified in the browser preview: setting a URL + "Show command" renders the argv panel.)
  • In-app yt-dlp updater. updateYtdlp(channel) (src/main/ytdlp.ts) runs yt-dlp's own --update-to stable|nightly. Surfaced in Settings → About, right next to the existing version check, with a channel picker + result output.

Phase D — Library, UX & notifications COMPLETE

History stays plain JSON (history.json, 500-entry cap) — search/filter/select are done client-side over the already-small array, so the planned better-sqlite3 migration wasn't needed for this phase and was deliberately skipped to avoid a native-module build step for no functional gain at this scale. Failed downloads (pre-spawn rejections and in-flight yt-dlp failures alike) now persist to a new errorlog.json (src/main/errorlog.ts, 200-entry cap) independent of the queue, so a report survives Clear finished. Native notifications and the backup file both reuse existing settings/templates plumbing (getSettings/setSettings, listTemplates/the new replaceTemplates) rather than adding parallel storage. All items verified in the UI preview (typecheck + npm run test clean).

  • History search + multi-select + filter (video/audio) + bulk delete + re-download. HistoryView.tsx gained a search box, an All/Video/Audio filter, a "Select" mode with per-row checkboxes + select-all + Delete selected (new historyRemoveMany IPC, src/main/history.ts), and a per-row re-download button that re-queues the entry's URL/kind/quality via the existing addFromUrl.
  • Native OS notifications on completion / failure (Electron Notification, src/main/download.ts). Gated by a new Notify when downloads finish switch (Settings.notifyOnComplete, default on); clicking a notification refocuses the window.
  • Private / incognito mode (surfaced). DownloadItem.incognito flows through buildItem → history-skip → the QueueItem "Private" badge, and store/downloads.ts checks it before ever calling useHistory().add(...). Now surfaced: the DownloadBar's Advanced options panel carries an "Incognito mode (no logging, no history, no cookies)" checkbox (useDownloadBar.setIncognito), one-shot per download. The main-process promise is also complete — L136 (Batch 8) threaded incognito through StartDownloadOptions so main skips the errorlog write, drops the notification detail, and withholds cookies for a private download, not just the renderer's history skip.
  • Backup / restore settings + templates (export / import JSON). New src/main/backup.ts writes/reads { settings, templates } via a save/open dialog; replaceTemplates() (src/main/templates.ts) does a full restore rather than a merge-by-id. Surfaced as Settings → Backup & restore.
  • Error log / copy error report view (Seal's debug report). New src/main/errorlog.ts persists every failure (pre-spawn rejection, spawn error, or non-zero yt-dlp exit); Settings → Diagnostics lists recent entries with Copy full report and Clear log.

Phase E — Theming & platform polish COMPLETE

Some items are Android-specific in Seal and adapted to Windows here.

  • Theme presets + "follow system" and high-contrast. Settings → Appearance has a Theme select (Light / Dark / Follow system) and four accent presets — Rose, Coral, Amber, Teal ("Sunset-to-sea", default Teal) — each a 16-stop BrandVariants ramp sharing one lightness curve at a different hue (src/renderer/src/theme.ts). "Follow system" reads Electron's nativeTheme.shouldUseDarkColors in the main process (src/main/index.ts's resolveBackgroundMode/registerSystemThemeBridge, pushed to the renderer over a new system-theme:update channel into src/renderer/src/store/systemTheme.ts) so the native window background stays in sync too, not just the in-page theme. High contrast: nativeTheme.shouldUseHighContrastColors surfaces as a status row in Settings → Appearance with a shortcut to Windows' contrast settings (ms-settings:easeofaccess-highcontrast); actual color overrides are left to Chromium's native forced-colors handling since nothing in the app sets forced-color-adjust: none. The sidebar's quick toggle still works under "system" — it sets an explicit light/dark away from whatever's currently resolved, breaking out of auto. Verified in the UI preview (typecheck + npm run test clean): default Teal, switching to Rose/Coral/Amber, Light/Dark/Follow-system, and the sidebar toggle's break-out-of-system behavior all checked visually.

  • Windows "open with" / share integration. Both mechanisms named above, since a Win32 (non-MSIX) app can't register as an actual Share Target: - aerofetch:// protocol (aerofetch://download?url=<encoded>) — registered at install time via electron-builder.yml's protocols field (NSIS), and at runtime via app.setAsDefaultProtocolClient in src/main/index.ts (covers the portable build and dev, which have no installer step to do it for them). - Explorer "Send to AeroFetch"src/main/deeplink.ts's registerSendToShortcut() writes a .lnk into %APPDATA%\Microsoft\Windows\SendTo via Electron's shell.writeShortcutLink (no native deps). Sending a .url Internet Shortcut file (what Explorer/browsers create from "Create shortcut"/dragging a tab to the desktop) passes its path as argv; extractIncomingUrl() parses the URL= line.

    Both paths funnel through the same `extractIncomingUrl()` (only ever forwards http(s)
    targets — same restriction as the existing external-link window-open handler) and a new
    `external-url` IPC push channel. The renderer surfaces the incoming link via
    `DownloadBar.tsx`'s existing clipboard-suggestion banner (now labeled "Link received:"
    for this source instead of "Use copied link?") rather than a new UI element. Added
    `app.requestSingleInstanceLock()` + a `second-instance` handler so a second invocation
    (the OS relaunching us for the protocol/SendTo case) hands its link to the already-running
    window instead of opening a duplicate. `extractIncomingUrl()`'s parsing/validation logic
    has 10 new unit tests (`test/deeplink.test.ts`); typecheck and the full suite are clean.
    *Caveat: the OS-level wiring (protocol registration, second-instance routing, the SendTo
    shortcut) is main-process/Electron-only and can't be exercised in the Vite browser
    preview used elsewhere in this phase — worth a real install + a manual
    `aerofetch://download?url=...` / "Send to" smoke test, same spirit as Phases AC's
    real-download caveats.*
    
  • Onboarding / welcome screen on first run. Settings.hasCompletedOnboarding (default false, persisted) gates a full-screen Onboarding.tsx rendered in place of the sidebar + main content in App.tsx — not a Fluent Dialog, since this app avoids Fluent's portal-based overlays entirely (GPU/driver blank-overlay issue noted in src/main/index.ts and Select.tsx). Shows the brand mark, a one-line pitch, the download-folder picker (same field as Settings → Downloads), three tip bullets, and a "Get started" button that flips the flag. Gated on the settings store's loaded flag so a returning user's real settings can never get clobbered by a one-frame flash of the default-false state. Verified in the UI preview (typecheck + npm run test clean): screen renders, folder field/browse work, "Get started" dismisses into the normal app.


Not portable from Seal (noted for completeness)

  • SAF directory picker (Android storage) → already covered by the native folder dialog.
  • Android share-sheet / quick-download-on-share → mapped to Phase E's aerofetch:// protocol
    • Explorer "Send to" integration (a true Share Target needs an MSIX package, out of scope for this app's NSIS/portable distribution).
  • F-Droid distribution → N/A (AeroFetch ships NSIS + portable).

Post-parity — editing, native polish & reliability

With both parity tracks complete — Seal (Phases AE above) and the Pinchflat media-manager roadmap (Phases FK ) — this final track collects what's left, from two sources:

  1. A feature study of YTDLnis (the other major Android yt-dlp frontend), keeping only the features AeroFetch doesn't already have.
  2. A Windows-native + daily-use UX gap analysis of AeroFetch itself (things neither Seal nor Pinchflat cover, but that a desktop app should do).

Nothing here is built yet — every box is unchecked. Phases continue the existing lettering (AE Seal, FK Pinchflat) from L onward, and each item carries the yt-dlp flag and the AeroFetch file it touches so it can be driven straight from the existing buildArgs / DownloadOptions / queue plumbing rather than new infrastructure.

Sources: YTDLnis GitHub · YTDLnis docs · YTDLnis changelogs · F-Droid listing.

Already at parity with YTDLnis (do not rebuild)

Format/quality/container picking · SponsorBlock (remove/mark + embed-as-chapters) · embed subtitles/metadata/chapters · cookies + login · custom command templates · backup/restore · Observe Sources (= our Phase J watched sources + RSS) · incognito mode · history with filter + bulk re-download · filename templating · self-managing yt-dlp + ffmpeg binaries. AeroFetch is even with, or ahead of, YTDLnis on all of these.

Phase L — Media editing (YTDLnis headline features)

YTDLnis's signature differentiator. The keyframe plumbing already exists in AeroFetch (--force-keyframes-at-cuts is emitted today for SponsorBlock-remove in src/main/buildArgs.ts), so this is cheaper than it looks.

  • Trim / cut downloads. Keep only chosen time ranges (multiple allowed). → parseTrimSections (src/main/buildArgs.ts) normalises raw user text into yt-dlp --download-sections "*START-END" specs (+ --force-keyframes-at-cuts, deduped against SponsorBlock-remove). Threaded as StartDownloadOptions.trim through store/downloads.ts, surfaced as a collapsible Trim panel in DownloadBar.tsx (single downloads only). Implemented + unit-tested (buildArgs.test.ts — 7 new tests; typecheck + test green). Live smoke test pending, plus two follow-ups: timestamp-range only so far (chapter-based cuts via the probe chapters[] array, and a scrubber/preview player, still TODO).
  • Split by chapters into separate files.--split-chapters (already flagged as a TODO in Phase A above). One toggle on DownloadOptions + src/renderer/src/components/DownloadOptionsForm.tsx; pairs naturally with trim. yt-dlp keeps the full file and writes per-chapter files via its default chapter: template. Implemented + unit-tested (buildArgs.test.ts, typecheck + test green); live smoke test still pending.
  • Metadata editing before download. Change title / author / artist pre-download (YTDLnis: "modify metadata such as title and author"). Uses --replace-in-metadata FIELD ^.*$ VALUE (not --parse-metadata FROM:TO which splits on colons). Only backslashes need escaping in the replacement string. Title, Artist, Album inputs in DownloadOptionsForm.tsx; setting any override implicitly enables --embed-metadata. Unit-tested in buildArgs.test.ts.

Phase M — Queue & daily-use UX COMPLETE

Neither Seal nor Pinchflat covers these; they're what makes the queue pleasant to live in. Today src/renderer/src/store/downloads.ts has cancel + retry only.

  • Pause / resume individual downloads. → yt-dlp resumes .part files via --continue (already the default). Done: pauseDownload() (src/main/download.ts) reuses the taskkill /T /F tree-kill via a new killTree helper but flags the record paused so the close/error handlers stay silent (no error event/log/notification); new download:pause IPC + preload bridge. Renderer pause/resume (store/downloads.ts) + a paused status: pause frees a concurrency slot, resume re-queues WITHOUT resetting progress so the relaunch continues the partial. Pause/Resume buttons + a frozen progress row in QueueItem.tsx; paused items survive "Clear finished". typecheck + test green; live resume-continues-the-.part still wants a smoke test.
  • Reorder / prioritize the queue. Done as a "Download next" action rather than drag: since the queue renders newest-first while pump() promotes oldest-first, a reposition primitive is clearer than drag. prioritize(id) (store/downloads.ts) moves a queued item to the front of the promotion order; an up-arrow button on queued rows (QueueItem.tsx).
  • Aggregate progress + total ETA + combined speed. A header strip on the Downloads view summing bytes/speed across active items. Feeds straight into Phase O's taskbar progress bar. Done: pure summarizeQueue (store/queueStats.ts) → a strip in DownloadsView.tsx (overall %, combined speed, longest-ETA "time left"); unit-tested (test/queueStats.test.ts). A true remaining-bytes combined ETA is still out of reach — we don't track per-item sizes — so "time left" is the longest single ETA.
  • Drag-and-drop a link or .url file onto the window. Done: the download card (DownloadBar.tsx) handles onDragOver/onDrop with a dashed-outline drag highlight, accepting text/uri-list/text/plain (firstUrl) and dropped Windows .url Internet-Shortcut files (parseUrlFile, read via File.text()); both http(s)-validated by the existing looksLikeUrl. A dropped link fills the URL field.
  • Retry all failed. One action that re-enqueues every failed item. Done: retryAll() in store/downloads.ts re-queues all error items + a "Retry all failed (N)" button in DownloadsView.tsx. (Re-queues live queue items rather than reading errorlog.json — same effect for the on-screen failures.)
  • Duplicate detection. Warn ("you already have this") before enqueuing a URL already in the active queue. Done: pure sameVideo() (store/queueStats.ts, YouTube-id aware + normalised-URL fallback) drives a "Already in your queue — Download anyway?" banner in DownloadBar.tsx; unit-tested. Checks the live queue (which still holds recently-completed items); cross-checking history.json for older downloads is a possible extension.
  • Per-download scheduling + "save for later." Done: a saved status + optional scheduledFor (store/downloads.ts); pump() ignores saved (the time gate), and a 15s ticker promotes due scheduled items to queued. UI: a Schedule panel with a native datetime-local input in DownloadBar.tsx (future time parks the new download as scheduled), plus per-row Save for later / Add to queue actions in QueueItem.tsx; saved items survive "Clear finished". Distinct from the Phase J --sync schedule (which is for sources). The queue is now persisted across restarts (audit M4 — queue.json via src/main/queue.ts), so a parked/scheduled item survives a quit and its schedule fires on the next launch once due. (The app must still be running at the scheduled moment for an immediate fire; a schedule that came due while closed fires at the next launch.)

Phase N — Power-user surface COMPLETE

YTDLnis leans hard into this; AeroFetch had the building blocks (command preview, templates, per-item options) but not the surfaces. typecheck + test + npm run build clean.

  • Built-in terminal mode. Done: src/main/terminal.ts runs the bundled yt-dlp with raw user args (binary fixed; gated on customCommandEnabled, enforced in main), streaming stdout/stderr line-by-line over a new terminal:output channel (terminal:run/cancel IPC + preload). New TerminalView.tsx (args box, Run/Stop/Clear, live log, Ctrl+Enter) and a Terminal sidebar tab.
  • Per-playlist-item editing. Done: the playlist panel in DownloadBar.tsx gained a per-row video/audio toggle + All video / All audio batch buttons; addPlaylist enqueues via addMany with each entry's own kind (quality falls back to that kind's default).
  • Per-playlist exact-format-per-item. Done: each video row in the playlist panel has an exact-format button that lazily probes just that entry's URL (reusing the single-video probe IPC — no new main code) and reveals a native <Select> of that entry's real formats; the pick is carried as a ChosenFormat and flows through addManyformatId per queue item. Probing is on-demand per row (never all up front — playlists can be huge / rate-limited). Flipping a row to audio drops its format. Pure mapping in downloadBar/playlistEntries.ts, unit-tested (test/playlistEntries.test.ts); verified end-to-end in the preview harness.
  • Weighted format sorting ("format aspect importance"). Done: a formatSort field on DownloadOptions (raw yt-dlp -S string); when set it emits -S <value>, overriding the codec tiebreaker (buildArgs.ts); an "advanced" input in DownloadOptionsForm. Unit-tested.
  • URL-regex template auto-matching. Done: CommandTemplate.urlPattern (regex); selectExtraArgs auto-applies a matching template ahead of the global default (matchesUrl, bad patterns never match), URL threaded via resolveExtraArgs. urlPattern field in TemplateManager + persisted in templates.ts. Unit-tested.
  • Command palette + keyboard shortcuts. Done: a portal-free CommandPalette.tsx (filter, ↑/↓, Enter, Esc) opened with Ctrl/Cmd+K from App.tsx; actions = navigate tabs, new download (focuses the aerofetch-url input), toggle theme.

Phase O — Windows-native integration & discoverability COMPLETE

All in the main process. typecheck + test + npm run build clean.

  • Taskbar progress bar. Done: renderer pushes the summarizeQueue aggregate over a new taskbar:progress channel (App-level store subscription, no re-render); mainWindow.setProgressBar with mode: 'normal' | 'error' (red when any failed) and -1 to clear when idle.
  • System tray + minimize-to-tray. Done: src/main/tray.ts builds a Tray (icon resolved via getAppIconPath(); build/icon.ico added to electron-builder extraResources for prod) with Show / Quit. A new Settings.minimizeToTray (Settings → Appearance) makes the window's close hide to tray instead of quitting; before-quit/tray-Quit set an isQuitting flag so real exits still work.
  • Jump list. Done: app.setUserTasks adds an "Open AeroFetch" task (focuses the single instance). Thumbnail toolbar buttons deferred — they need per-button NativeImage assets this repo doesn't have tooling to generate here.
  • Taskbar overlay badge (win.setOverlayIcon) showing active-download count / error state. Green dot when downloading, red dot on error, cleared when idle. Pure-JS PNG generator in src/main/badge.ts (Buffer + zlib, no new deps); badgeCount threaded through the existing taskbarProgress IPC channel from summarizeQueue.
  • Settings discoverability. Done: a search box atop SettingsView filters the ~11 cards live by matching each card's text (DOM display toggle keyed off the root's children — no per-card refactor), with a "no settings match" hint.

Phase P — Reliability, longevity & trust (code shipped; cert + manual passes still required)

The least glamorous, highest-leverage track. The buildable code is done (typecheck + test + build clean); the parts that inherently need a human or a paid cert are delivered as artifacts.

  • PO Token / YouTube reliability plumbing. Done: Settings.youtubePlayerClient + youtubePoToken → one --extractor-args "youtube:player_client=…;po_token=…" group (accessArgs in buildArgs.ts, unit-tested), with "advanced" fields in Settings → Network. Lets a power user switch extraction client (web_safari/tv/mweb) or paste a token. Automatic WebView token minting shipped: src/main/poToken.ts opens a YouTube video page in a hardened BrowserWindow (shared login session), injects an async poll for ytInitialPlayerResponse.serviceIntegrityDimensions.poToken, saves the result encrypted via safeStorage. "Fetch" button in Settings → Network triggers it via youtube:po-token-mint IPC. Falls back gracefully (null) if YouTube's page structure changes.
  • Verify the shipped-but-untested OS wiring. *Can't be executed here (no real install). Delivered as a release-gate checklist — docs/SMOKE-TEST.md — enumerating every untested path (cookies window, aerofetch:// + Send-to, scheduled --sync + RSS, aria2c, proxy, plus the new tray/taskbar/pause-resume/terminal) with exact steps + expected results. Still needs a human to run it.
  • Code signing — non-goal. Decision: no certificate will be purchased, so builds ship unsigned and the SmartScreen first-run prompt is accepted. The build stays signing-ready — electron-builder picks the cert up from CSC_LINK / CSC_KEY_PASSWORD with no yml change (unset = unsigned, as today), documented in docs/SIGNING.md (OV vs EV, local + CI, HSM/EV hook, signature verification) — if that decision is ever reversed.
  • i18n / language setting. Still deferred — deliberately not faked. Shipping a language picker that doesn't change anything would be misleading; doing it properly means wiring an i18n library + extracting every string, with translations arriving incrementally. Tracked, not started.

Post-parity — what we reuse vs. build

Reuse unchanged: buildArgs/buildCommand flag emission, DownloadOptions + DownloadOptionsForm, the renderer queue scheduler + concurrency cap + per-item options, the persisted cookies BrowserWindow, the probe's chapters[]/format data, errorlog.ts, CommandTemplates, extractIncomingUrl(), and the self-managing binary updater.

Build new: the trim/split UI + --download-sections/--split-chapters wiring (L), the pause/reorder/schedule queue states (M), the terminal view + per-item playlist editor (N), the main-process taskbar/tray/jumplist integration (O), and the PO-token provider (P).

Post-parity — suggested order

L is the high-value headline — trim + split-chapters share one UI surface and the keyframe plumbing already exists, so they land cheaply and give AeroFetch YTDLnis's marquee capability. Then P's PO-token + verify-wiring as a reliability block (keeps YouTube working and de-risks shipped features). M and O are the daily-use polish and pair well (aggregate progress → taskbar bar). N is the power-user surface, each item independently shippable. Each phase is self-contained and rebuild-gated (npm run typecheck + npm run test + npm run build) the same way Phases AK were.