music: report what the nightly full actually found
The nightly full rebuilds every album by definition, so "it completed" says nothing about whether it is still needed. Diff the from-scratch build against the index that was already live, just before the slot swaps in, and log the delta: albums added, removed, and entries whose contents changed. A run reporting NONE did no useful work. A string of those is the evidence for retiring the nightly; a delta that keeps coming back names the albums to go and look at instead of guessing. Entries are compared field by field rather than by JSON.stringify: the optional fields are spread conditionally, so two entries built by the same code can serialise with different key order, and stringify would report every album as changed every night. A cache-format upgrade is labelled, since that rebuilds everything legitimately and would otherwise read as total rot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -645,6 +645,57 @@ export async function reindexNow(): Promise<IndexStatus> {
|
||||
});
|
||||
}
|
||||
|
||||
type ManifestDelta = { added: string[]; removed: string[]; changed: string[] };
|
||||
|
||||
// Compare a from-scratch build against the index that was already live. This exists to answer one
|
||||
// question with evidence rather than opinion: is the nightly full still FINDING anything, or is it
|
||||
// spending 40 minutes of disk rebuilding what the incremental had already got right?
|
||||
//
|
||||
// A run that reports no differences did no useful work. A run of those is the case for retiring the
|
||||
// nightly; a delta that keeps reappearing names the albums to go and look at.
|
||||
//
|
||||
// Compared field-by-field rather than by JSON.stringify, because two entries built by the same code
|
||||
// can serialise with different key order (the optional fields are spread conditionally) and that would
|
||||
// report drift where there is none.
|
||||
function diffManifest(prev: Manifest, next: Manifest): ManifestDelta {
|
||||
const canon = (e: ManifestEntry) =>
|
||||
[e.v, e.cover, e.tracks, e.videos ?? 0, e.images ?? 0, e.disco ?? false, e.lyrics ?? 0, e.posters ?? 0].join('|');
|
||||
|
||||
const added: string[] = [];
|
||||
const changed: string[] = [];
|
||||
for (const [rel, entry] of Object.entries(next.albums)) {
|
||||
const before = prev.albums[rel];
|
||||
if (!before) added.push(rel);
|
||||
else if (canon(before) !== canon(entry)) changed.push(rel);
|
||||
}
|
||||
const removed = Object.keys(prev.albums).filter((rel) => !next.albums[rel]);
|
||||
return { added, removed, changed };
|
||||
}
|
||||
|
||||
function logManifestDelta(prev: Manifest, next: Manifest): void {
|
||||
// A cache-format upgrade rebuilds every album by definition, so the delta is expected and says
|
||||
// nothing about drift. Label it rather than let it read as 6k albums of rot.
|
||||
if (prev.version !== next.version) {
|
||||
console.log(`[music] full reindex: cache format v${prev.version} → v${next.version}, delta below is the upgrade itself`);
|
||||
}
|
||||
|
||||
const { added, removed, changed } = diffManifest(prev, next);
|
||||
const total = added.length + removed.length + changed.length;
|
||||
if (total === 0) {
|
||||
console.log('[music] full reindex delta: NONE — the incremental index was already identical');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[music] full reindex delta: +${added.length} added, -${removed.length} removed, ~${changed.length} changed`);
|
||||
const sample = (label: string, rels: string[]) => {
|
||||
for (const rel of rels.slice(0, 5)) console.log(`[music] ${label} ${rel || '.'}`);
|
||||
if (rels.length > 5) console.log(`[music] ${label} …and ${rels.length - 5} more`);
|
||||
};
|
||||
sample('+', added);
|
||||
sample('-', removed);
|
||||
sample('~', changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full, from scratch: build a complete index into a FRESH slot without touching the live one, then swap
|
||||
* it in atomically only on success — a failed rebuild leaves the live index untouched. For the nightly cron.
|
||||
@@ -652,6 +703,8 @@ export async function reindexNow(): Promise<IndexStatus> {
|
||||
export function reindexFull(): Promise<IndexStatus> {
|
||||
return withIndexLock(async () => {
|
||||
await ensureCacheSetup();
|
||||
// Read the live index BEFORE the swap — it is what the delta below is measured against.
|
||||
const live = await loadManifest();
|
||||
const slot = slotPath(String(Date.now()));
|
||||
await rm(slot, { recursive: true, force: true }).catch(() => {});
|
||||
// Empty prev ⇒ everything rebuilds into the fresh slot.
|
||||
@@ -660,6 +713,7 @@ export function reindexFull(): Promise<IndexStatus> {
|
||||
await rm(slot, { recursive: true, force: true }).catch(() => {});
|
||||
console.error('[music] full reindex failed — live index left untouched');
|
||||
} else {
|
||||
logManifestDelta(live, next);
|
||||
await activateSlot(slot);
|
||||
manifest = next;
|
||||
manifestLoaded = true;
|
||||
|
||||
Reference in New Issue
Block a user