Files
AeroFetch/src/main/profiles.ts
T
debont80 ed1daf8bef 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>
2026-07-02 16:02:07 -04:00

47 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { MediaProfile, MediaKind, DownloadOptions } from '@shared/ipc'
import { isProfileLike } from './core/validation'
import { createJsonStore } from './jsonStore'
import { PROFILES_MAX } from './constants'
// Named download presets a Library Source can point at (PINCHFLAT Media
// Profiles). Plain JSON in userData, same shape/pattern as templates.ts —
// atomic writes / corruption backup / caching from the shared jsonStore (R1R3).
// A persisted row must at least be an object carrying an id (isProfileLike);
// everything else is coerced by sanitize(), so a hand-edited profiles.json can't
// inject malformed entries. (audit S5)
const store = createJsonStore('profiles.json', isProfileLike, PROFILES_MAX)
const KINDS: readonly MediaKind[] = ['video', 'audio']
/** Normalise a persisted/incoming profile so every field is well-formed. */
function sanitize(p: MediaProfile): MediaProfile {
const out: MediaProfile = {
id: String(p.id),
name: (p.name ?? '').trim() || 'Untitled profile'
}
if (p.kind && KINDS.includes(p.kind)) out.kind = p.kind
if (typeof p.quality === 'string' && p.quality.trim()) out.quality = p.quality.trim()
// options is stored whole when present; the download path re-sanitizes it
// through the settings schema before use, so a weak object check is enough here.
if (p.options && typeof p.options === 'object') out.options = p.options as DownloadOptions
if (typeof p.outputTemplate === 'string' && p.outputTemplate.trim()) {
out.outputTemplate = p.outputTemplate.trim()
}
if (typeof p.extraArgs === 'string' && p.extraArgs.trim()) out.extraArgs = p.extraArgs.trim()
return out
}
export function listProfiles(): MediaProfile[] {
return store.read().map(sanitize)
}
/** Add a new profile, or update an existing one (matched by id). */
export function saveProfile(profile: MediaProfile): MediaProfile[] {
const clean = sanitize(profile)
return store.write([clean, ...listProfiles().filter((p) => p.id !== clean.id)])
}
export function removeProfile(id: string): MediaProfile[] {
return store.write(listProfiles().filter((p) => p.id !== id))
}