01Steamappcache/appinfo.vdf
A binary file with no public documentation. The library cache says which app ids you have; only appinfo.vdf says which of them are games rather than DLC, tools or test configs — and it still knows games the store has delisted, which the web API no longer returns.
const magic = buf.readUInt32LE(0);
if (magic !== 0x07564429) throw new Error(`unsupported appinfo.vdf format 0x${magic.toString(16)}`);
const tableAt = Number(buf.readBigUInt64LE(8));The format version is checked before anything else is read. When Valve changes the layout, Playdex says so by name, rather than reading the new layout as if it were the old one and filling the library with garbage.
// Entry: appid, size, then 60 bytes of header (state, update time, token, 2 hashes, change number), then KeyValues.
for (let off = 16; off < tableAt; ) {
const appid = String(buf.readUInt32LE(off));
if (appid === '0') break;
const size = buf.readUInt32LE(off + 4);
if (wanted.has(appid)) {
const common = readKV(off + 68).appinfo?.common;
if (common) apps.set(appid, { name: common.name, type: common.type });
}
off += 8 + size;
}Every entry carries its own size, so the loop can jump straight over every app it does not need and decode only the ones in your library. Key names are not stored inline: they are indexes into a string table at the end of the file, which is read first.
02EpicData/Catalog/catcache.bin
Not binary at all once you look: it is base64-encoded JSON of every catalog entry in your library. The work is in the filtering, because Epic files creator kits as engines and applications, and Unreal Engine as an engine alone.
const catalog = JSON.parse(Buffer.from(fs.readFileSync(path.join(base, 'Catalog/catcache.bin'), 'utf8'), 'base64').toString('utf8'));
return catalog
// Same as Epic's Library tab: games plus creator/mod kits (filed as engines + applications, e.g.
// "Hogwarts Legacy Creator Kit"). Not DLC (has a main game), Unreal Engine (engines only) or Twinmotion (software).
.filter(i => {
const cats = new Set(i.categories?.map(c => c.path));
return !i.mainGameItem?.id && (cats.has('games') || (cats.has('engines') && cats.has('applications')));
})The cache also stores ™ and ® as a literal question mark — Apex Legends? — so titles are cleaned before they are matched against the other two stores.
03GOG Galaxystorage/galaxy-2.0.db
A real SQLite database, which Galaxy keeps open the whole time it runs. Playdex never opens the live file. It copies it to a temporary folder and reads the copy with Node's built-in SQLite.
// Copy first: Galaxy keeps the live DB open, and SQLite on Windows fails on very long paths.
const tmp = path.join(os.tmpdir(), 'playdex-gog');
fs.mkdirSync(tmp, { recursive: true });
for (const ext of ['', '-wal', '-shm']) {
if (fs.existsSync(src + ext)) fs.copyFileSync(src + ext, path.join(tmp, 'galaxy-2.0.db' + ext));
}
const db = new DatabaseSync(path.join(tmp, 'galaxy-2.0.db'));Copying the .db alone looks right and is subtly stale: in WAL mode, recent writes sit in the -wal file until SQLite checkpoints them. Copy all three, and the snapshot matches what Galaxy itself is showing.