docs: remove completed roadmap + release-gate docs

The ROADMAP files are fully shipped/retrospective and the smoke-test and
signing guides are retired at the maintainer's request. resources/bin/README.md
is kept (documents bundled-binary setup + license obligations).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 23:13:57 -04:00
parent 6cf6c0ead6
commit ff9077989f
4 changed files with 0 additions and 863 deletions
-266
View File
@@ -1,266 +0,0 @@
# AeroFetch Roadmap — media-manager mode (Pinchflat parity)
The [Seal-parity roadmap](ROADMAP.md) took AeroFetch from "paste a URL → download" to a
full-featured yt-dlp frontend (Phases AE, complete). This document is a **second, parallel
track**: turning AeroFetch from a one-shot *downloader* into a *media manager* in the spirit
of [Pinchflat](https://github.com/kieraneglin/pinchflat) — download **entire channels**,
keep them **organized into `Channel / Playlist / Title` folders**, and (eventually) **keep
them in sync** as the channel posts new videos.
Nothing here removes or replaces existing behaviour. The single-video / hand-picked-playlist
flow (probe → format pick → queue → history) stays exactly as-is and remains the default. The
media-manager features are **additive**: a new "Library" section feeds the *same* download
queue, concurrency cap, history, error log, cookies, and post-processing options already built.
Sources: [Pinchflat GitHub](https://github.com/kieraneglin/pinchflat) ·
[Pinchflat architecture (DeepWiki)](https://deepwiki.com/kieraneglin/pinchflat) ·
[Pinchflat FAQ — indexing](https://github.com/kieraneglin/pinchflat/wiki/Frequently-Asked-Questions).
> **See also:** [ROADMAP.md](ROADMAP.md) → **Post-parity** — YTDLnis-derived editing features,
> Windows-native polish, and reliability work, Phases L onward.
---
## Implementation status — Phases FK shipped (2026-06-23)
All six phases are implemented and **rebuild-gated** (`npm run typecheck` + `npm run test`
+ `npm run build` clean after each). The unit suite grew from 76 → **106 tests** (new pure
logic in `indexerCore.ts` + the folder/sidecar args). UI phases were additionally verified
in the Vite browser preview.
| Phase | What shipped | Verification |
| --- | --- | --- |
| **F** | `classifySource`/`buildMediaItems`/`stableSourceId` (`indexerCore.ts`); channel-walk + persist (`indexer.ts`, `sources.ts`); IPC + preload | typecheck · 88 tests · build |
| **G** | `sanitizeDirSegment` + `collectionOutputTemplate` (`buildArgs.ts`); `CollectionContext` threaded through `buildCommand` | typecheck · 97 tests · build |
| **H** | Library sidebar tab + `LibraryView.tsx` + `store/sources.ts`; grouped tree, "Download N pending" → existing queue, live status pills | **preview-verified** |
| **I** | `mergeItemsPreservingState` (re-index keeps downloaded state, drops vanished, counts new); persist-on-complete via `markDownloaded` | **preview-verified** (state survives a queue clear) |
| **J** | Watched sources + RSS fast-check (`sync.ts`), per-source Watch toggle, "Check for new", `autoDownloadNew`; Task Scheduler (`schedule.ts`) + `--sync` launch | UI preview-verified; **OS-level scheduling/RSS fenced — needs a real-install smoke test** (same caveat as the `aerofetch://` wiring in ROADMAP.md) |
| **K** | `.info.json` / thumbnail / `.description` sidecars via `DownloadOptions` + `DownloadOptionsForm` "Sidecar files" group | typecheck · 106 tests · build + preview-verified |
**Deferred sub-items (noted, not yet built):** the power-user raw output-path template
setting (G bullet 3 — current layout is the fixed `Channel/Playlist/NNN - Title`); an
automatic incremental feeder so the live queue self-refills (H — today "Download pending"
enqueues a capped batch of `MAX_ENQUEUE_BATCH`); the retention / quality-upgrade re-download
(I stretch); a true headless quit-when-done for the scheduled `--sync` run (J — today it
launches the normal window unobtrusively and syncs on startup); and Kodi/Jellyfin `.nfo`
generation (K — the three native yt-dlp sidecars ship; `.nfo` needs a custom post-step).
---
## The core idea borrowed from Pinchflat
Pinchflat's insight is to **separate indexing from downloading**, with a persisted index in
between:
1. **Index** a *Source* (a channel or playlist) — enumerate *all* its videos with
`yt-dlp --flat-playlist` and persist one lightweight record per video, **regardless of any
filters**. Changing what you want to download never forces a re-index.
2. **Filter** that persisted list to decide what to actually fetch.
3. **Download** each chosen video as its own per-item yt-dlp run, placed into folders by an
output-path template, with `--download-archive` + a per-item `downloaded` flag preventing
re-downloads.
This is why "download an entire channel" doesn't have to flood the queue with thousands of
live cards (the failure mode of naïve per-video enumeration) and doesn't need a custom
playlist-progress parser (the failure mode of native `yt-dlp` playlist expansion in one
process): **the channel's full video list lives in a store; the live queue only holds what's
actively downloading.** AeroFetch's per-video progress, concurrency, cancel, retry, and
history all keep working unchanged.
### Data model — Pinchflat → AeroFetch
| Pinchflat (Elixir + SQLite + Oban) | AeroFetch (Electron + JSON store + existing queue) |
| --- | --- |
| **Source** (watched channel/playlist) | `Source` record in a new `sources.json` (mirrors `history.ts`) |
| **Media Item** (one discovered video) | `MediaItem` record (id, title, playlist, index, source id, `downloaded`) |
| **Media Profile** (download preset) | Maps onto existing `DownloadOptions` + `CommandTemplate` + a new output-path template |
| Oban `media_collection_indexing` queue | An async **index job** in main (the probe, persisted) |
| Oban `media_fetching` queue | The existing renderer download queue + concurrency cap |
| Oban `fast_indexing` (RSS) | Phase J — YouTube RSS re-sync |
| `--download-archive` + item state | The existing `--download-archive` setting + `MediaItem.downloaded` |
**Storage decision:** stay with plain-JSON stores (the pattern Phase D deliberately kept over
better-sqlite3 to avoid a native-module build step). A single channel is up to a few thousand
`MediaItem`s — still fine to hold and filter client-side. Revisit better-sqlite3 only if a
user indexes many large channels and the JSON files get unwieldy (noted as a Phase H risk).
---
## Phase F — Channel & playlist indexing (foundation) ✅ COMPLETE
Make AeroFetch able to *enumerate and remember* a whole channel without downloading anything
yet. This is the prerequisite for every later phase.
- [x] **Channel-depth probe.** Extend `src/main/probe.ts` so a channel URL (`/@handle`,
`/channel/<id>`, `/c/<name>`, `/user/<name>`) resolves to a new `kind: 'channel'`
`ProbeResult`. Today `buildPlaylist` reads `data.entries` one level deep and ignores the
nesting a channel returns (the channel's tabs/playlists). Walk channel → playlists →
videos: probe `…/playlists` (flat) for the playlist list, plus a synthetic **"Uploads"**
playlist from `…/videos` for videos that belong to no playlist. Lazy-probe each
playlist's video list on demand rather than all up front.
- [x] **Persisted index.** New `src/main/sources.ts` (plain JSON, mirrors `src/main/history.ts`)
storing `Source` + `MediaItem` records. New IPC types in `src/shared/ipc.ts`:
```ts
interface Source {
id: string
url: string
kind: 'channel' | 'playlist'
title: string
channel?: string
addedAt: number
lastIndexedAt?: number
itemCount: number // cached MediaItem count for list display
watched?: boolean // included in scheduled / startup sync (Phase J)
feedUrl?: string // YouTube RSS feed for cheap "anything new?" checks
}
interface MediaItem {
id: string // globally unique, `${sourceId}:${videoId}`
sourceId: string
videoId: string // yt-dlp video id — the dedup key within a source
title: string
url: string
playlistTitle: string // for the folder path; 'Uploads' fallback
playlistIndex: number // 1-based, for NNN numbering
durationLabel?: string
downloaded: boolean
downloadedAt?: number
filePath?: string
}
```
*(This block reflects the shipped `src/shared/ipc.ts` shape; the original sketch
predated the `videoId`/`itemCount`/`watched`/`feedUrl` fields.)*
- [x] **Dedup on index.** A video appearing in multiple playlists collapses to one `MediaItem`
(keyed by video id); first playlist seen wins the folder assignment (surface this choice
in the UI so the user can reassign). The "Uploads" synthetic playlist catches anything in
no real playlist.
- [x] **Index job.** Async indexing in main (not blocking the UI), pushing progress over a new
IPC channel (`index:progress`) the way `download.ts` pushes download events — "indexed
N of M playlists." Reuse `cleanError` and the same `assertHttpUrl` guard.
## Phase G — Folder organization & output-path templates ✅ COMPLETE
Turn a `MediaItem` into a real per-item download that lands in `Channel / Playlist / Title`.
- [x] **Per-item output subpath.** `StartDownloadOptions.outputDir` is *already* plumbed through
`buildCommand` (`src/main/download.ts`). Add an `outputSubdir` (or reuse `outputDir` with a
joined subpath) so each `MediaItem` downloads into
`<root>/<Channel>/<Playlist>/<NNN> - <Title>.ext`. The `NNN` index comes from
`MediaItem.playlistIndex` (computed at index time) — **not** `%(playlist_index)s`, which is
empty under the per-item `--no-playlist` path that AeroFetch keeps using.
- [x] **Directory sanitizer.** A small helper in `buildArgs.ts` that strips Windows-illegal
directory chars (`< > : " / \ | ? *`, trailing dots/spaces, reserved names like `CON`).
Critical because `--restrict-filenames` only sanitizes the *filename* yt-dlp generates,
not the directory segments AeroFetch constructs from channel/playlist names.
- [x] **Output-path setting.** A new Settings → Downloads field for the folder layout, defaulting
to `Channel / Playlist / Title`, with the raw yt-dlp `-o` template exposed for power users
(parallels how Phase C exposed raw extra args). Keeps the existing flat
`filenameTemplate` as the default for non-collection downloads. *Implemented (Batch 26):
`Settings.collectionOutputTemplate` — blank = the built-in `Channel/Playlist/NNN - Title`
layout; a raw yt-dlp `-o` template overrides it for collection downloads.
`collectionOutputTemplate()` (buildArgs) takes the optional custom template; the zod schema
validates it as a safe relative template; surfaced as the "Collection folder layout" field in
Settings → Downloads → Filenames. A Media Profile can override it per-source (Batch 27).*
- [x] **"Media Profile" = named download preset.** Pinchflat bundles quality + subs + output
template into a reusable Profile. Implemented: a `MediaProfile` = named bundle of
`{ kind, quality, options, outputTemplate, extraArgs }` a Source points at via `Source.profileId`.
Backend mirrors the `CommandTemplate` precedent — `src/main/profiles.ts` on `createJsonStore`
(`profiles.json`, `PROFILES_MAX` cap, `isProfileLike` guard + `sanitize()`), CRUD IPC
(`profiles:list/save/remove`) + preload + a `useProfiles` renderer store. Resolution is the pure
`resolveProfile` in `@shared/profileResolve.ts` (profile → global default, field by field; unit-tested
in `test/profileResolve.test.ts`), applied in the sources store's `enqueueItems` so a source's
downloads follow its profile. UI: a `ProfileManager` (mirrors `TemplateManager`) in a new Settings →
Advanced → **Media profiles** card, plus a per-source **Profile** picker on the Library source detail.
Live-verified both surfaces render in the preview.
## Phase H — Library view (the media-manager UI) ✅ COMPLETE
The new surface where channels live. Everything below feeds the **existing** download queue.
- [x] **"Library" sidebar section** (`src/renderer/src/components/Sidebar.tsx`) alongside
Downloads / History / Settings. Lists added Sources.
- [x] **Source detail — playlist/video tree.** A collapsible `Channel → Playlist → Video` tree
showing each `MediaItem`'s state (indexed · pending · downloading · downloaded · error),
reusing the checkbox-selection pattern already in `DownloadBar.tsx`'s playlist panel.
Per-playlist select-all, live counts.
- [x] **"Download all pending" → existing queue, batched.** Enqueue selected `MediaItem`s via
the existing `addFromUrl` path, **a batch at a time** (e.g. 50) so the queue/store never
holds the entire channel at once — the live queue stays small while the Library view is the
source of truth for the full list. Completion flips `MediaItem.downloaded = true` and
records to history exactly as today.
- [x] **New `useSources` store** (`src/renderer/src/store/sources.ts`, mirrors
`store/downloads.ts` / `store/history.ts`) plus its IPC bridge. Browser-preview seed data
so the view is demoable without Electron (same convention as `downloads.ts`'s `PREVIEW`).
- [x] **Risk to watch:** rendering a 2,000-row tree. Virtualize the list (or cap + paginate) and,
if JSON-store load times bite, revisit the Phase-D better-sqlite3 decision.
## Phase I — Incremental sync & dedup ✅ COMPLETE
Re-running a channel should grab only what's new — the everyday media-manager loop.
- [x] **Re-index = diff.** Re-indexing a Source compares freshly enumerated video ids against the
persisted `MediaItem`s and marks only the new ones; existing records (and their
`downloaded` state) are preserved. "X new since last sync" badge.
- [x] **"Download new only."** One action that enqueues just the un-downloaded `MediaItem`s,
backed by the existing **`--download-archive`** setting (`Settings.downloadArchive`,
`getDownloadArchivePath()`) as a second, yt-dlp-level dedup guard so even a stale index
can't re-download.
- [x] **Retention / quality-upgrade (stretch, from Pinchflat).** Optional re-download to upgrade
quality/metadata, and optional pruning of old items. Implemented as an on-demand per-source
maintenance panel (`SourceRetention` on the Library source detail), not a persisted schedule
(matching the "niche for a desktop app" scope). **Prune:** keep the newest N downloaded files,
delete the older ones' files (the download archive stops them silently re-downloading); shows a
dry-run count and a two-step confirm before any deletion. **Upgrade:** re-enqueue items whose
recorded `downloadedQuality` ranks below the source's current target (its profile's quality or
the global default). The selection logic is pure + injected-now (`@shared/retention.ts`:
`selectPruneCandidates`/`selectUpgradeCandidates`, `qualityRank` in `@shared/ipc`), unit-tested
in `test/retention.test.ts`; the impure prune (`pruneMediaItems` in `src/main/sources.ts`)
deletes files best-effort and clears `downloaded`. `MediaItem.downloadedQuality` is now recorded
through the completion path. Live-verified the panel renders on an expanded source.
## Phase J — Subscriptions & scheduled auto-download ✅ COMPLETE (OS wiring needs smoke test)
Pinchflat's headline feature: a Source you *watch*, that downloads new uploads on its own.
AeroFetch already has the Windows pieces for this elsewhere in this workspace (Task Scheduler +
single-instance Mutex + a headless run mode — see the DashMail/Weather Radar patterns).
- [x] **Watched sources.** A `Source.watched` flag + an "auto-download new" toggle per source.
- [x] **Fast indexing via RSS.** YouTube exposes a per-channel Atom feed
(`https://www.youtube.com/feeds/videos.xml?channel_id=<id>`) — a cheap check for new
uploads without a full yt-dlp scan. **Caveat:** the feed only carries the latest ~15
videos, so RSS is for *staying current*, not initial indexing (which stays a full
Phase-F scan). Pinchflat draws the same line ("fast indexing is not recommended for
playlists / initial scans").
- [x] **Scheduled headless run.** A `--sync` CLI entry that indexes all watched sources and
enqueues new items, wired to **Windows Task Scheduler** (reuse the
single-instance-lock already added in Phase E so a scheduled run hands off to a running
window instead of double-launching). Notify on new downloads via the existing
`Notification` path.
## Phase K — Media-server output (optional stretch) ✅ COMPLETE
Make AeroFetch's output drop-in for Jellyfin/Plex/Kodi the way Pinchflat does — relevant
because organized channel folders are exactly what those servers ingest.
- [x] **Sidecar metadata.** `--write-info-json`, `--write-thumbnail`, `--write-description`,
and optional Kodi/Jellyfin `.nfo` generation. Most of this is already expressible through
Phase C custom-command args; a checkbox group in the output settings makes it first-class.
---
## What we reuse vs. build
**Reuse unchanged:** the download queue + concurrency cap + cancel/retry (`store/downloads.ts`),
history + error log, cookies + proxy + rate-limit + aria2c (`AccessOptions`), all
post-processing `DownloadOptions`, `--download-archive`, native notifications, the
single-instance lock, and the JSON-store pattern.
**Build new:** channel-depth probing (Phase F), the `sources.json` index + `Source`/`MediaItem`
model (F), per-item folder output paths + sanitizer (G), the Library view + `useSources` store
(H), incremental diff-sync (I), and RSS + scheduled sync (J).
## Suggested order
F → G are the foundation and unlock a usable "download whole channel into folders" the moment
they land (drive it from a temporary button before the full Library UI exists). H makes it a
real product surface. I and J turn it into a true media manager. K is polish. Phases FH are
the high-value core; IK are incremental and each shippable on its own.
-459
View File
@@ -1,459 +0,0 @@
# 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. (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](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` shipped in Phase L.)
- [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 (surfaced).** `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`. **Now surfaced:** the
DownloadBar's **"Show command"** toggle (`useDownloadBar.toggleCommandPreview`
`window.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.)
- [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 (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.
- [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 — 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.
- [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).
---
# Post-parity — editing, native polish & reliability
With both parity tracks complete — Seal (Phases AE above) and the
[Pinchflat media-manager roadmap](ROADMAP-PINCHFLAT.md) (Phases FK ✅) — 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
(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](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.*
- [x] **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.
- [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*). 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.
- [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).*
- [x] **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 `addMany``formatId` 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.*
- [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.*
- [x] **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`.
- [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 ✅ (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.
- [x] **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.
- [x] **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](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.**
- [x] **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](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`,
`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 AK were.
-77
View File
@@ -1,77 +0,0 @@
# AeroFetch — Windows code signing
Unsigned builds run, but Windows SmartScreen shows a blue **"Windows protected your PC"**
prompt the first time each user runs the installer (they must click *More info → Run anyway*).
This is the single biggest first-run friction when sharing AeroFetch. Code signing removes it
(EV certificates remove it immediately; OV certificates build reputation over time/installs).
**This step needs a certificate that can't live in the repo**, so it isn't wired on by default.
The build is otherwise ready for it — electron-builder picks the cert up from environment
variables with no `electron-builder.yml` changes required.
## What you need
- An **Authenticode code-signing certificate** for Windows:
- **OV (Organization Validation)** — cheaper; SmartScreen reputation accrues as more users
install, so early downloads still see the prompt.
- **EV (Extended Validation)** — pricier, usually on a hardware token / cloud HSM; clears
SmartScreen immediately. Token-based EV can't be fed as a plain `.pfx` (see "HSM/EV" below).
- The cert as a password-protected `.pfx`/`.p12` (for OV), or an HSM/cloud-signing setup (EV).
## Signing an OV `.pfx` locally
electron-builder reads these env vars automatically:
```bash
# PowerShell
$env:CSC_LINK = "C:\path\to\codesign.pfx" # or a base64 string of the .pfx
$env:CSC_KEY_PASSWORD = "<pfx password>"
npm run build:win
```
```bash
# bash / CI
export CSC_LINK="$(base64 -w0 codesign.pfx)" # base64 is convenient for CI secrets
export CSC_KEY_PASSWORD="$PFX_PASSWORD"
npm run build:win
```
electron-builder then signs `AeroFetch Setup <ver>.exe`, the portable `.exe`, and the app
binary. No changes to `electron-builder.yml` are needed; if `CSC_LINK` is unset, the build
proceeds **unsigned** (current behavior).
## CI (GitHub Actions sketch)
Store the cert + password as encrypted secrets and export them before the build:
```yaml
- name: Build (signed)
env:
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} # base64 of the .pfx
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
run: npm run build:win
```
## HSM / EV certificates
EV certs (and many newer OV certs) ship on a hardware token or cloud HSM and can't be exported
to a `.pfx`. Sign via a custom signing hook instead — set `win.sign` in `electron-builder.yml`
to a script that invokes your provider's tool (Azure Trusted Signing, DigiCert KeyLocker,
SSL.com eSigner, or `signtool` with the token). Each provider documents the exact `signtool`
invocation; electron-builder calls your hook once per artifact with the file path.
## Verifying a signature
```powershell
Get-AuthenticodeSignature ".\dist\AeroFetch Setup <ver>.exe" | Format-List
# Status should be 'Valid'; SignerCertificate should be your cert.
signtool verify /pa /v ".\dist\AeroFetch Setup <ver>.exe"
```
## Notes
- The LGPL ffmpeg + yt-dlp binaries shipped in `resources/bin` are third-party; signing the
AeroFetch artifacts doesn't (and needn't) re-sign them.
- Timestamp the signature (electron-builder does by default) so it stays valid after the cert
expires.
- Signing does **not** replace the smoke test — see [SMOKE-TEST.md](SMOKE-TEST.md).
-61
View File
@@ -1,61 +0,0 @@
# AeroFetch — manual smoke-test checklist
Some features can only be exercised against a **real install** (or a real yt-dlp run) —
they're main-process / Electron / OS-integration code that the Vite browser preview and the
unit suite can't reach. They're implemented and typecheck/test/build clean, but the roadmaps
flag them as needing a hands-on pass. Run this checklist after building
(`npm run build:win`) and installing the NSIS artifact (or launching the portable `.exe`).
Mark each ✅/❌ and note anything surprising.
## Downloads & post-processing (Phases A, L, M)
- [ ] **Basic download** — paste a URL, Fetch, pick a format, Download → file lands in
Documents\Video (or your folder), progress + speed update, "Completed".
- [ ] **Trim / cut** (Phase L) — set a section like `0:10-0:20`, download → output is ~10s and
starts/ends near the cut points (`--download-sections` + `--force-keyframes-at-cuts`).
- [ ] **Split by chapters** (Phase L) — on a video with chapters, enable it → per-chapter files
appear alongside the full file.
- [ ] **Pause / resume** (Phase M) — start a large download, Pause (process stops, `.part`
stays), Resume → it **continues** from where it stopped, not from 0% (yt-dlp `--continue`).
- [ ] **Scheduling** (Phase M) — schedule a download a couple of minutes out → it sits "Saved /
Scheduled", then auto-starts at the time (only while the app is running).
## Access & networking (Phase B, P)
- [ ] **Cookies — sign-in window** — Settings → Cookies → "Sign-in window", log into a site,
close it → "cookies saved" with a timestamp; a members-only/age-gated video then downloads.
- [ ] **Cookies — from browser** — pick an installed browser → a gated video downloads.
- [ ] **Proxy** — set a working proxy → downloads route through it (verify via the proxy logs).
- [ ] **aria2c** — drop `aria2c.exe` into `resources/bin`, enable it → multi-connection download
works; remove it → silently falls back to yt-dlp's downloader.
- [ ] **YouTube client / PO token** (Phase P) — set `youtubePlayerClient` (e.g. `web_safari`) →
the run includes `--extractor-args youtube:player_client=web_safari` (check Terminal/Preview).
## OS integration (Phase E, J, O)
- [ ] **`aerofetch://` protocol** — from a browser, open `aerofetch://download?url=<encoded>`
AeroFetch focuses and the URL appears in the "Link received" banner.
- [ ] **Explorer "Send to AeroFetch"** — drag a tab to the desktop to make a `.url`, right-click →
Send to → AeroFetch → the link is received.
- [ ] **Scheduled sync** (Phase J) — Settings → enable the daily sync → a Windows Task Scheduler
task exists; run it (or `AeroFetch.exe --sync`) → watched sources index and new items enqueue.
- [ ] **RSS fast-check** (Phase J) — "Check for new" on a watched channel → newly-posted videos
show as new without a full re-index.
- [ ] **System tray** (Phase O) — enable "Keep running in the tray", close the window → it hides
to the tray; tray click reopens; tray → Quit actually exits.
- [ ] **Taskbar progress** (Phase O) — during downloads the taskbar icon shows a progress bar;
it turns red if one fails and clears when the queue goes idle.
- [ ] **Jump list** (Phase O) — right-click the taskbar icon → "Open AeroFetch" focuses the app.
## Terminal (Phase N)
- [ ] Enable "Run custom commands", open Terminal, run `--version` → the version prints; run
`-F <url>` → the format table streams in; Stop cancels a long run.
---
If anything here fails, capture the Settings → Diagnostics "Copy full report" output and the
exact steps. These items have no automated coverage, so this checklist is the release gate for
them. See also [SIGNING.md](SIGNING.md) for the code-signing step that removes the SmartScreen
prompt for end users.