From ff9077989fbf76b2efc9e381daa5adf7200cca37 Mon Sep 17 00:00:00 2001 From: debont80 Date: Thu, 2 Jul 2026 23:13:57 -0400 Subject: [PATCH] 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 --- ROADMAP-PINCHFLAT.md | 266 ------------------------- ROADMAP.md | 459 ------------------------------------------- docs/SIGNING.md | 77 -------- docs/SMOKE-TEST.md | 61 ------ 4 files changed, 863 deletions(-) delete mode 100644 ROADMAP-PINCHFLAT.md delete mode 100644 ROADMAP.md delete mode 100644 docs/SIGNING.md delete mode 100644 docs/SMOKE-TEST.md diff --git a/ROADMAP-PINCHFLAT.md b/ROADMAP-PINCHFLAT.md deleted file mode 100644 index 4f60734..0000000 --- a/ROADMAP-PINCHFLAT.md +++ /dev/null @@ -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 A–E, 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 F–K 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/`, `/c/`, `/user/`) 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 - `/// - .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 F–H are -the high-value core; I–K are incremental and each shippable on its own. diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index da5cbe3..0000000 --- a/ROADMAP.md +++ /dev/null @@ -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 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.* -- [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 A–K were. diff --git a/docs/SIGNING.md b/docs/SIGNING.md deleted file mode 100644 index dcdfdf2..0000000 --- a/docs/SIGNING.md +++ /dev/null @@ -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). diff --git a/docs/SMOKE-TEST.md b/docs/SMOKE-TEST.md deleted file mode 100644 index d2b9e58..0000000 --- a/docs/SMOKE-TEST.md +++ /dev/null @@ -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.