Cache library index and refresh changed manga

This commit is contained in:
Codex committed 2026-10-08 21:53:08 +02:00
1 parent eef4ffa07c
commit 8ac27e1f3d
6 files changed
+269 -30

No files matched your search

+69 -1
View File
@@ -164,4 +164,72 @@ async function scanLibrary(root, oldCache = {}) {
return { index: { mangas, byId: new Map(mangas.map(m => [m.id, m])) }, cache };
}
module.exports = { scanLibrary, parseKey, sizeFromBuffer, imageSize };
// Book-directory renames are the publisher's atomic commit point. A shallow
// stamp pass costs one stat per book, rather than one stat/JSON read per chapter.
// In-place chapter.json edits are picked up by the scheduled deep reconciliation.
async function fileStamp(file) {
try { const s = await fsp.stat(file); return `${s.mtimeMs}:${s.size}`; }
catch (e) { if (e.code === 'ENOENT') return ''; throw e; }
}
async function libraryStamps(root) {
const stamps = {};
for (const name of (await list(root)).dirs) {
const mangaDir = path.join(root, name), books = {};
const listed = await list(mangaDir);
for (const bookName of listed.dirs.filter(parseKey)) {
const bookDir = path.join(mangaDir, bookName);
books[bookName] = [await fileStamp(bookDir), await fileStamp(path.join(bookDir, 'book.json'))];
}
stamps[name] = [await fileStamp(mangaDir), await fileStamp(path.join(mangaDir, 'manga.json')), books];
}
return stamps;
}
async function scanChangedLibrary(root, prior, oldCache, oldStamps, stamps) {
stamps = stamps || await libraryStamps(root);
const names = new Set([...Object.keys(oldStamps || {}), ...Object.keys(stamps)]);
const changed = [...names].filter(name => JSON.stringify(oldStamps?.[name]) !== JSON.stringify(stamps[name]));
if (!changed.length) return { index: prior, cache: oldCache, stamps, changed };
const byId = new Map(prior.mangas.map(m => [m.id, m]));
const cache = { ...oldCache };
for (const name of changed) {
byId.delete(name);
for (const key of Object.keys(cache)) if (key.startsWith(name + '/')) delete cache[key];
if (!Object.hasOwn(stamps, name)) continue;
const m = await scanManga(root, name, oldCache, cache);
if (m.chapters.length) byId.set(name, m);
}
const mangas = [...byId.values()].sort((a, b) => cmp(a.title, b.title));
return { index: { mangas, byId: new Map(mangas.map(m => [m.id, m])) }, cache, stamps, changed };
}
// Store each chapter only once. Maps and book chapter references are rebuilt
// from the snapshot, which is a disposable cache rather than library authority.
function snapshotIndex(index) {
return index.mangas.map(m => ({ ...m, chapterById: undefined,
books: m.books.map(b => ({ ...b, chapters: b.chapters.map(ch => ch.id) })) }));
}
function restoreIndex(mangas) {
if (!Array.isArray(mangas)) throw new Error('Invalid library index snapshot');
const byId = new Map();
for (const m of mangas) {
if (!m || typeof m.id !== 'string' || !Array.isArray(m.books) || !Array.isArray(m.chapters) || byId.has(m.id))
throw new Error('Invalid manga in library index snapshot');
m.chapterById = new Map();
for (const ch of m.chapters) {
if (!ch || typeof ch.id !== 'string' || !Array.isArray(ch.versions) || ch.versions.some(v => !Array.isArray(v.pages)))
throw new Error('Invalid chapter in library index snapshot');
m.chapterById.set(ch.id, ch);
}
for (const book of m.books) {
if (!Array.isArray(book.chapters) || book.chapters.some(id => !m.chapterById.has(id)))
throw new Error('Invalid book in library index snapshot');
book.chapters = book.chapters.map(id => m.chapterById.get(id));
}
byId.set(m.id, m);
}
return { mangas, byId };
}
module.exports = { scanLibrary, scanChangedLibrary, libraryStamps, snapshotIndex, restoreIndex,
parseKey, sizeFromBuffer, imageSize };