feat(art): default to host-served cover art (scales to large libraries)
CI / publish (push) Has been skipped
CI / exporter (push) Successful in 12s
CI / build (push) Successful in 20s

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>
This commit is contained in:
2026-07-19 00:56:15 +02:00
parent f77790db05
commit 9988729aab
6 changed files with 69 additions and 27 deletions
+15 -5
View File
@@ -1,8 +1,8 @@
// Cover art. Playnite stores art as local files on the host; the host's library only proxies Steam
// art, and provider entries are fetched by clients directly — so for a Playnite title we inline the
// cover as a `data:` URL the clients render without any network fetch. A `data:` URL bloats the
// library payload, so anything over `maxBytes` is dropped rather than embedded (design default: cover
// only; backgrounds are large and opt-in).
// 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";
@@ -48,6 +48,16 @@ export const buildArtwork = (
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)
+9 -5
View File
@@ -25,10 +25,14 @@ export interface FilterOptions {
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";
/** How cover art reaches the clients. Playnite art is local files on the host:
* - `host` (default): send the local file PATH; the host serves the bytes through its art proxy
* (`/library/art/…`, like Steam art). The reconcile payload carries no image data, so this scales
* to libraries of any size — the recommended mode.
* - `dataurl`: inline each cover as a size-capped `data:` URL. Self-contained but does NOT scale — a
* large library blows past the host's reconcile body limit. Small libraries only.
* - `off`: sync titles with no art. */
export type ArtMode = "host" | "dataurl" | "off";
export interface ArtOptions {
mode: ArtMode;
@@ -88,7 +92,7 @@ export const DEFAULT_FILTER: FilterOptions = {
};
export const DEFAULT_ART: ArtOptions = {
mode: "dataurl",
mode: "host",
maxBytes: 1_500_000,
includeBackground: false,
};
+5 -4
View File
@@ -61,8 +61,9 @@ export interface ComputeResult {
report: SyncReport;
}
/** Warn above this many entries — every cover is an inlined `data:` URL, so a huge library makes a
* heavy library payload (see README "Box art"). */
/** In `dataurl` art mode only: warn above this many entries, since each inlined cover adds to the
* reconcile payload and a large library blows past the host's body limit (see README "Box art").
* `host` mode (the default) sends paths, not bytes, so it has no such ceiling. */
const WARN_ENTRIES = 3000;
export const computeEntries = ({
@@ -128,9 +129,9 @@ export const computeEntries = ({
// Stable order (by title) so the reconcile fingerprint is deterministic across reads.
entries.sort((a, b) => a.title.localeCompare(b.title));
if (entries.length > WARN_ENTRIES) {
if (config.art.mode === "dataurl" && entries.length > WARN_ENTRIES) {
warnings.push(
`${entries.length} entries — large library; inlined covers make a heavy library payload. Consider art.mode "off" or a tighter source filter.`,
`${entries.length} entries with art.mode "dataurl" — inlined covers will exceed the host's reconcile body limit. Switch to art.mode "host" (the default), which serves covers by path.`,
);
}