feat(library): surface albums the scan couldn't match

The library scan silently skipped album folders with no MusicBrainz match
(or an unreadable file). Persist them so they're visible and fixable.

- New UnmatchedAlbum model (migration add_unmatched_album), keyed by path.
- scan_chunk records a skipped album as unmatched (reason "no MusicBrainz
  match" or "unreadable: <err>"), clears the row when an album later
  resolves, and prunes stale rows (a prior scan's, or a removed folder)
  when a scan completes.
- GET /api/library/unmatched lists them; DELETE ?id= dismisses one.
- /library shows a "Couldn't match" section (path + reason + Dismiss).

Worker + web tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 20:56:31 +02:00
parent 9e5d5d3af4
commit 6ca39859fa
9 changed files with 212 additions and 2 deletions
+15
View File
@@ -191,6 +191,21 @@ model ScanWorkItem {
@@index([scanId, done, artist, album])
}
// Album folders on disk that the library scan could not resolve to a MusicBrainz release
// (no match, or an unreadable/corrupt file). Surfaced on /library so the user can fix the
// metadata or add them manually instead of them being silently skipped. Keyed by path; a
// row is removed when the album later matches, and stale rows (a prior scan's, or a folder
// since removed) are pruned when a scan completes.
model UnmatchedAlbum {
id String @id @default(cuid())
artist String
album String
path String @unique
reason String
scanId String
scannedAt DateTime @default(now())
}
// Generic read-through cache for external API responses shared by web + worker (keyed on
// the LOGICAL operation, not the request URL, so both processes share entries). TTL is
// applied at read time from fetchedAt in code (no expiresAt column → tunable, no migration).