// The plugin's configuration model — the single source of truth for the sync engine, editable by hand // (headless boxes) or by the web UI. Everything the engine needs is derivable from this file plus the // exporter's `punktfunk-library.json`; `resolveConfig` fills defaults so the rest of the code works // against a fully-populated shape. /** When and how often we re-read the export and reconcile. */ export interface SyncOptions { /** Interval poll in minutes — the backstop under the exporter-driven file watch. */ pollMinutes: number; /** Watch the exporter's output directory and reconcile on change (the primary trigger). */ watch: boolean; /** Debounce window for coalescing watch/poll-triggered re-reads (ms). */ debounceMs: number; } /** Which Playnite games become library entries. */ export interface FilterOptions { /** Only sync games Playnite marks installed (default). Off = include not-yet-installed titles too. */ installedOnly: boolean; /** Include games flagged Hidden in Playnite (default off). */ includeHidden: boolean; /** If non-empty, keep ONLY games whose source is in this list ("Steam", "GOG", …). */ sources: string[]; /** Drop games whose source is in this list (applied after `sources`). */ excludeSources: string[]; } /** How cover art reaches the clients. Playnite art is local files on the host, and the host only * proxies Steam art — so for a provider entry we inline the cover as a size-capped `data:` URL that * clients render directly. `off` syncs titles with no art. */ export type ArtMode = "dataurl" | "off"; export interface ArtOptions { mode: ArtMode; /** Skip an image whose file is larger than this (bytes) — a `data:` URL bloats the library payload, * so covers over the cap are dropped rather than inlined. */ maxBytes: number; /** Also inline the Playnite background as `hero` art (usually large — off by default). */ includeBackground: boolean; } export interface UiOptions { /** Fallback standalone mode: serve the UI on a fixed port behind a password instead of via the * console proxy. For host-only installs without the console. */ standalone: boolean; /** Standalone bind port. */ port: number; /** Standalone bind address (defaults to loopback; a non-loopback bind REQUIRES a password). */ bind: string; /** scrypt password hash (`scrypt$$`), required for a non-loopback standalone bind. */ passwordHash?: string; } /** The on-disk config as authored (all optional — `resolveConfig` fills the rest). */ export interface RawConfig { /** Override the auto-detected Playnite data directory (`%APPDATA%\Playnite`). Point it at a * portable install, or at the interactive user's profile when the runner runs as a different user. */ playniteDir?: string; sync?: Partial; filter?: Partial; art?: Partial; ui?: Partial; /** Dev/acceptance flag: reconcile one hardcoded entry so the round-trip is observable. */ devEntry?: boolean; } /** The fully-resolved config the engine consumes. */ export interface Config { playniteDir?: string; sync: SyncOptions; filter: FilterOptions; art: ArtOptions; ui: UiOptions; devEntry: boolean; } export const DEFAULT_SYNC: SyncOptions = { pollMinutes: 5, watch: true, debounceMs: 1500, }; export const DEFAULT_FILTER: FilterOptions = { installedOnly: true, includeHidden: false, sources: [], excludeSources: [], }; export const DEFAULT_ART: ArtOptions = { mode: "dataurl", maxBytes: 1_500_000, includeBackground: false, }; export const DEFAULT_UI: UiOptions = { standalone: false, port: 47994, bind: "127.0.0.1", }; /** Fill defaults over an authored config so the rest of the engine sees a total shape. */ export const resolveConfig = (raw: RawConfig): Config => ({ playniteDir: raw.playniteDir?.trim() || undefined, sync: { ...DEFAULT_SYNC, ...raw.sync }, filter: { ...DEFAULT_FILTER, ...raw.filter }, art: { ...DEFAULT_ART, ...raw.art }, ui: { ...DEFAULT_UI, ...raw.ui }, devEntry: raw.devEntry ?? false, }); /** The empty starting config for a fresh install. */ export const emptyConfig = (): Config => resolveConfig({});