ed1daf8bef
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>
47 lines
2.1 KiB
TypeScript
47 lines
2.1 KiB
TypeScript
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 (R1–R3).
|
||
// 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))
|
||
}
|