feat(pinchflat): Media Profiles — named download presets per Library source

A MediaProfile bundles { kind, quality, options, outputTemplate, extraArgs }
that a Source points at via Source.profileId, overriding the global defaults
field-by-field for that source's downloads.

Backend mirrors CommandTemplate: profiles.ts (createJsonStore, PROFILES_MAX,
isProfileLike + sanitize), CRUD IPC (profiles:list/save/remove) + preload +
useProfiles store; Source.profileId + sources:set-profile. Pure resolution
in @shared/profileResolve.ts (profile → global, per field; unit-tested).
Applied in the sources-store enqueueItems; a per-download collectionOutput-
Template override threads to buildArgs so a profile's outputTemplate wins.

UI: ProfileManager (mirrors TemplateManager) in a new Settings → Advanced →
Media profiles card, plus a per-source Profile picker in the Library source
detail. Both live-verified in the preview; 354 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:02:07 -04:00
parent a02953f6d1
commit ed1daf8bef
21 changed files with 685 additions and 14 deletions
+11 -5
View File
@@ -155,11 +155,17 @@ Turn a `MediaItem` into a real per-item download that lands in `Channel / Playli
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.
- [ ] **"Media Profile" = named download preset (optional sugar).** Pinchflat bundles
quality + subs + output template into a reusable Profile. AeroFetch already persists
`DownloadOptions` and named `CommandTemplate`s; a Profile is just a named bundle of
`{ DownloadOptions, outputTemplate, extraArgs }` a Source can point at. Worth it once
multiple sources want different rules; skip for v1 (use the global defaults).
- [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