music indexer: cache-format version → full rebuild for posters/lyrics migration

Videos/tracks indexed before posters+lyrics existed were skipped by the per-album
`v` check (unchanged v → skip), so their posters/ and lyrics/ never generated —
a plain reindex couldn't fix it.

Add CACHE_VERSION (now 2). The `v` skip is only trusted when the on-disk
manifest is already at the current format; an older version forces a one-time
FULL rebuild that regenerates every album (incl. the new posters/lyrics), then
writes version:2 so subsequent builds skip normally. Verified: old-cache rebuild
regenerates the poster, next build skips (no loop).

Deploy = restart officer-music, then one reindex (a full rebuild, slower than an
incremental — one time only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 14:59:08 +00:00
co-authored by Claude Opus 4.8
parent b3a4da4b97
commit 4d3c17b2df
+10 -2
View File
@@ -34,6 +34,11 @@ const COVER_FILES = ['folder.jpg', 'cover.jpg', 'folder.png', 'cover.png'];
const DISCO_FILE = '_discography.md';
const TRACK_CONCURRENCY = 6;
const COVER_MAX_PX = 600;
// Cache-format version. Bump when the build produces NEW per-album outputs (so far: v2 added video
// posters + lyrics). A manifest written by an older CACHE_VERSION forces a one-time FULL rebuild — the
// per-album `v` skip only applies once the cache is already at the current format.
// v1 → initial (meta + cover) v2 → + posters/ + lyrics/
const CACHE_VERSION = 2;
// ── Types (meta.json matches the app's IndexMeta/IndexTrack) ──
@@ -445,7 +450,7 @@ export async function buildMusicIndex(): Promise<IndexStatus> {
console.log('[music] resync started');
const prev = await loadManifest();
const next: Manifest = { version: 1, generatedAt: status.startedAt!, albums: {} };
const next: Manifest = { version: CACHE_VERSION, generatedAt: status.startedAt!, albums: {} };
try {
await mkdir(CACHE_ROOT, { recursive: true });
@@ -584,7 +589,10 @@ async function walk(dirAbs: string, prev: Manifest, next: Manifest): Promise<voi
].filter((f): f is string => f !== null);
const outputsExist = expected.every((f) => existsSync(join(cacheDir, f)));
if (prev.albums[rel]?.v === v && outputsExist) {
// Only trust the per-album `v` skip once the cache is already at the current format — an older
// CACHE_VERSION means new outputs (posters/lyrics) may be missing, so rebuild every folder once.
const formatCurrent = prev.version === CACHE_VERSION;
if (formatCurrent && prev.albums[rel]?.v === v && outputsExist) {
status.albumsSkipped += 1;
} else {
await mkdir(cacheDir, { recursive: true });