188 lines
6.8 KiB
Go
188 lines
6.8 KiB
Go
package coverart
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
|
)
|
|
|
|
// Provider is the base shape every cover-art source implements.
|
|
// Providers register themselves at init() time via Register(). The
|
|
// enricher reads the current per-provider settings and dispatches to
|
|
// the capability methods below.
|
|
type Provider interface {
|
|
// ID is a stable lowercase token persisted in DB columns
|
|
// (cover_art_source / artist_art_source) and the
|
|
// cover_art_provider_settings.provider_id key, plus referenced
|
|
// in the operator-facing UI.
|
|
ID() string
|
|
|
|
// DisplayName is the human-readable label shown in the admin
|
|
// Settings card.
|
|
DisplayName() string
|
|
|
|
// RequiresAPIKey reports whether the provider needs a key to
|
|
// function. True for theaudiodb / fanart.tv; false for mbcaa.
|
|
// The admin UI uses this to show or hide the api_key text field;
|
|
// it does NOT gate the enabled toggle (per the no-coercive-
|
|
// settings principle, providers with usable defaults — such as
|
|
// TheAudioDB's public test key — ship working).
|
|
RequiresAPIKey() bool
|
|
|
|
// DefaultEnabled is whether a freshly-installed Minstrel ships
|
|
// with this provider on. Both v1 providers return true.
|
|
DefaultEnabled() bool
|
|
|
|
// Configure receives the operator's settings (api_key, enabled
|
|
// flag) and applies them. Called at boot during reconciliation
|
|
// and on every admin PATCH. Implementations must be idempotent
|
|
// and goroutine-safe (the enricher may be reading concurrently).
|
|
Configure(ProviderSettings) error
|
|
}
|
|
|
|
// ProviderSettings is the runtime config a provider receives when
|
|
// Configure() is called. Providers may interpret an empty APIKey
|
|
// however they wish — TheAudioDB falls back to its public test key,
|
|
// MBCAA ignores the field entirely.
|
|
type ProviderSettings struct {
|
|
Enabled bool
|
|
APIKey string
|
|
}
|
|
|
|
// ArtistRef is the lookup key passed to ArtistArtProvider.FetchArtistArt.
|
|
// MBID is preferred when present; Name is the always-populated fallback
|
|
// for providers that support name-based search (Deezer, Last.fm).
|
|
// MBID-only providers (TheAudioDB) return ErrNotFound when MBID is empty.
|
|
type ArtistRef struct {
|
|
MBID string
|
|
Name string
|
|
}
|
|
|
|
// AlbumRef is the lookup key passed to AlbumCoverProvider.FetchAlbumCover.
|
|
// Same MBID-preferred semantics as ArtistRef; ArtistName + AlbumTitle
|
|
// support name-based providers.
|
|
type AlbumRef struct {
|
|
MBID string
|
|
ArtistName string
|
|
AlbumTitle string
|
|
}
|
|
|
|
// AlbumCoverProvider is an opt-in capability: providers implementing
|
|
// it can fetch album cover art. MBID-only providers use ref.MBID and
|
|
// return ErrNotFound when it is empty; name-based providers (Deezer,
|
|
// Last.fm) fall back to ref.ArtistName + ref.AlbumTitle.
|
|
type AlbumCoverProvider interface {
|
|
Provider
|
|
// FetchAlbumCover returns the image bytes for the given album ref,
|
|
// or ErrNotFound (terminal — no art available) /
|
|
// ErrTransient (retry-eligible failure).
|
|
FetchAlbumCover(ctx context.Context, ref AlbumRef) ([]byte, error)
|
|
}
|
|
|
|
// ArtistArtProvider is an opt-in capability: providers implementing
|
|
// it can fetch artist images. Returns thumb + fanart atomically at
|
|
// the JSON metadata step (one round-trip yields both URLs); the two
|
|
// image GETs are independent — partial success returns whatever bytes
|
|
// landed and the source is still stamped, so the enricher persists
|
|
// asymmetric results (thumb-only or fanart-only) gracefully.
|
|
type ArtistArtProvider interface {
|
|
Provider
|
|
FetchArtistArt(ctx context.Context, ref ArtistRef) (thumb, fanart []byte, err error)
|
|
}
|
|
|
|
// TestableProvider is an opt-in capability: providers that can answer
|
|
// "is my config working?" without performing a full enrichment cycle.
|
|
// Used by the admin Test-Connection button. v1: both MBCAA and
|
|
// TheAudioDB implement this.
|
|
type TestableProvider interface {
|
|
Provider
|
|
TestConnection(ctx context.Context) error
|
|
}
|
|
|
|
// ErrNotTestable is returned by the SettingsService.TestProvider
|
|
// method when the named provider doesn't implement TestableProvider.
|
|
var ErrNotTestable = errors.New("coverart: provider does not support test connection")
|
|
|
|
// ErrProviderNotFound is returned when the named provider is not
|
|
// registered.
|
|
var ErrProviderNotFound = errors.New("coverart: provider not registered")
|
|
|
|
// ErrNotFound indicates the upstream confirmed there is no art for
|
|
// this MBID — a terminal-this-pass result. The enricher treats this
|
|
// as "try the next provider in the chain"; if all providers return
|
|
// ErrNotFound, the row settles to cover_art_source='none'.
|
|
//
|
|
// Aliased to apierror.ErrNotFound so handler error mapping (errors.Is
|
|
// against the shared sentinel) and existing per-package callsites both
|
|
// resolve to the same pointer.
|
|
var ErrNotFound = apierror.ErrNotFound
|
|
|
|
// ErrTransient indicates a temporary failure (5xx, network, timeout,
|
|
// auth issue). The enricher leaves the row's source NULL so the next
|
|
// scan pass can retry.
|
|
var ErrTransient = errors.New("coverart: transient error")
|
|
|
|
// registry is the package-private list of all compiled-in providers.
|
|
// Providers register at init() via Register(). The enricher iterates
|
|
// over this list filtered by the current per-provider Settings.
|
|
var registry = struct {
|
|
mu sync.Mutex
|
|
providers []Provider
|
|
}{}
|
|
|
|
// Register adds a provider to the registry. Must be called from
|
|
// init() of a provider file (provider_mbcaa.go, provider_theaudiodb.go,
|
|
// etc.). Calling twice with the same ID panics — providers are
|
|
// compiled-in, duplicate registration is a build-time bug.
|
|
func Register(p Provider) {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
for _, existing := range registry.providers {
|
|
if existing.ID() == p.ID() {
|
|
panic(fmt.Sprintf("coverart: duplicate provider ID %q", p.ID()))
|
|
}
|
|
}
|
|
registry.providers = append(registry.providers, p)
|
|
}
|
|
|
|
// AllProviders returns a snapshot of all registered providers in
|
|
// registration order. Caller must not mutate the returned slice.
|
|
func AllProviders() []Provider {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
out := make([]Provider, len(registry.providers))
|
|
copy(out, registry.providers)
|
|
return out
|
|
}
|
|
|
|
// ProviderByID looks up a registered provider by ID. Returns
|
|
// ErrProviderNotFound if no such provider has registered.
|
|
func ProviderByID(id string) (Provider, error) {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
for _, p := range registry.providers {
|
|
if p.ID() == id {
|
|
return p, nil
|
|
}
|
|
}
|
|
return nil, ErrProviderNotFound
|
|
}
|
|
|
|
// resetRegistryForTests clears the registry. Test-only helper
|
|
// (provider_test.go); never called from production code.
|
|
func resetRegistryForTests() {
|
|
registry.mu.Lock()
|
|
defer registry.mu.Unlock()
|
|
registry.providers = nil
|
|
}
|
|
|
|
// ResetRegistryForTests is an exported wrapper around resetRegistryForTests
|
|
// for use by tests in other packages (e.g. internal/api). Never call from
|
|
// production code.
|
|
func ResetRegistryForTests() {
|
|
resetRegistryForTests()
|
|
}
|