20ee913394
- 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>
439 lines
30 KiB
Markdown
439 lines
30 KiB
Markdown
# AeroFetch Roadmap — feature parity with Seal
|
||
|
||
AeroFetch is the Windows/Electron counterpart to [Seal](https://github.com/JunkFood02/Seal),
|
||
the Android Material-You frontend for [yt-dlp](https://github.com/yt-dlp/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](https://github.com/JunkFood02/Seal) ·
|
||
[Seal on F-Droid](https://f-droid.org/packages/com.junkfood.seal/) ·
|
||
[It's FOSS overview](https://itsfoss.com/news/seal/).
|
||
|
||
> **See also:** [ROADMAP-PINCHFLAT.md](ROADMAP-PINCHFLAT.md) — a parallel track that extends
|
||
> AeroFetch from a one-shot downloader into a [Pinchflat](https://github.com/kieraneglin/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, which the download
|
||
bar also hosts in a collapsible per-download **Options** panel. The main process emits
|
||
the flags in `buildArgs` (`src/main/download.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](test/real-download.integration.test.ts). That pass
|
||
found and fixed a real shipping bug — the bundled `ffprobe.exe` was missing (see CODE-AUDIT C1).
|
||
|
||
- [x] **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.
|
||
- [x] **Subtitles.** Download + embed (`--write-subs`, `--write-auto-subs`, `--sub-langs`,
|
||
`--embed-subs`, `--convert-subs srt`); language field + auto-caption toggle.
|
||
- [x] **SponsorBlock.** `--sponsorblock-mark` / `--sponsorblock-remove` with a category
|
||
multi-select (sponsor, intro, outro, selfpromo, music_offtopic…) + `--force-keyframes-at-cuts`.
|
||
- [x] **More audio formats.** Was mp3-only. Now best / mp3 / m4a / opus / flac / wav / aac
|
||
via `--audio-format`.
|
||
- [x] **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.
|
||
- [x] **Embed chapters.** `--embed-chapters` toggle. (`--split-chapters` still TODO.)
|
||
- [x] **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.*
|
||
- [x] **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.*
|
||
|
||
- [x] **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).
|
||
- [x] **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`.
|
||
- [x] **Proxy** (`--proxy`) and **rate limit** (`--limit-rate`). Settings → Network fields
|
||
(`Settings.proxy` / `Settings.rateLimit`), threaded through `buildArgs`'s
|
||
`AccessOptions` param.
|
||
- [x] **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 `CommandTemplate`s (`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.
|
||
|
||
- [x] **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.
|
||
- [x] **Command preview.** `IpcChannels.commandPreview` → `previewCommand()`
|
||
(`src/main/download.ts`) builds the exact argv via the same `buildCommand()` path
|
||
a real download would take, then renders it with `formatCommandLine`. Surfaced as
|
||
a **Preview command** button in the download bar, with a Copy button.
|
||
- [x] **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).
|
||
|
||
- [x] **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`.
|
||
- [x] **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.
|
||
- [x] **Private / incognito mode** — a download bar toggle (`DownloadBar.tsx`, sticky like an
|
||
incognito tab) sets `DownloadItem.incognito`, which `store/downloads.ts` checks before
|
||
ever calling `useHistory().add(...)`. Queue items show an eye-off badge when private.
|
||
- [x] **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**.
|
||
- [x] **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.
|
||
|
||
- [x] **Theme presets + "follow system" and high-contrast.** Settings → Appearance has a
|
||
Theme select (Light / Dark / **Follow system**) and four accent presets — Toffee
|
||
(original), Slate, Evergreen, Lavender — each a 16-stop `BrandVariants` ramp sharing
|
||
toffee's 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 Toffee,
|
||
switching to Slate/Evergreen/Lavender, Light/Dark/Follow-system, and the sidebar
|
||
toggle's break-out-of-system behavior all checked visually.
|
||
- [x] **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 A–C's
|
||
real-download caveats.*
|
||
- [x] **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 A–E above) and the
|
||
[Pinchflat media-manager roadmap](ROADMAP-PINCHFLAT.md) (Phases F–K ✅) — this final track
|
||
collects what's left, from two sources:
|
||
|
||
1. A **feature study of [YTDLnis](https://github.com/deniscerri/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
|
||
(A–E Seal, F–K 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](https://github.com/deniscerri/ytdlnis) ·
|
||
[YTDLnis docs](https://ytdlnis.org/) · [YTDLnis changelogs](https://ytdlnis.org/changelogs/) ·
|
||
[F-Droid listing](https://f-droid.org/packages/com.deniscerri.ytdl/).
|
||
|
||
### 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.
|
||
|
||
- [x] **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).*
|
||
- [x] **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"). → `--parse-metadata` /
|
||
`--replace-in-metadata`; fits the audio path. Good for building a clean music library
|
||
where the source title is messy. *Held: the exact set-a-literal recipe is fragile
|
||
(`--parse-metadata FROM:TO` splits on a colon that titles often contain;
|
||
`--replace-in-metadata FIELD REGEX REPLACE` has empty-field + regex-replacement edge
|
||
cases) — confirm against a live yt-dlp run before wiring it, rather than guessing the
|
||
flag.*
|
||
|
||
## 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.
|
||
|
||
- [x] **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.*
|
||
- [x] **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`).*
|
||
- [x] **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.*
|
||
- [x] **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.*
|
||
- [x] **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.)*
|
||
- [x] **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.*
|
||
- [x] **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*). **Limitation: the queue isn't persisted, so a schedule only fires while
|
||
AeroFetch is running** — noted in the UI hint and the code.*
|
||
|
||
## 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.
|
||
|
||
- [x] **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.*
|
||
- [x] **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-item exact-format selection (needs probing each entry) left as a further
|
||
extension.*
|
||
- [x] **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.*
|
||
- [x] **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.*
|
||
- [x] **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.
|
||
|
||
- [x] **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.*
|
||
- [x] **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.*
|
||
- [x] **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.
|
||
*Deferred: a numeric/status badge needs a drawn `NativeImage` (no canvas in main); the
|
||
taskbar progress bar's `error` mode already signals failures.*
|
||
- [x] **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
|
||
|
||
The least glamorous, highest-leverage track: make sure what's *already shipped* actually works,
|
||
keep YouTube downloads working as the platform tightens, and remove first-run friction.
|
||
|
||
- [ ] **PO Token generation.** YTDLnis mints YouTube tokens in a WebView to dodge throttling
|
||
and "Sign in to confirm you're not a bot." This is becoming **required** for YouTube to
|
||
keep working at all. → reuse the existing persisted Electron `BrowserWindow`
|
||
(`persist:aerofetch-login` in `src/main/cookies.ts`) to host a token-minting script, or
|
||
support a PO-token provider, passed via `--extractor-args youtube:po_token=…`. A
|
||
longevity hedge more than a feature.
|
||
- [ ] **Verify the shipped-but-untested OS wiring** (flagged across both parity tracks as
|
||
"reasoning + typecheck only"). A single real NSIS-install smoke-test pass over: the
|
||
cookies sign-in window export · the `aerofetch://` protocol + Explorer "Send to" · the
|
||
scheduled `--sync` + RSS fast-check (Phase J) · aria2c · proxy. Converts a lot of
|
||
"probably works" into "verified."
|
||
- [ ] **Code signing.** The unsigned `.exe` triggers SmartScreen "Run anyway" — the single
|
||
biggest first-run friction for anyone you share it with. electron-builder `win` signing
|
||
(OV/EV cert); EV builds SmartScreen reputation fastest.
|
||
- [ ] **i18n / language setting.** Carried over from Phase E (deferred). YTDLnis and Seal both
|
||
ship many languages. Large but optional — needs an i18n library + string extraction, with
|
||
translations arriving incrementally.
|
||
|
||
---
|
||
|
||
## 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`,
|
||
`CommandTemplate`s, `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 A–K were.
|