// Cover art. Playnite stores art as local files on the host. Two delivery modes (see ArtMode): // - `host` (default): emit the local file PATH; the updated host serves the bytes via its art proxy // (`/library/art/…`), so the reconcile payload carries no image data and scales to any library size. // - `dataurl`: inline the cover as a size-capped `data:` URL (self-contained, but only fit for small // libraries — a big one blows past the host's reconcile body limit). `maxBytes` caps a single image. import * as fs from "node:fs"; import * as path from "node:path"; import type { ArtOptions } from "./config.js"; import type { ExportedGame } from "./export-schema.js"; import type { Artwork } from "./wire.js"; const MIME: Record = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif", ".bmp": "image/bmp", ".ico": "image/x-icon", ".tga": "image/x-tga", }; const mimeFor = (file: string): string | null => MIME[path.extname(file).toLowerCase()] ?? null; /** Read a local image file into a `data:` URL, or null if missing, oversized, or an unknown type. */ export const fileToDataUrl = ( file: string | null, maxBytes: number, ): string | null => { if (!file) return null; const mime = mimeFor(file); if (!mime) return null; try { const st = fs.statSync(file); if (!st.isFile() || st.size === 0 || st.size > maxBytes) return null; const b64 = fs.readFileSync(file).toString("base64"); return `data:${mime};base64,${b64}`; } catch { return null; } }; /** Build the artwork block for a game per the art config. Returns undefined when nothing was embedded. */ export const buildArtwork = ( game: ExportedGame, art: ArtOptions, ): Artwork | undefined => { if (art.mode === "off") return undefined; // `host` mode: emit the local file paths verbatim (the exporter only sets a path when the file // exists). The host serves the bytes via its art proxy, so the reconcile payload carries no image // data and scales to any library size. The default. if (art.mode === "host") { const out: Artwork = {}; if (game.cover) out.portrait = game.cover; if (art.includeBackground && game.background) out.hero = game.background; return out.portrait || out.hero ? out : undefined; } // `dataurl` mode: inline (size-capped) — self-contained but only fit for small libraries. const portrait = fileToDataUrl(game.cover, art.maxBytes); const hero = art.includeBackground ? fileToDataUrl(game.background, art.maxBytes) : null; if (!portrait && !hero) return undefined; const out: Artwork = {}; if (portrait) out.portrait = portrait; if (hero) out.hero = hero; return out; };