Files
AeroFetch/src/main/settingsOptions.ts
T
debont80 8f6b2a761b feat(audit): Batch 24 — one validate-and-coerce schema layer for settings (CC9)
zod field schemas in settingsSchema.ts replace setSettings' bespoke
per-key switch: parse → skip-on-fail → store, side effects (secret
encryption, launch-at-startup sync) kept in settings.ts. sanitizeOptions
extracted to settingsOptions.ts to break the import cycle; backup import
flows through the same schema. Field-by-field parity pinned in
test/settingsSchema.test.ts (15 tests); full suite unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:41:00 -04:00

63 lines
2.9 KiB
TypeScript

import {
AUDIO_FORMATS,
VIDEO_CONTAINERS,
VIDEO_CODECS,
SPONSORBLOCK_CATEGORIES,
DEFAULT_DOWNLOAD_OPTIONS,
type DownloadOptions,
type SponsorBlockCategory,
type AudioFormat,
type VideoContainer,
type VideoCodecPref
} from '@shared/ipc'
// Coerce an untrusted partial into a valid DownloadOptions, falling back to the
// defaults for any missing/invalid field. Used both to migrate older settings
// files (which predate downloadOptions) and to validate renderer writes. Lives
// in its own module (not settings.ts) so the settings field schemas (CC9) can
// reuse it without an import cycle.
export function sanitizeOptions(input: unknown): DownloadOptions {
const o = (input && typeof input === 'object' ? input : {}) as Partial<DownloadOptions>
const d = DEFAULT_DOWNLOAD_OPTIONS
const bool = (v: unknown, fallback: boolean): boolean => (typeof v === 'boolean' ? v : fallback)
const cats = Array.isArray(o.sponsorBlockCategories)
? (o.sponsorBlockCategories.filter((c) =>
(SPONSORBLOCK_CATEGORIES as readonly string[]).includes(c)
) as SponsorBlockCategory[])
: d.sponsorBlockCategories
return {
audioFormat: (AUDIO_FORMATS as readonly string[]).includes(o.audioFormat as string)
? (o.audioFormat as AudioFormat)
: d.audioFormat,
videoContainer: (VIDEO_CONTAINERS as readonly string[]).includes(o.videoContainer as string)
? (o.videoContainer as VideoContainer)
: d.videoContainer,
preferredVideoCodec: (VIDEO_CODECS as readonly string[]).includes(
o.preferredVideoCodec as string
)
? (o.preferredVideoCodec as VideoCodecPref)
: d.preferredVideoCodec,
formatSort: typeof o.formatSort === 'string' ? o.formatSort.trim() : d.formatSort,
embedSubtitles: bool(o.embedSubtitles, d.embedSubtitles),
subtitleLanguages:
typeof o.subtitleLanguages === 'string' && o.subtitleLanguages.trim()
? o.subtitleLanguages.trim()
: d.subtitleLanguages,
autoSubtitles: bool(o.autoSubtitles, d.autoSubtitles),
sponsorBlock: bool(o.sponsorBlock, d.sponsorBlock),
sponsorBlockMode: o.sponsorBlockMode === 'mark' ? 'mark' : 'remove',
sponsorBlockCategories: cats,
embedChapters: bool(o.embedChapters, d.embedChapters),
splitChapters: bool(o.splitChapters, d.splitChapters),
embedMetadata: bool(o.embedMetadata, d.embedMetadata),
metadataTitle: typeof o.metadataTitle === 'string' ? o.metadataTitle : undefined,
metadataArtist: typeof o.metadataArtist === 'string' ? o.metadataArtist : undefined,
metadataAlbum: typeof o.metadataAlbum === 'string' ? o.metadataAlbum : undefined,
embedThumbnail: bool(o.embedThumbnail, d.embedThumbnail),
cropThumbnail: bool(o.cropThumbnail, d.cropThumbnail),
writeInfoJson: bool(o.writeInfoJson, d.writeInfoJson),
writeThumbnailFile: bool(o.writeThumbnailFile, d.writeThumbnailFile),
writeDescription: bool(o.writeDescription, d.writeDescription)
}
}