'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const database = require('../../src/database'); const { cacheLocalArtwork, discoverGameId, scanLocalLibraries, gameIdFrom, } = require('../../src/services/local-library-scanner'); function normaliseLocationPath(filePath) { return String(filePath || '').replace(/\\/g, '/').replace(/\/+/g, '/'); } function deriveRelativePath(filePath) { const normalized = normaliseLocationPath(filePath); const trimmed = normalized.replace(/^\/+/, ''); const parts = trimmed.split('/').filter(Boolean); return parts.length > 1 ? parts.slice(1).join('/') : trimmed || 'legacy-root'; } function withTempDatabase(fn) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-library-test-')); database.configure({ dataDir: root, logger: { info() {}, warn() {}, error() {} }, }); return database.initialize() .then(() => fn()) .finally(() => { database.close(); fs.rmSync(root, { recursive: true, force: true }); }); } function seedLocalCopy({ root, gameId, copyType = 'folder', version = '1.00', size = 0 }) { const relativePath = deriveRelativePath(path.join(root, gameId)); const trimmedRoot = normaliseLocationPath(root); const rootPart = trimmedRoot.replace(/^\/+/, '').split('/').filter(Boolean); const fingerprint = `legacy:${rootPart[0] || 'unknown-device'}`; const volumeKey = rootPart.length > 1 ? `legacy:${rootPart.slice(0, 2).join('/')}` : `legacy:${rootPart[0] || 'unknown'}`; const content = String(gameId).toUpperCase(); database.runMutatingSqlWithParams(` INSERT INTO titles (platform, title_id, content_id, name, sort_name, region, metadata_source) VALUES (@platform, @gameId, @content, @name, @name, 'unknown', 'test') `, { platform: content.startsWith('CUSA') ? 'ps4' : 'ps5', gameId: content, content: `${content}-CONTENT`, name: content, }); const titleId = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0]?.id || 0); database.runMutatingSqlWithParams(` INSERT INTO storage_devices (scope, stable_fingerprint, display_name, device_type, availability_state) VALUES ('server', @fingerprint, @displayName, 'server', 'available') `, { fingerprint, displayName: root }); const deviceId = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0]?.id || 0); database.runMutatingSqlWithParams(` INSERT INTO storage_volumes (device_id, stable_volume_key, mount_path, can_copy, can_play, can_install, can_mount, availability_state) VALUES (@deviceId, @volumeKey, @mountPath, 1, 1, 1, 1, 'available') `, { deviceId, volumeKey, mountPath: root, }); const volumeId = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0]?.id || 0); database.runMutatingSqlWithParams(` INSERT INTO local_copies (title_id, volume_id, relative_path, copy_type, base_version, version_source, size_bytes, last_seen_at, availability_state) VALUES (@titleId, @volumeId, @relativePath, @copyType, @baseVersion, 'scan', @sizeBytes, 0, 'available') `, { titleId, volumeId, relativePath, copyType, baseVersion: version, sizeBytes: size, }); } test('local library scan writes local_copies and records scan coverage', () => { return withTempDatabase(async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-library-')); fs.mkdirSync(path.join(root, 'Game.CUSA03041')); fs.mkdirSync(path.join(root, 'Game.PS5-PPSA12345')); fs.mkdirSync(path.join(root, 'Release-without-title-id')); try { const result = await scanLocalLibraries({ roots: [root], db: database }); const copies = Number(database.runQuery('SELECT COUNT(*) AS c FROM local_copies')[0]?.c || 0); assert.deepEqual([...new Set(result.found)].sort(), ['CUSA03041', 'PPSA12345']); assert.equal(result.total, 2); assert.equal(result.added, 2); assert.equal(result.skippedWithoutGameId, 1); assert.equal(result.scan_run_id > 0, true); assert.equal(copies, 2); assert.equal(database.runQuery('SELECT COUNT(*) AS c FROM scan_root_coverage')[0]?.c || 0, 1); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); }); test('nested platform folders register separate PS5 JSON-identified backups', () => withTempDatabase(async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-nested-json-')); try { for (const [name, id] of [['Museum backup', 'PPSA12345'], ['Second backup', 'PPSA54321']]) { const directory = path.join(root, 'ps5', name, 'sce_sys'); fs.mkdirSync(directory, { recursive: true }); fs.writeFileSync(path.join(directory, 'param.json'), JSON.stringify({ titleId: id, contentId: `UP0001-${id}_00-CHIMERAFPKG00001`, contentVersion: '01.000.000', localizedParameters: { enUS: { titleName: name } } })); } const result = await scanLocalLibraries({ roots: [root], db: database }); assert.deepEqual(result.found.sort(), ['PPSA12345', 'PPSA54321']); const copies = database.runQuery('SELECT relative_path FROM local_copies'); assert.equal(copies.length, 2); assert.ok(copies.every(copy => /backup$/.test(copy.relative_path))); } finally { fs.rmSync(root, { recursive: true, force: true }); } })); test('local library discovery reads IDs from nested archive and NFO names', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-library-nested-')); const archiveRelease = path.join(root, 'Marvel Spider-Man Remastered'); const nfoRelease = path.join(root, 'Disney Epic Mickey Rebrushed'); fs.mkdirSync(path.join(archiveRelease, 'archives'), { recursive: true }); fs.mkdirSync(nfoRelease, { recursive: true }); fs.writeFileSync(path.join(archiveRelease, 'archives', 'release-PPSA01467.part01.rar'), 'fixture'); fs.writeFileSync(path.join(nfoRelease, 'release.nfo'), 'Title ID: PPSA15657\n'); try { assert.equal(await discoverGameId(archiveRelease, true), 'PPSA01467'); assert.equal(await discoverGameId(nfoRelease, true), 'PPSA15657'); assert.equal(await discoverGameId(path.join(root, 'Unknown release'), false), ''); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('recursive scan preserves NFO and multipart release wrappers as single copies', () => withTempDatabase(async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-wrapper-scan-')); try { const nfo = path.join(root, 'ps5', 'Nfo release'); const archive = path.join(root, 'ps5', 'Archive release'); fs.mkdirSync(nfo, { recursive: true }); fs.mkdirSync(path.join(archive, 'archives'), { recursive: true }); fs.writeFileSync(path.join(nfo, 'release.nfo'), 'Title ID: PPSA12345'); for (const part of ['01', '02']) fs.writeFileSync(path.join(archive, 'archives', `PPSA54321.part${part}.rar`), 'fixture'); const result = await scanLocalLibraries({ roots: [root], db: database }); assert.deepEqual(result.found.sort(), ['PPSA12345', 'PPSA54321']); const copies = database.runQuery('SELECT relative_path FROM local_copies'); assert.equal(copies.length, 2); assert.ok(copies.every(copy => /release$/.test(copy.relative_path))); } finally { fs.rmSync(root, { recursive: true, force: true }); } })); test('linked subtree is not followed and incomplete coverage preserves prior copies', () => withTempDatabase(async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-partial-scan-')); const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-outside-scan-')); try { seedLocalCopy({ root, gameId: 'PPSA11111' }); fs.mkdirSync(path.join(root, 'PPSA22222')); fs.mkdirSync(path.join(outside, 'PPSA33333')); fs.symlinkSync(outside, path.join(root, 'linked'), process.platform === 'win32' ? 'junction' : 'dir'); const result = await scanLocalLibraries({ roots: [root], db: database }); assert.deepEqual(result.found, ['PPSA22222']); assert.equal(database.runQuery('SELECT coverage_state FROM scan_root_coverage')[0].coverage_state, 'partial'); assert.equal(database.runQuery("SELECT availability_state FROM local_copies WHERE relative_path LIKE '%PPSA11111'")[0].availability_state, 'available'); } finally { fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(outside, { recursive: true, force: true }); } })); test('conflicting folder and PS5 metadata identities are not combined', () => withTempDatabase(async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-identity-scan-')); try { const metadata = path.join(root, 'PPSA11111', 'sce_sys'); fs.mkdirSync(metadata, { recursive: true }); fs.writeFileSync(path.join(metadata, 'param.json'), JSON.stringify({ titleId: 'PPSA22222' })); const result = await scanLocalLibraries({ roots: [root], db: database }); assert.deepEqual(result.found, []); assert.equal(database.runQuery('SELECT coverage_state FROM scan_root_coverage')[0].coverage_state, 'partial'); } finally { fs.rmSync(root, { recursive: true, force: true }); } })); test('local library scanner detects IDs from bounded package metadata', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-library-containers-')); const pkgRelease = path.join(root, 'Uncharted 4 release'); const zipRelease = path.join(root, 'Destroy All Humans 2 release'); const ignoredRelease = path.join(root, 'Unknown binary release'); fs.mkdirSync(pkgRelease); fs.mkdirSync(zipRelease); fs.mkdirSync(ignoredRelease); const pkg = Buffer.alloc(256 * 1024); pkg.write('EP9000-CUSA00917_00-UNCHARTED4000000', 4096, 'ascii'); fs.writeFileSync(path.join(pkgRelease, 'game.pkg'), pkg); const zip = Buffer.alloc(512 * 1024); zip.write('PPSA05398_00-DAH2REPROBED0000.pkg', zip.length - 8192, 'ascii'); fs.writeFileSync(path.join(zipRelease, 'release.zip'), zip); fs.writeFileSync(path.join(ignoredRelease, 'unrelated.bin'), 'CUSA99999'); try { assert.equal(gameIdFrom(await discoverGameId(pkgRelease, true)), 'CUSA00917'); assert.equal(gameIdFrom(await discoverGameId(zipRelease, true)), 'PPSA05398'); assert.equal(gameIdFrom(await discoverGameId(ignoredRelease, true)), ''); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('cacheLocalArtwork validates and stores local cover images', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-artwork-')); const artworkDir = path.join(root, 'sce_sys'); const cacheDir = path.join(root, 'cache'); fs.mkdirSync(artworkDir, { recursive: true }); const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]); fs.writeFileSync(path.join(artworkDir, 'icon0.png'), png); try { const result = await cacheLocalArtwork({ metadata: { artwork_dir: artworkDir }, gameId: 'CUSA03041', cacheDir }); assert.equal(result.icon, '/pork-cache/game-metadata/CUSA03041/local-icon.png'); assert.deepEqual(fs.readFileSync(path.join(cacheDir, 'game-metadata', 'CUSA03041', 'local-icon.png')), png); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('scheduled scan reuses previous size when refresh is disabled', () => { return withTempDatabase(() => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-library-size-')); const gamePath = path.join(root, 'CUSA03041'); fs.mkdirSync(gamePath); fs.writeFileSync(path.join(gamePath, 'large.bin'), Buffer.alloc(256)); const expected = 12345; seedLocalCopy({ root, gameId: 'CUSA03041', size: expected }); return scanLocalLibraries({ roots: [root], db: database }).then(() => { const relativePath = deriveRelativePath(gamePath); const row = database.runQuery(` SELECT lc.size_bytes FROM local_copies lc JOIN titles t ON t.id = lc.title_id WHERE t.title_id = 'CUSA03041' AND lc.relative_path = ${JSON.stringify(relativePath)} `)[0]; assert.equal(row.size_bytes, expected); fs.rmSync(root, { recursive: true, force: true }); }); }); });