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>
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import { afterAll, describe, expect, test } from "bun:test";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { buildArtwork, fileToDataUrl } from "../src/art.js";
|
|
import { DEFAULT_ART } from "../src/config.js";
|
|
import type { ExportedGame } from "../src/export-schema.js";
|
|
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pf-playnite-art-"));
|
|
const write = (name: string, bytes: Buffer): string => {
|
|
const p = path.join(dir, name);
|
|
fs.writeFileSync(p, bytes);
|
|
return p;
|
|
};
|
|
afterAll(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
|
|
const game = (over: Partial<ExportedGame>): ExportedGame => ({
|
|
id: "11111111-1111-1111-1111-111111111111",
|
|
name: "Game",
|
|
installed: true,
|
|
hidden: false,
|
|
source: "Steam",
|
|
platforms: [],
|
|
cover: null,
|
|
background: null,
|
|
icon: null,
|
|
...over,
|
|
});
|
|
|
|
describe("fileToDataUrl", () => {
|
|
test("encodes a small png as a data URL", () => {
|
|
const p = write("cover.png", Buffer.from([1, 2, 3, 4]));
|
|
expect(fileToDataUrl(p, 1000)).toBe(
|
|
`data:image/png;base64,${Buffer.from([1, 2, 3, 4]).toString("base64")}`,
|
|
);
|
|
});
|
|
test("drops files over the size cap", () => {
|
|
const p = write("big.jpg", Buffer.alloc(2000, 7));
|
|
expect(fileToDataUrl(p, 1000)).toBeNull();
|
|
});
|
|
test("rejects unknown extensions and null", () => {
|
|
const p = write("weird.xyz", Buffer.from([1]));
|
|
expect(fileToDataUrl(p, 1000)).toBeNull();
|
|
expect(fileToDataUrl(null, 1000)).toBeNull();
|
|
});
|
|
test("mime follows the extension", () => {
|
|
const p = write("c.jpeg", Buffer.from([9]));
|
|
expect(fileToDataUrl(p, 1000)?.startsWith("data:image/jpeg;base64,")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("buildArtwork", () => {
|
|
test("off mode yields no art", () => {
|
|
const cover = write("c1.png", Buffer.from([1]));
|
|
expect(
|
|
buildArtwork(game({ cover }), { ...DEFAULT_ART, mode: "off" }),
|
|
).toBeUndefined();
|
|
});
|
|
test("host mode (default) emits the raw cover path — no bytes", () => {
|
|
const cover = write("c2.png", Buffer.from([1, 2]));
|
|
const art = buildArtwork(game({ cover }), DEFAULT_ART);
|
|
expect(art?.portrait).toBe(cover);
|
|
expect(art?.hero).toBeUndefined();
|
|
});
|
|
test("host mode includeBackground adds the hero path", () => {
|
|
const cover = write("c2b.png", Buffer.from([1]));
|
|
const background = write("b2b.jpg", Buffer.from([2]));
|
|
const art = buildArtwork(game({ cover, background }), {
|
|
...DEFAULT_ART,
|
|
includeBackground: true,
|
|
});
|
|
expect(art?.portrait).toBe(cover);
|
|
expect(art?.hero).toBe(background);
|
|
});
|
|
test("host mode with no cover yields undefined", () => {
|
|
expect(buildArtwork(game({ cover: null }), DEFAULT_ART)).toBeUndefined();
|
|
});
|
|
test("dataurl mode embeds the cover as a portrait data URL", () => {
|
|
const cover = write("c3.png", Buffer.from([1, 2]));
|
|
const art = buildArtwork(game({ cover }), {
|
|
...DEFAULT_ART,
|
|
mode: "dataurl",
|
|
});
|
|
expect(art?.portrait?.startsWith("data:image/png;base64,")).toBe(true);
|
|
expect(art?.hero).toBeUndefined();
|
|
});
|
|
test("dataurl includeBackground adds a hero data URL", () => {
|
|
const cover = write("c4.png", Buffer.from([1]));
|
|
const background = write("b4.jpg", Buffer.from([2]));
|
|
const art = buildArtwork(game({ cover, background }), {
|
|
...DEFAULT_ART,
|
|
mode: "dataurl",
|
|
includeBackground: true,
|
|
});
|
|
expect(art?.portrait).toBeTruthy();
|
|
expect(art?.hero?.startsWith("data:image/jpeg;base64,")).toBe(true);
|
|
});
|
|
test("dataurl mode with no readable art yields undefined", () => {
|
|
expect(
|
|
buildArtwork(game({ cover: "/does/not/exist.png" }), {
|
|
...DEFAULT_ART,
|
|
mode: "dataurl",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
});
|