Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974
This commit is contained in:
commit
c737a37f71
531 files changed
+119270
No files matched your search
@@ -0,0 +1,251 @@
|
||||
'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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user