feat(server/m7-cover-sources): album enricher uses provider chain

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>
This commit is contained in:
2026-05-06 12:52:57 -04:00
parent 392b8aa12a
commit 385eb3b163
9 changed files with 521 additions and 381 deletions
+17 -20
View File
@@ -11,21 +11,18 @@ import (
)
// newMBCAAProviderForTest registers a fresh mbcaaProvider pointing at
// a test server, mirroring the pattern from fetcher_test.go but
// exercising the interface-side methods.
// a test server.
func newMBCAAProviderForTest(t *testing.T, mockURL string) *mbcaaProvider {
t.Helper()
resetRegistryForTests()
t.Cleanup(resetRegistryForTests)
p := &mbcaaProvider{
fetcher: NewFetcher(FetcherConfig{
BaseURL: mockURL,
UserAgent: "Minstrel/test (https://example.com)",
MinPeriod: 0,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
}),
}
p := newMBCAAProvider(FetcherConfig{
BaseURL: mockURL,
UserAgent: "Minstrel/test (https://example.com)",
MinPeriod: 0,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
})
Register(p)
p.enabled.Store(true)
return p
@@ -34,7 +31,7 @@ func newMBCAAProviderForTest(t *testing.T, mockURL string) *mbcaaProvider {
func TestMBCAAProvider_ID(t *testing.T) {
resetRegistryForTests()
defer resetRegistryForTests()
p := &mbcaaProvider{fetcher: NewFetcher(FetcherConfig{})}
p := newMBCAAProvider(FetcherConfig{})
if p.ID() != "mbcaa" {
t.Errorf("ID = %q, want mbcaa", p.ID())
}
@@ -102,7 +99,7 @@ func TestMBCAAProvider_FetchAlbumCover_DisabledReturnsNotFound(t *testing.T) {
func TestMBCAAProvider_Configure_TogglesEnabled(t *testing.T) {
resetRegistryForTests()
defer resetRegistryForTests()
p := &mbcaaProvider{fetcher: NewFetcher(FetcherConfig{})}
p := newMBCAAProvider(FetcherConfig{})
if err := p.Configure(ProviderSettings{Enabled: true}); err != nil {
t.Fatalf("Configure: %v", err)
@@ -143,12 +140,12 @@ func TestMBCAAProvider_TestConnection_PropagatesTransient(t *testing.T) {
}
}
func TestNewMBCAAProviderFromConfig_ReplacesRegisteredFetcher(t *testing.T) {
func TestNewMBCAAProviderFromConfig_ReplacesRegisteredConfig(t *testing.T) {
resetRegistryForTests()
defer resetRegistryForTests()
// Pre-register with default config (mimics init() behaviour).
Register(&mbcaaProvider{fetcher: NewFetcher(FetcherConfig{})})
Register(newMBCAAProvider(FetcherConfig{}))
cfg := FetcherConfig{
UserAgent: "Minstrel/test-replaced (x)",
@@ -161,11 +158,11 @@ func TestNewMBCAAProviderFromConfig_ReplacesRegisteredFetcher(t *testing.T) {
t.Fatalf("ProviderByID: %v", err)
}
mp := p.(*mbcaaProvider)
if mp.fetcher == nil {
t.Fatal("fetcher is nil after NewMBCAAProviderFromConfig")
// Verify the cfg was replaced on the registered instance.
mp.mu.Lock()
gotUA := mp.cfg.UserAgent
mp.mu.Unlock()
if gotUA != "Minstrel/test-replaced (x)" {
t.Errorf("cfg.UserAgent = %q, want %q", gotUA, "Minstrel/test-replaced (x)")
}
// We can't peek the fetcher's UserAgent directly (it's an unexported
// field on FetcherConfig held inside Fetcher), but the side effect of
// ProviderByID returning the same registered instance with a swapped
// fetcher is what we're verifying here.
}