// The CROSS-PROCESS contract: the other half of this file is C#. // // The Playnite exporter (`exporter/`, a GenericPlugin) writes `punktfunk-library.json` in // exactly this shape on every library change; this plugin decodes it. Keep it in lockstep // with `exporter/Model.cs` — the `[SerializationPropertyName]` attributes there pin the // JSON keys these Schemas read. Bump `SCHEMA` on any breaking change; the exporter stamps // the version it wrote and the `schema` check below turns a too-new file into a decode // error rather than a silent mis-read. // // Leniency is deliberate and asymmetric: every field except `id`, `schema` and `games` has // a decoding default, so an exporter that predates (or postdates) a field addition still // decodes. Per-GAME validity — a malformed Guid, a blank title — is NOT a decode failure: // those become skip reasons in the pure core, so one bad row can never cost you the whole // library. import { Effect, Schema } from "effect"; /** The file the exporter writes (ingest inbox, and its own `ExtensionsData//`). */ export const EXPORT_FILE = "punktfunk-library.json"; /** Schema version this reader understands. A newer major is refused (see `LibraryExport`). */ export const SCHEMA = 1; const withDefault = (schema: S, value: S["Encoded"]) => schema.pipe( Schema.withDecodingDefaultKey(Effect.succeed(value), { encodingStrategy: "omit", }), ); /** One game as the Playnite exporter sees it. Art fields are ABSOLUTE local paths on the * host box (resolved via `IPlayniteAPI.Database.GetFullFilePath`) — except a remote * `http(s)` art URL, which the exporter passes through verbatim. */ export const ExportedGame = Schema.Struct({ /** Playnite's stable game `Guid` ("d" format) — our reconcile `external_id`. */ id: Schema.String, name: withDefault(Schema.String, ""), /** Whether Playnite considers the game installed (drives the `installedOnly` filter). */ installed: withDefault(Schema.Boolean, false), /** Playnite's per-game "Hidden" flag. */ hidden: withDefault(Schema.Boolean, false), /** The library source name ("Steam", "GOG", "Epic", emulator name, …) or null. */ source: withDefault(Schema.NullOr(Schema.String), null), /** Platform display names ("PC (Windows)", "Nintendo Switch", …). */ platforms: withDefault(Schema.Array(Schema.String), []), cover: withDefault(Schema.NullOr(Schema.String), null), background: withDefault(Schema.NullOr(Schema.String), null), icon: withDefault(Schema.NullOr(Schema.String), null), }); export type ExportedGame = typeof ExportedGame.Type; export const PlayniteInfo = Schema.Struct({ version: withDefault(Schema.NullOr(Schema.String), null), /** "Desktop" | "Fullscreen" — informational. */ mode: withDefault(Schema.NullOr(Schema.String), null), }); export type PlayniteInfo = typeof PlayniteInfo.Type; /** The whole export document. Decoding it IS the schema-version guard. */ export const LibraryExport = Schema.Struct({ schema: Schema.Number.pipe( Schema.check( Schema.isLessThanOrEqualTo(SCHEMA, { description: `a punktfunk-library.json schema this plugin understands (≤ ${SCHEMA}) — a higher one means the Playnite exporter is newer than the plugin; update the plugin`, }), ), ), /** ISO-8601 timestamp the exporter stamped on this document. */ generatedAt: withDefault(Schema.String, ""), playnite: withDefault(PlayniteInfo, {}), games: Schema.Array(ExportedGame), }); export type LibraryExport = typeof LibraryExport.Type;