9988729aab
Adds art.mode "host" (now the default): emit each cover's local file PATH and let the host serve the bytes via its art proxy, instead of inlining a data URL. The reconcile payload carries no image data, so a large Playnite library no longer exceeds the host's request-body limit. "dataurl" (inline, small libraries) and "off" remain. Requires a host with the provider-art proxy (punktfunk main: feat/library-local-art). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
// 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<string, string> = {
|
|
".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;
|
|
};
|