Steam library: safe removal and title matching, artwork within Steam's limit

- Remove: collections and artwork clearing are best effort in Steam's JS, and
  the host carries on to delete the files when Steam isn't running (Android
  apps and devkit titles).
- Devkit titles: the shortcut is found by devkit id, the saved id, or an
  executable/start folder inside the title's folder; never by display name.
  Steam's overviews don't carry devkit_gameid (checked on the Frame
  2026-09-28), so 'list' now reads exe/start dir from app details.
- Photo-based grid/wide/hero slots render as JPEG (a noise-heavy 3840x1240
  hero was over 12 MiB as PNG on the Frame); the logo stays transparent PNG.
  Rendering gets 75 s and retries once with generated art. Each slot is
  cleared before it is set, since Steam keeps .png and .jpg side by side.
- Devkit titles keep their own VR flag (vr=None skips SetShortcutIsVR) and
  their Sideloaded collection.
- A failed title install's cleanup can't replace the original error.
- refresh_art for devkit titles (frame_titles.refresh_art).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 23:06:25 +10:00
1 parent baefa105a9
commit f0db805d4b
9 files changed
+378 -85

No files matched your search

+6 -1
View File
@@ -6,8 +6,13 @@ const stub=require(process.cwd()+'/tests/fakeframe/rootfs/usr/local/lib/fakefram
const result=await ctx.renderLibraryArtwork({label:'Example Game',images:{icon:['png','fixture']}});
assert.deepEqual(Object.keys(result.images),['grid','wide','hero','logo','icon']);
for(const [slot,size] of Object.entries({grid:[600,900],wide:[920,430],hero:[3840,1240],logo:[1280,480],icon:[256,256]})) {
const b=Buffer.from(result.images[slot],'base64');assert.equal(b.readUInt32BE(16),size[0]);assert.equal(b.readUInt32BE(20),size[1]);
assert.equal(result.images[slot][0],'png');
const b=Buffer.from(result.images[slot][1],'base64');assert.equal(b.readUInt32BE(16),size[0]);assert.equal(b.readUInt32BE(20),size[1]);
}
// A photo scene is JPEG (Steam's 12 MiB limit at hero size); the logo stays transparent PNG.
const photo=await ctx.renderLibraryArtwork({label:'Photo',images:{hero:['jpg','fixture'],banner:['jpg','fixture']}});
for(const slot of ['wide','hero'])assert.equal(photo.images[slot][0],'jpg');
for(const slot of ['grid','logo','icon'])assert.equal(photo.images[slot][0],'png');
const hero=surface.canvases.find(c=>c.width===3840);assert.equal(hero.text.length,0);
const logo=surface.canvases.find(c=>c.width===1280);assert(logo.text.length);assert.equal(logo.draws,0);
const before=surface.canvases.length;await ctx.renderLibraryArtwork({label:'No Icon',images:{}});