A portable Playnite's covers were dropped, and nothing republished them once you fixed it #330

Merged
enricobuehler merged 2 commits from worktree-playnite-art-roots-and-blank-sources into main 2026-08-19 12:29:29 +00:00
Owner

Two bug reports came in against the Playnite plugin. This is the host half of the fix; the plugin half is unom/punktfunk-plugin-playnite#8.

A portable Playnite's covers survive the art confinement

A Playnite unzipped outside the users base (D:\Apps\Playnite) keeps its whole library beside the exe, so every cover it exports lives at <PlayniteDir>\library\files\… — outside every default art root. The games synced and all 70 covers were dropped, with only PUNKTFUNK_LIBRARY_ART_ROOTS as a way out:

library reconcile: dropped local art the proxy may not serve
  provider=playnite dropped=70
  example_path=D:\Apps\Playnite\library\files\…\….jpg

The Playnite install dirs are now art roots too, exactly as Steam's install root already is. And playnite_install_dirs learned to find a portable copy at all: it registers no uninstall entry and sits under no profile, but it does register the playnite:// handler — the very registration this host's launch path already follows to start a title. So the same probe also gives a portable install its Fullscreen launcher tile, which it never had.

The confinement is not loosened. Every root comes from the host's own registry and filesystem probes, never from the plugin lane that supplies the art path, and the extension, regular-file, magic-byte and config-dir gates all still apply. The per-user hives part of this comes from is a bar the host already stands on, one rung lower here than where it stood: the same lookup picks the Playnite.FullscreenApp.exe a launcher tile spawns, so trusting it to name a directory whose image files may be read is strictly weaker than trusting it to name a program to run.

…and a restart or a Sync-now republishes, instead of answering "no changes"

The reported workaround ended with "delete plugin-state/playnite/cache.json", and that step was load-bearing — which is its own bug.

The sync engine's fingerprint says we would compute the same entries again. It does not say the host still holds them, and the host may accept a payload and store less of it than was sent: an art path outside its roots is stripped and the games kept (deliberately — a cover must not cost a library), a launcher tile it cannot open is dropped the same way. Once that happens the fingerprint is a permanent "no changes": you fix the host side, nothing republishes, and the only escape is deleting the cache file.

So the two triggers with a person behind them always apply — startup, the restart everyone reaches for, and manual, the console's Sync-now button and the CLI's sync. The loop reasons (poll, fs-change, config-change, coalesced) keep the short-circuit, which is where it earns its keep: they are what would otherwise PUT the whole library every few minutes.

Testing

  • plugin-kit: 84 pass / 0 fail, typecheck and biome clean. The existing "unchanged content skips the apply" test used manual as a stand-in for a loop trigger and now uses poll, preserving its intent; a new test covers the forced reasons.
  • Host: cargo fmt --check clean. exe_from_shell_command's test passes (extracted and run standalone — the crate does not build on macOS for pre-existing reasons: opus and procscan::Scanner are cfg'd out there).
  • The whole cfg(windows) Playnite block typechecks against x86_64-pc-windows-msvc in a scratch crate with winreg 0.56. Worth knowing at review time: no CI here compiles that path before merge — ci.yml is Linux-only on PRs and windows-host.yml only runs on push to main.

Residual

If Playnite's URI handler was never registered on a box, that install stays undiscoverable and PUNKTFUNK_LIBRARY_ART_ROOTS remains the escape hatch. docs-site/content/docs/configuration.md is updated to say Playnite is covered by default.

Two bug reports came in against the Playnite plugin. This is the host half of the fix; the plugin half is [unom/punktfunk-plugin-playnite#8](https://git.unom.io/unom/punktfunk-plugin-playnite/pulls/8). ## A portable Playnite's covers survive the art confinement A Playnite unzipped outside the users base (`D:\Apps\Playnite`) keeps its whole library beside the exe, so every cover it exports lives at `<PlayniteDir>\library\files\…` — outside every default art root. The games synced and all 70 covers were dropped, with only `PUNKTFUNK_LIBRARY_ART_ROOTS` as a way out: ``` library reconcile: dropped local art the proxy may not serve provider=playnite dropped=70 example_path=D:\Apps\Playnite\library\files\…\….jpg ``` The Playnite install dirs are now art roots too, exactly as Steam's install root already is. And `playnite_install_dirs` learned to find a portable copy **at all**: it registers no uninstall entry and sits under no profile, but it does register the `playnite://` handler — the very registration this host's launch path already follows to start a title. So the same probe also gives a portable install its Fullscreen launcher tile, which it never had. The confinement is not loosened. Every root comes from the host's own registry and filesystem probes, never from the plugin lane that supplies the art path, and the extension, regular-file, magic-byte and config-dir gates all still apply. The per-user hives part of this comes from is a bar the host already stands on, one rung *lower* here than where it stood: the same lookup picks the `Playnite.FullscreenApp.exe` a launcher tile spawns, so trusting it to name a directory whose image files may be read is strictly weaker than trusting it to name a program to run. ## …and a restart or a Sync-now republishes, instead of answering "no changes" The reported workaround ended with *"delete `plugin-state/playnite/cache.json`"*, and that step was load-bearing — which is its own bug. The sync engine's fingerprint says we would compute the same entries again. It does **not** say the host still holds them, and the host may accept a payload and store less of it than was sent: an art path outside its roots is stripped and the games kept (deliberately — a cover must not cost a library), a launcher tile it cannot open is dropped the same way. Once that happens the fingerprint is a permanent "no changes": you fix the host side, nothing republishes, and the only escape is deleting the cache file. So the two triggers with a person behind them always apply — `startup`, the restart everyone reaches for, and `manual`, the console's Sync-now button and the CLI's `sync`. The loop reasons (`poll`, `fs-change`, `config-change`, `coalesced`) keep the short-circuit, which is where it earns its keep: they are what would otherwise PUT the whole library every few minutes. ## Testing - `plugin-kit`: 84 pass / 0 fail, typecheck and biome clean. The existing "unchanged content skips the apply" test used `manual` as a stand-in for a loop trigger and now uses `poll`, preserving its intent; a new test covers the forced reasons. - Host: `cargo fmt --check` clean. `exe_from_shell_command`'s test passes (extracted and run standalone — the crate does not build on macOS for pre-existing reasons: `opus` and `procscan::Scanner` are cfg'd out there). - The whole `cfg(windows)` Playnite block typechecks against `x86_64-pc-windows-msvc` in a scratch crate with winreg 0.56. **Worth knowing at review time:** no CI here compiles that path before merge — `ci.yml` is Linux-only on PRs and `windows-host.yml` only runs on push to main. ## Residual If Playnite's URI handler was never registered on a box, that install stays undiscoverable and `PUNKTFUNK_LIBRARY_ART_ROOTS` remains the escape hatch. `docs-site/content/docs/configuration.md` is updated to say Playnite is covered by default.
enricobuehler added 2 commits 2026-08-19 11:46:40 +00:00
A Playnite unzipped outside the users base (D:\Apps\Playnite) keeps its whole
library beside the exe, so every cover it exports lives at
`<PlayniteDir>\library\files\…` — outside every default art root. The games
synced and all 70 covers were dropped, with only PUNKTFUNK_LIBRARY_ART_ROOTS
as a way out.

The Playnite install dirs are now art roots too, exactly as Steam's install
root already is, and `playnite_install_dirs` learned to find a portable copy at
all: it registers no uninstall entry and sits under no profile, but it does
register the `playnite://` handler — the very registration this host's launch
path already follows to start a Playnite title. So the same probe also gives a
portable install its Fullscreen launcher tile, which it never had.

The confinement is not loosened: the roots come from the host's own registry
and filesystem probes, never from the plugin lane that supplies the art path,
and the extension, regular-file, magic-byte and config-dir gates all still
apply.
fix(plugin-kit): a restart or a Sync-now republishes, instead of answering "no changes"
ci / web (pull_request) Successful in 1m20s
ci / rust-arm64 (pull_request) Successful in 2m18s
ci / bun-nix (pull_request) Successful in 28s
ci / docs-site (pull_request) Successful in 2m15s
ci / rust (pull_request) Successful in 7m2s
android / android (pull_request) Successful in 7m21s
9ddf802665
The sync engine's fingerprint says we would compute the same entries again. It does
NOT say the host still holds them — and the host may accept a payload and store less
of it than was sent: an art path outside its allowed roots is stripped and the games
kept (deliberately, a cover must not cost a library), a launcher tile it cannot open
is dropped the same way.

Once that happens the fingerprint is a permanent "no changes". The operator fixes the
host side, nothing republishes, and the only way out is to delete the plugin's cache
file — which is exactly the advice a portable-Playnite library with 70 dropped covers
was given.

So the two triggers with a person behind them always apply: `startup`, the restart
everyone reaches for, and `manual`, the console's Sync-now button and the CLI's
`sync`. Both mean "publish my library NOW", and "no changes" is the wrong answer to
that. The loop reasons keep the short-circuit, which is where it earns its keep —
they are what would otherwise PUT the whole library every few minutes.
enricobuehler merged commit 7537e8e6b2 into main 2026-08-19 12:29:29 +00:00
enricobuehler deleted branch worktree-playnite-art-roots-and-blank-sources 2026-08-19 12:29:37 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#330