Files
chimera-Public/test/server/local-library-scanner.test.js
T

252 lines
12 KiB
JavaScript

'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 });
});
});
});