Persists a stage tally to scan_runs at a bounded cadence — every
flushEvery items OR every flushPeriod elapsed, whichever fires
first. Caller supplies marshal + persist closures so each stage owns
its own tally type. Tick swallows mid-stage persist errors silently
(observability bug, not scan failure); Flush propagates errors so
the orchestrator's captureErr can surface them.
Tests cover: count-based flush, time-based flush, Flush always
persists, Tick swallows errors and keeps firing, Flush propagates
errors, counters reset after each flush.
Sweep round 4: 5 more httptest handlers in provider_theaudiodb_test.go
where r is unused. Lint flagged 3 (lines 89/103/118); also fixing
2 more (TestConnection_SuccessOnEmptyAlbum, FailureOn401) that lint
would catch on the next run. The 4 handlers that DO use r.URL.Path
or r.Host are left as-is.
revive's unused-parameter rule flagged 7 test handler/method args:
- provider_test.go: fakeProvider.Configure(s) and
fakeAlbumProvider.FetchAlbumCover(ctx, mbid) — interface
conformance dummies that don't use their args.
- provider_mbcaa_test.go: 4 httptest handlers that ignore one or
both of (w, r).
- provider_theaudiodb_test.go: the disabled-provider negative
test's httptest handler.
All 7 renamed to _ per revive's convention.
T11 added *coverart.SettingsService to Mount() between the
coverArtBackfillCap (int) and the *library.Scanner pointer. The
TestRoutesRegisteredInMount call site in library_test.go missed
the new arg. Insert h.coverSettings at the right position.
Three issues from the previous push:
1. internal/api/admin_covers_test.go used t.Context() (Go 1.24+) but
go.mod declares go 1.23.0. Replace with context.Background() in
the testHandlersWithCovers helper; add "context" import.
2. internal/server/server_test.go's New() call missed the
*coverart.SettingsService argument added in T11. The new param
sits between the cover-art-backfill cap (int) and the *library.Scanner
pointer in the function signature; the test was using 12 args
instead of 13. Insert nil at the right position in all six call sites.
3. integrations.test.ts had 11 failing tests after T13 added the
cover-art-providers section. Both panels render "Save changes",
"Test connection", and "API key" controls, so the existing
Lidarr tests' role/text queries matched multiple elements. Add
helper functions (lidarrSection, coverProvidersSection,
providerCard) and scope every relevant query with within() so
each panel's tests interact only with their own controls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three failures from the prior push:
1. internal/api/library.go and 3 other callers of q.GetArtistByID
broke because T2 modified the query to use an explicit column
list, making sqlc generate GetArtistByIDRow instead of returning
dbq.Artist. dbq.Artist already has every column added by
migration 0018 (artist_thumb_path / artist_fanart_path /
artist_art_source / artist_art_sources_version), so the explicit
column list was unnecessary work. Revert the SELECT to SELECT *,
regenerate sqlc, GetArtistByIDRow disappears, all callers compile
again.
2. internal/tracks/service_test.go missed the dataDir parameter
added to NewService in T9. Add "" as the 4th arg to every test
call site (tests don't exercise the artist-art cleanup path; the
empty dataDir is fine).
3. integrations.test.ts:356's mock.calls.filter callback had a
too-narrow type annotation that svelte-check rejected. Relax to
any[] to match what mock.calls actually is.
Adds a "Cover art providers" section to the existing /admin/integrations
page with one card per registered provider (currently MBCAA + TheAudioDB).
Each card has:
- Provider name + capability badges (album cover · artist thumb · artist fanart)
- Status dot (green if enabled; muted if disabled)
- Enabled toggle + API key text field (only when requires_api_key)
- Save button (explicit save matches existing Lidarr card pattern)
- Test connection button (when testable) with inline ok/fail indicator
- Footer help line for TheAudioDB linking to the free key application
Save invalidates qk.coverProviders() always and qk.coverage() when
the PATCH response indicates version_bumped: true (so the coverage
gauge re-queries to show settled→with_art transitions on the next
scan).
API key field uses type=password with a placeholder that clarifies
the test-key fallback behaviour — never echoes back the stored key
(api_key_set bool from the GET response controls the "✓ Set"
indicator).
Local edit state is keyed by provider_id and survives query refetches
so in-flight edits aren't clobbered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds CoverProvider type + capability union, getCoverProviders /
updateCoverProvider / testCoverProvider against the three new admin
endpoints, plus createCoverProvidersQuery (60s staleTime; refetches
on focus + after mutations rather than polling — settings rarely
change in operator time).
New api.patch helper added to client.ts (the existing helpers were
get/post/put/del; PATCH was missing).
New qk.coverProviders() key follows the existing tuple pattern.
api_key_set: bool replaces echoing the credential. UpdateCoverProviderPatch
uses optional fields so the operator can change just enabled, just
key, or both atomically.
Vitest covers GET path, PATCH path with body, version_bumped
response, POST /test path with empty body, ok=false error response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three endpoints for the admin Settings UI:
- GET /api/admin/cover-sources — lists registered providers with
current settings + capability badges + sources_version
- PATCH /api/admin/cover-sources/{provider_id} — updates enabled
and/or api_key; returns version_bumped: true only when the
enabled set changed
- POST /api/admin/cover-sources/{provider_id}/test — invokes
TestableProvider.TestConnection; returns ok:bool + duration_ms +
error string. Returns 200 in both success and failure cases (the
operation completed; the test result is data, not an error).
api_key is never echoed in GET — replaced with api_key_set: bool
to follow the standard credential-display convention. PATCH
api_key: "" clears; omitting the field leaves it unchanged. PATCH
on an unknown provider_id returns 404.
All under existing RequireAdmin middleware. Routes registered
alongside the library/coverage endpoint in the /api/admin/ tree.
handlers struct gains a coverSettings *coverart.SettingsService
field; server.New + cmd/minstrel/main.go plumb the existing
coverSettings instance through.
coverart.ResetRegistryForTests() exported wrapper added so api
package tests can reset the registry without importing internals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an artist-art-enrich stage after cover-enrich in RunScan. The
stage drains EnrichArtistBatch with a configurable cap and the
operator's data_dir, persists processed/succeeded/failed tallies to
the scan_runs.artist_art_enrich jsonb column added in migration
0018.
The admin /admin Library Scan card grid expands from 3 to 4 columns
with the new "Artist art" card mirroring the cover-enrichment one
(Processed / Succeeded / Failed). The TS ScanStatus type gains an
optional artist_art_enrich field. The Go scanStatusResp gains the
matching ArtistArtEnrich json.RawMessage field.
cmd/minstrel/main.go (and admin scan-trigger handler) wire the new
RunScanConfig fields: ArtistEnrichCap reuses cfg.Library.CoverArtBackfillCap
for now (can be split if separate budgets are useful), DataDir is
cfg.Storage.DataDir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parallels EnrichAlbum but with no sidecar layer (artists have no
per-artist directory in a music library). Iterates
EnabledArtistProviders, writes thumb.jpg + fanart.jpg to
<data_dir>/artist-art/<artist_id>/, stamps artist_art_source +
artist_art_sources_version atomically.
Atomicity: provider returns whichever images it has — partial
success (thumb-only or fanart-only) is persisted with whichever
paths landed, source still stamped. ErrNotFound from every enabled
provider settles the row to 'none' with the current version stamp.
Any ErrTransient leaves the row NULL for next-pass retry.
CleanupArtistArt removes <data_dir>/artist-art/<artist_id>/ on
artist delete; idempotent on missing dir. Hooked into the artist-
delete cascade in tracks/service.go after the transaction commits;
non-fatal on failure (logs but doesn't fail the delete).
EnrichArtistBatch drains rows from ListArtistsMissingArt; the
orchestrator's 4th stage (added in T10) calls this. Tests cover
the eligibility table, atomicity (thumb-only / fanart-only),
all-404-settles, ErrTransient-leaves-NULL, filesystem layout, and
CleanupArtistArt idempotency.
tracks.Service gains a dataDir field; tracks.NewService takes it
as a new parameter. All three call sites updated (server/server.go,
api/auth_test.go, api/admin_tracks_test.go). The dataDir flows from
cfg.Storage.DataDir → server.New → s.DataDir → tracks.NewService
without any change to cmd/minstrel/main.go.
Rewires EnrichAlbum to iterate EnabledAlbumProviders from the
SettingsService instead of a single hardcoded MBCAA fetcher. Sidecar
layer is unchanged (always tried first). Per-row eligibility check
honors cover_art_sources_version: terminal 'found' values skip;
'none' flips to NULL via ClearAlbumCoverNone and proceeds (the DB-
level ListAlbumsMissingCover query guards the version check for batch
callers; RetryAlbum clears via ClearAlbumCover first); NULL is eligible.
The provider chain uses an inline allWere404 accumulator to settle
the row to 'none' only when every provider returned ErrNotFound.
Any provider returning ErrTransient leaves the row NULL for next-
pass retry — even if other providers returned ErrNotFound — since
the transient call's MBID might succeed on a future attempt.
Boot wiring (cmd/minstrel/main.go) replaces the
NewFetcher+NewEnricher(fetcher, mbcaaOn) shape with
NewMBCAAProviderFromConfig (swaps in production User-Agent on the
registered provider) + NewSettingsService + NewEnricher(settings).
The old cfg.Library.CoverArtFromMBCAA flag is no-op'd; DB-backed
settings replace it. Field stays in the config struct for backward
compat with deployed config files.
Consolidates fetcher.go's HTTP logic into provider_mbcaa.go (no more
wrapper indirection): mbcaaProvider now holds cfg/mu/lastCall/client
directly. Deletes fetcher.go and fetcher_test.go. ErrNotFound and
ErrTransient move into provider.go alongside the other sentinels.
Updates admin_covers_test.go and provider_mbcaa_test.go to the new
constructor shapes. Adds multi-provider-chain, transient-leaves-null,
and stale-none-flips-and-retries test cases in enricher_test.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reconciles the compile-time provider registry with the
cover_art_provider_settings table at boot, and on every admin
update. Calls Configure() on each provider with the row data so
runtime config (enabled, api_key) propagates immediately.
Owns the cover_art_sources_meta.current_version stamp.
UpdateProvider returns versionBumped=true only when the change
altered the *enabled set* of providers — key rotations don't bump
version (the recheck trigger is for "we have a new source to ask,"
not config rotations).
Capability filtering: EnabledAlbumProviders / EnabledArtistProviders
return only registered providers that (a) are enabled per DB and
(b) implement the requested capability interface. Snapshots —
callers don't mutate.
ListProviderInfo builds the wire-shaped slice the admin GET handler
returns. APIKeySet bool replaces echoing the key (credential never
leaves the server).
TestProvider dispatches to TestableProvider.TestConnection on the
named provider, ErrNotTestable if the provider doesn't implement
the capability, ErrProviderNotFound if not registered.
UpdateProviderSettingsParams uses the sqlc-generated field names:
Enabled bool (not *bool — the COALESCE is handled by passing the
cached current value when patch.Enabled is nil), Column3 bool
(apiKeyChanged flag), ApiKey *string (the api_key value — sqlc
named this from the column, not Column4 as the task spec assumed).
Tests gated on MINSTREL_TEST_DATABASE_URL: boot-inserts-defaults,
flip-enabled-bumps-version, key-only-doesn't-bump, clear-key,
TestProvider routing for testable / non-testable / missing,
ListProviderInfo capability badges. Reuses newPool / discardLogger
and fakeProvider / fakeAlbumProvider from enricher_test.go and
provider_test.go (same package).
New album-cover and artist-art source implementing
AlbumCoverProvider, ArtistArtProvider, and TestableProvider. Hits
the documented /album-mb.php and /artist-mb.php JSON endpoints by
MBID, then GETs the returned image URLs (CDN, key-less). 2 req/sec
rate limit per the upstream's documented free/test-key tier;
mutex+lastCall serialises ALL HTTP calls regardless of method so
JSON metadata fetches and image GETs share the same budget.
429 / 5xx triggers exponential backoff with jitter (250ms × 2^attempt
± 20%) up to 3 retries before surfacing ErrTransient. 401 / 403
surface as ErrTransient (auth issues are config errors, not "this
album has no art" — the row stays NULL across passes so the operator
sees pending count not decreasing as the diagnostic signal).
Artist art is atomic at the JSON metadata step (one round-trip
yields both URLs); the two image GETs are independent — partial
success returns whatever bytes landed and ErrNotFound only if
neither URL is present in the response.
Configure() falls back to the documented public test key (2) when
the operator's APIKey is empty, per the no-coercive-settings
principle. Test connection hits a stable popular release MBID
(Pink Floyd, "The Dark Side of the Moon").
theAudioDBBaseURL is a var (not const) so tests can override it.
Adapts the existing Fetcher-based MBCAA client to the new Provider
abstraction. mbcaaProvider embeds *Fetcher and implements
Provider + AlbumCoverProvider + TestableProvider. init() registers a
default-config instance; NewMBCAAProviderFromConfig swaps in
production config (User-Agent, MinPeriod) at boot.
Wrapper approach (rather than reimplementing the HTTP logic) keeps
the package compiling during the T4–T8 window: enricher.go and
main.go still reference the legacy Fetcher type / NewFetcher
constructor / Fetch method, all of which remain unchanged in
fetcher.go. A-T8 will consolidate the HTTP logic into this file and
delete fetcher.go after the enricher rewrite removes the last
legacy call sites.
TestConnection hits a hardcoded popular release MBID (Beatles
"Abbey Road" UK release) and treats both 200 and 404 as "connection
works" — MBCAA needs no auth, so only 5xx / network failures
surface. Tests cover ID/DisplayName/Capability metadata, success
path, 404→ErrNotFound, disabled-returns-ErrNotFound, Configure
toggle, TestConnection variants, and NewMBCAAProviderFromConfig
swap.
Defines the abstraction every cover-art source will implement:
- Provider — base interface (ID, DisplayName, RequiresAPIKey,
DefaultEnabled, Configure).
- AlbumCoverProvider — opt-in capability for fetching album covers
by MBID.
- ArtistArtProvider — opt-in capability for fetching artist thumb +
fanart atomically by MBID.
- TestableProvider — opt-in capability for the admin Test-Connection
button.
Sentinel ErrNotTestable / ErrProviderNotFound. ErrNotFound and
ErrTransient stay in fetcher.go for now; they move into provider.go
in the next commit when fetcher.go is deleted as part of the MBCAA
refactor.
Compile-time registry: providers register from init() in their own
file. Duplicate IDs panic at startup (compiled-in registration is a
build-time bug, not a runtime concern).
Tests cover: Register adds, duplicate panics, ProviderByID success
+ ErrProviderNotFound, capability-interface filtering via Go type
assertion.
- Extends GetAlbumCoverageRollup's with_art FILTER to include
'theaudiodb' so F's coverage gauge surfaces TheAudioDB-found rows
as with_art rather than silently undercounting.
- Modifies ListAlbumsMissingCover to take the current sources_version
as a parameter; eligibility now includes stale-'none' rows.
- Adds SetAlbumCoverWithVersion (atomic write of path + source +
version stamp) and the parallel SetArtistArtWithVersion +
ClearArtistArtNone + ListArtistsMissingArt for the artist-art
enricher.
- New coverart_settings.sql with ListProviderSettings,
UpsertProviderSettings (boot reconciliation, preserves operator
values on conflict), UpdateProviderSettings (admin PATCH; tri-
state api_key handling), GetCurrentSourcesVersion,
BumpSourcesVersion (RETURNING the new value).
Adds the schema for the pluggable cover-art provider abstraction:
- Extends albums.cover_art_source CHECK to accept 'theaudiodb' (the
constraint from migration 0016 only allowed embedded/sidecar/mbcaa/
none, which would block writes from the new provider).
- Adds artist_thumb_path / artist_fanart_path / artist_art_source
on artists, with a parallel CHECK constraint and source index.
- Adds *_sources_version stamps on both albums and artists for the
per-row recheck eligibility logic. Default 0 so every existing row
becomes stale relative to the seeded current_version=1, retrying
through the new chain on first scan after migrate.
- New cover_art_provider_settings table (per-provider enabled / api_key
/ display_order) and cover_art_sources_meta singleton holding
current_version. Seeds both v1 providers (mbcaa + theaudiodb) as
enabled.
- New artist_art_enrich jsonb column on scan_runs for the 4th
scan-orchestrator stage tally.
The seed leaves theaudiodb.api_key NULL; the application supplies the
upstream's documented test key (2) as the default when the column is
NULL, per the no-coercive-settings principle.
Adds a library-wide cover-art coverage gauge inline to the right of
the "Refetch missing covers" button. Three buckets: with_art ·
pending · settled. Native HTML title= tooltip on "pending" surfaces
the pending_no_mbid sub-count when > 0, telling the operator how
many "pending" rows are blocked on missing MBID and won't be moved
by another scan.
Run-scan and Refetch-missing handlers extend their TanStack
invalidation to also clear qk.coverage(), so the gauge ticks
immediately after the operator clicks instead of waiting up to 3s
for the next refetch interval.
flex-wrap on the row so the gauge drops below the button on narrow
viewports instead of overflowing.
The existing admin page vitest mock for $lib/api/admin gains a
createCoverageQuery stub returning data: undefined so the new
import doesn't break the existing page tests (which don't touch
the gauge).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Workspace flatten brought these operator-scoped state dirs (Claude
Code per-machine settings and remember-skill memory artifacts) into
the repo's working tree. Neither belongs in git — the canonical
project memory lives under ~/.claude/projects/ outside the repo.
Adds CoverageRollup type, getCoverageRollup() against
/api/admin/library/coverage, and createCoverageQuery() with the same
3s/30s pacing used by scan-status. New qk.coverage() key follows the
existing zero-arg tuple pattern. Vitest covers the GET path and the
empty-library zero-state plus the with_art + pending + settled = total
invariant.
Code-review polish on admin_coverage_test.go:
- Replace inline artist-seed SQL with the seedArtist helper used
across the rest of the api test suite (admin_covers_test.go,
likes_test.go, me_history_test.go, admin_quarantine_test.go).
- Rename the cover_art_source pointer locals from none/sidecar/mbcaa
to sourceNone/sourceSidecar/sourceMbcaa so they don't read like
zero-value identifiers.
New admin handler returns the library-wide cover-art coverage rollup
{total, with_art, pending, settled, pending_no_mbid} for the admin
dashboard gauge. Always 200; zeros on empty library. Same RequireAdmin
middleware as /api/admin/scan/status. Lives under a new /library/
admin sub-namespace, parallel to /scan/ (per-run state) and /covers/
(actions).
Tests gated on MINSTREL_TEST_DATABASE_URL: empty library returns all
zeros, mixed rows produce correct bucket math (verifying the
with_art + pending + settled = total invariant), non-admin returns 403.
Code-review polish on GetAlbumCoverageRollup's comment block:
- Records the invariant that with_art + pending + settled = total
(and that pending_no_mbid is a subset of pending, not a fourth
bucket).
- Notes that the IN ('sidecar','embedded','mbcaa') list must stay
in sync with the cover_art_source CHECK constraint in migration
0016 — without this note a future source addition could silently
undercount with_art.
FILTER-aggregate count over albums.cover_art_source and albums.mbid
returning total, with_art, pending, settled, pending_no_mbid in a
single round-trip. Sub-millisecond on realistic libraries; no index
needed for v1. pending_no_mbid is a subset of pending — the UI
tooltip uses it to show how many "pending" rows are blocked on
missing MBID and won't be moved by another scan.
Also bumps the Makefile sqlc pin from 1.27.0 to 1.31.1 to match the
version that produced all existing committed dbq/*.go files. The
prior pin was stale; running make generate against 1.27.0 silently
rolled the codegen back (cosmetic version headers on most files
plus a real codegen shape change in DeleteArtistIfEmpty).
Forward-fix from running the prior two commits in production. Backfill
on a real library exposed two further issues:
(1) gofmt -s flagged the column-aligned stubMeta methods in
mbids_test.go. Switched to the gofmt-canonical single-space form.
(2) Many albums failed to heal with SQLSTATE 23505 (unique constraint
albums_mbid_unique). Cause: the operator's library has duplicate
album rows in the DB — same MusicBrainz release, split into
multiple rows by the pre-MBID scanner because of subtle title or
artist disagreements between files. When backfill now correctly
extracts the MBID, two rows want to claim the same one and the
partial unique index rejects the second.
The conflict is correct DB behavior — we shouldn't have two rows
with the same MBID — but it's not a write failure to alarm the
operator about. Detect 23505 specifically:
- downgrade the log line from Warn to Info
- track these in a new BackfillMBIDsResult.Duplicates counter
(separate from Skipped, which retains its "no MBID in tag"
meaning)
- leave the duplicate row's mbid NULL; merging duplicates is a
separate (future) operator workflow
Same handling threaded through the scanner heal path so a regular
rescan doesn't generate the noise either.
JSON tally + admin UI gain a "Duplicates" line so the operator
can see how many duplicate rows their library carries.
Follow-up scope (separate task): a duplicate-album merge UX that
reparents the duplicate's tracks to the canonical row and deletes
the orphaned album row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 947f944. With the dhowden/tag/mbz extractor now correctly
matching ID3v2 TXXX frames, mbid backfill started rejecting every row
with "invalid byte sequence for encoding UTF8: 0x00 (SQLSTATE 22021)".
Cause: dhowden's readTextWithDescrFrame (id3v2frames.go:454) — the path
that decodes TXXX text — does NOT strip the trailing/embedded NUL bytes
that ID3v2.4 uses as frame terminator and multi-value separator. Unlike
readTFrame, which does (line 313). So a TXXX:MusicBrainz Album Id of
"abc-123\x00" comes back to us verbatim, and Postgres refuses to store
NULs in text columns.
cleanMBID splits on NUL and returns the first non-empty segment, which
also handles the multi-value case (collaboration artist IDs are
NUL-separated; Minstrel uses the primary for MBCAA, so first wins).
Tests cover the trailing-NUL, multi-value, and all-NUL paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hand-rolled extractor in internal/library/mbids.go looked up keys that
dhowden/tag never produces, for every supported audio format:
Vorbis (FLAC/OGG): dhowden lowercases keys at parse time (vorbis.go:59),
we looked up MUSICBRAINZ_ALBUMID. Always missed.
ID3v2 (MP3): dhowden stores TXXX frames under bare TXXX/TXXX_N keys
with *tag.Comm values; the Picard tag name is on the value's
Description field, not part of the key. We looked up
TXXX:MusicBrainz Album Id. Always missed.
MP4 (M4A): dhowden stores freeform iTunes atoms under their bare
sub-name. We looked up ----:com.apple.iTunes:MusicBrainz Album Id.
Always missed.
Net effect: every album in the m7-380 boot backfill reported
processed=N, healed=0, skipped=N. The m7-379 enricher gate then
short-circuited NULL on every album, so MBCAA was never queried.
Symptom: "all albums skipped" during cover enrichment.
The pre-existing unit tests passed because they hand-built Raw() maps
with the intended-but-incorrect keys, never round-tripping through
dhowden's parser.
Replace the hand-rolled extractor with dhowden's own mbz sub-package,
which knows the per-format Raw() conventions and uses lowercase
canonical keys (mbz.Album, mbz.Artist). Update both call sites to pass
the full tag.Metadata instead of just Raw(). Rewrite the test with a
stubMeta implementing tag.Metadata, asserting against the actual
per-format Raw() shape, plus a regression guard that pins "uppercase
Vorbis keys must not match" so the v0 bug can't sneak back.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds getScanStatus/triggerScan helpers, createScanStatusQuery factory
(3s poll), qk.scanStatus(), and a Library Scan section on the admin
overview page with per-stage tallies and inline Run scan button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GET /api/admin/scan/status and POST /api/admin/scan/run under the
existing RequireAdmin middleware block; plumbs *library.Scanner and
library.RunScanConfig through api.Mount, server.New, and main.go so the
manual trigger reuses the same RunScan orchestrator as startup scans.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds internal/library/scanrun.go with RunScan, which creates a scan_runs
row and sequences file-walk → MBID backfill → cover enrich, persisting
per-stage tallies as jsonb. Extends coverart.EnrichBatch to return
(processed, succeeded, failed, err) so the orchestrator can classify
outcomes. Replaces main.go's two separate boot goroutines with a single
RunScan call; passes nil scanner in the no-startup-scan branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a boot-time goroutine that walks albums with NULL mbid, re-reads
tags from one track per album via dhowden/tag, and persists album + artist
MBIDs. Healed albums also get their cover_art_source='none' cleared so the
enricher's next batch retries the MBCAA fetch. Caps at 5000 albums per
boot to avoid stalling startup on huge libraries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>