6694be9848
A two-part Punktfunk plugin that syncs the Playnite library into the host
game library as the `playnite` provider:
- exporter/ — a minimal C# Playnite GenericPlugin ("Punktfunk Sync") that
writes punktfunk-library.json on every library change. Reading Playnite's
live-locked LiteDB from outside isn't robust, so this adapter runs inside
Playnite. Builds net462 via reference assemblies; packaged as a .pext.
- src/ — the TS plugin (on @punktfunk/host, rom-manager shape): watches the
export, embeds covers as data URLs, reconciles via
PUT /library/provider/playnite, with a console-hosted web UI + standalone
fallback + CLI. Launch = playnite://playnite/start/<id>, run by the host
in the interactive session so Playnite performs the real launch.
Verified locally: backend tsc + 23 tests + biome clean, SPA build, C#
exporter build + .pext pack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Dev/debug CLI — reuses the pure core and the engine. `where`/`preview` are offline (no host needed);
|
|
// `sync`/`uninstall` connect to the host and reconcile. Handy for configuring a headless box or
|
|
// checking what a `config.json` will produce before pushing it to the library.
|
|
//
|
|
// bunx punktfunk-plugin-playnite where # where the exporter output was found
|
|
// bunx punktfunk-plugin-playnite preview # the desired library (no host write)
|
|
// bunx punktfunk-plugin-playnite sync # reconcile into the live library
|
|
// bunx punktfunk-plugin-playnite uninstall # remove all this plugin's entries
|
|
// bunx punktfunk-plugin-playnite set-password <pw> # standalone-UI password
|
|
|
|
import { connect } from "@punktfunk/host";
|
|
import { Engine } from "./engine/index.js";
|
|
import { computeEntries } from "./engine/reconcile.js";
|
|
import { locateExport, readExport } from "./playnite.js";
|
|
import { hashPassword } from "./server/password.js";
|
|
import { loadConfig, saveConfig } from "./state.js";
|
|
|
|
const cmdWhere = (): void => {
|
|
const config = loadConfig();
|
|
const loc = locateExport(config);
|
|
console.log(`Playnite data dir: ${loc.dir ?? "(not found)"}`);
|
|
console.log(`Export file: ${loc.file ?? "(not found)"}`);
|
|
if (loc.mtime)
|
|
console.log(`Last written: ${new Date(loc.mtime).toISOString()}`);
|
|
console.log("Candidates probed:");
|
|
for (const c of loc.candidates) console.log(` ${c}`);
|
|
if (!loc.file) {
|
|
console.log(
|
|
"\nNo export found. Install the Punktfunk Sync extension in Playnite (see exporter/README.md),",
|
|
);
|
|
console.log("or set `playniteDir` in the plugin config.json.");
|
|
}
|
|
};
|
|
|
|
const cmdPreview = (): void => {
|
|
const config = loadConfig();
|
|
const loc = locateExport(config);
|
|
if (!loc.file) {
|
|
console.log("No exporter output found — run `where` for details.");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const exp = readExport(loc.file);
|
|
const { entries, report } = computeEntries({
|
|
exp,
|
|
config,
|
|
artFor: () => undefined,
|
|
});
|
|
console.log(
|
|
`Preview: ${report.included}/${report.considered} game(s) would sync (${report.skipped.length} skipped).`,
|
|
);
|
|
const bySource = Object.entries(report.perSource).sort((a, b) => b[1] - a[1]);
|
|
for (const [s, n] of bySource) console.log(` ${s}: ${n}`);
|
|
console.log();
|
|
for (const e of entries.slice(0, 40))
|
|
console.log(` ${e.title} → ${e.launch?.value ?? "(no launch)"}`);
|
|
if (entries.length > 40) console.log(` … and ${entries.length - 40} more`);
|
|
if (report.skipped.length) {
|
|
console.log(`\n${report.skipped.length} skipped (first 20):`);
|
|
for (const s of report.skipped.slice(0, 20))
|
|
console.log(` ${s.title}: ${s.reason}`);
|
|
}
|
|
for (const w of report.warnings) console.log(`! ${w}`);
|
|
};
|
|
|
|
const cmdSync = async (): Promise<void> => {
|
|
const pf = await connect();
|
|
try {
|
|
const report = await new Engine({ pf }).sync("cli");
|
|
if (report) {
|
|
console.log(
|
|
`Synced: ${report.included} title(s), ${report.skipped.length} skipped.`,
|
|
);
|
|
} else {
|
|
console.log("Sync did not reconcile (no change, or see log).");
|
|
}
|
|
} finally {
|
|
pf.close();
|
|
}
|
|
};
|
|
|
|
const cmdUninstall = async (): Promise<void> => {
|
|
const pf = await connect();
|
|
try {
|
|
await new Engine({ pf }).uninstall();
|
|
console.log("Removed all playnite entries from the library.");
|
|
} finally {
|
|
pf.close();
|
|
}
|
|
};
|
|
|
|
const cmdSetPassword = (password: string | undefined): void => {
|
|
if (!password) {
|
|
console.error("usage: set-password <password>");
|
|
process.exitCode = 2;
|
|
return;
|
|
}
|
|
const config = loadConfig();
|
|
config.ui.passwordHash = hashPassword(password);
|
|
saveConfig(config);
|
|
console.log("Standalone-UI password set (config.ui.passwordHash).");
|
|
};
|
|
|
|
const main = async (): Promise<void> => {
|
|
const [cmd, arg] = process.argv.slice(2);
|
|
switch (cmd) {
|
|
case "where":
|
|
return cmdWhere();
|
|
case "preview":
|
|
return cmdPreview();
|
|
case "sync":
|
|
return cmdSync();
|
|
case "uninstall":
|
|
return cmdUninstall();
|
|
case "set-password":
|
|
return cmdSetPassword(arg);
|
|
default:
|
|
console.log(
|
|
"usage: punktfunk-plugin-playnite <where|preview|sync|uninstall|set-password>",
|
|
);
|
|
process.exitCode = cmd ? 2 : 0;
|
|
}
|
|
};
|
|
|
|
await main();
|