The Library modal showed MusicBrainz's canonical tracklist, hiding incomplete
downloads / edition or track-order mismatches, and showed nothing for albums
without an rgMbid. Now it shows the REAL files on disk.
- LibraryItem gains trackNames String[] (migration add_library_track_names):
the album's on-disk "## Title.ext" audio filenames.
- Captured at import (pipeline._import lists the final album folder) and at scan
(scan._record_owned now also refreshes trackNames on re-scan via ON CONFLICT
DO UPDATE + xmax=0 to preserve the newly-imported flag). New shared
library.list_audio_files() helper.
- /api/library exposes trackNames; the AlbumModal prefers on-disk tracks when
present (parsed "## Title" → position/title, zero network, labeled "On disk ·
N tracks"), falling back to the MusicBrainz listing otherwise. Items imported
before this column show the MB fallback until re-scanned.
worker 221 tests / 7-skip, web 153, tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AlbumModal showed the artist name as static text — no way to follow the artist
or jump to them. Adds a Follow affordance (todo #13):
- AlbumInfo gains optional `artistMbid`. When present, the modal title links the
artist name to /discover/artist/{mbid} and shows a Follow/Following button that
reflects live follow state.
- Follow state fetched via a new lightweight GET /api/artists?mbid= check
(avoids pulling the full watched list + discography); Follow reuses
POST /api/artists. Non-ok POST surfaces a toast (also chips at todo #14).
- Wired artistMbid in the three discovery contexts where following is valuable:
Discover, Discover artist preview, and Last.fm. Library/discography callers
pass none (already-followed artists) so the button stays hidden — graceful.
web 152 tests (the ?mbid= follow-state check asserted), tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reopening an artist (discography), an album (tracklist), or repeating a lookup
re-hit MusicBrainz every time with no cache and no throttle. Adds a generic
Postgres-backed read-through cache keyed on the LOGICAL operation (not the
request URL) so the worker and web app share entries.
- New ApiCache(key, json, fetchedAt) model + migration (generic name so the
Last.fm browse cache can ride the same table).
- lib/apicache.ts cached(): read-through, TTL from fetchedAt (tunable, no
migration), serve-stale-on-outage, never caches null.
- Wrap the high-level MB fns with logical keys + TTLs: discography &
tracklists & artist-name 30d, searchReleaseGroup 7d, searchArtists 1d.
Search keys normalized (trim+lowercase) to match the worker byte-for-byte.
- album-modal: a session-scoped Map<rgMbid,Track[]> so reopening is instant
with zero network (complements the DB cache).
- Test setup clears ApiCache between tests. web 147 tests, tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CoverArt loads album art from the Cover Art Archive CDN by release-group MBID
(browser-side, cached, graceful placeholder fallback). AlbumModal shows cover +
tracklist (fetched from MB) with slots for a status chip and context actions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>