Files
AeroFetch/ROADMAP.md
T
debont80 47da3e6746 Add Pinchflat-style media manager: index channels into playlist folders
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>
2026-06-23 20:50:13 -04:00

237 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## 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
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.
- [ ] **i18n / language setting** (Seal ships ~40 languages). Large but optional — deferred,
since shipping ~40 real translations isn't something to fake; doing this properly means
wiring up an i18n library + extracting every string now, with translation content
arriving incrementally from contributors.
- [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 AC'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).