Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.
Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
today rather than a milestone that would change it.
Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
package coverart
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// FetcherConfig assembles the tunable knobs for the MBCAA
|
|
// HTTP client. Kept exported because main.go's boot wiring constructs
|
|
// one to pass into NewMBCAAProviderFromConfig.
|
|
type FetcherConfig struct {
|
|
BaseURL string // default "https://coverartarchive.org"
|
|
UserAgent string // e.g. "Minstrel/<version> (<contact>)"
|
|
MinPeriod time.Duration // 1s default per MBCAA etiquette
|
|
HTTPClient *http.Client // optional override for tests
|
|
}
|
|
|
|
// Sample MBID used by TestConnection. Beatles' "Abbey Road" UK release.
|
|
const mbcaaTestSampleMBID = "f4b3df80-8a6c-3e87-b51e-5c1ee70a78ee"
|
|
|
|
// mbcaaProvider implements Provider, AlbumCoverProvider, and
|
|
// TestableProvider against the MusicBrainz Cover Art Archive.
|
|
// Safe for concurrent use; the internal mutex serialises HTTP
|
|
// requests so the rate limit is respected.
|
|
type mbcaaProvider struct {
|
|
cfg FetcherConfig
|
|
enabled atomic.Bool
|
|
mu sync.Mutex
|
|
lastCall time.Time
|
|
client *http.Client
|
|
}
|
|
|
|
func init() {
|
|
Register(newMBCAAProvider(FetcherConfig{}))
|
|
}
|
|
|
|
// NewMBCAAProviderFromConfig replaces the registered MBCAA provider's
|
|
// internal config with the supplied one. main.go calls this at boot
|
|
// to set the production User-Agent.
|
|
func NewMBCAAProviderFromConfig(cfg FetcherConfig) {
|
|
p, err := ProviderByID("mbcaa")
|
|
if err != nil {
|
|
Register(newMBCAAProvider(cfg))
|
|
return
|
|
}
|
|
mp, ok := p.(*mbcaaProvider)
|
|
if !ok {
|
|
panic("coverart: provider with ID 'mbcaa' is not *mbcaaProvider")
|
|
}
|
|
cfg = applyMBCAADefaults(cfg)
|
|
mp.mu.Lock()
|
|
mp.cfg = cfg
|
|
mp.client = cfg.HTTPClient
|
|
mp.mu.Unlock()
|
|
}
|
|
|
|
func newMBCAAProvider(cfg FetcherConfig) *mbcaaProvider {
|
|
cfg = applyMBCAADefaults(cfg)
|
|
return &mbcaaProvider{cfg: cfg, client: cfg.HTTPClient}
|
|
}
|
|
|
|
func applyMBCAADefaults(cfg FetcherConfig) FetcherConfig {
|
|
if cfg.BaseURL == "" {
|
|
cfg.BaseURL = "https://coverartarchive.org"
|
|
}
|
|
if cfg.MinPeriod == 0 {
|
|
cfg.MinPeriod = time.Second
|
|
}
|
|
if cfg.HTTPClient == nil {
|
|
cfg.HTTPClient = &http.Client{Timeout: 30 * time.Second}
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func (p *mbcaaProvider) ID() string { return "mbcaa" }
|
|
func (p *mbcaaProvider) DisplayName() string { return "Cover Art Archive" }
|
|
func (p *mbcaaProvider) RequiresAPIKey() bool { return false }
|
|
func (p *mbcaaProvider) DefaultEnabled() bool { return true }
|
|
|
|
func (p *mbcaaProvider) Configure(s ProviderSettings) error {
|
|
p.enabled.Store(s.Enabled)
|
|
return nil
|
|
}
|
|
|
|
// FetchAlbumCover retrieves the 500px front cover for the given
|
|
// release MBID. MBCAA is MBID-only; returns ErrNotFound when
|
|
// ref.MBID is empty. Returns ErrNotFound on 404 or ErrTransient otherwise.
|
|
func (p *mbcaaProvider) FetchAlbumCover(ctx context.Context, ref AlbumRef) ([]byte, error) {
|
|
return p.fetchFront(ctx, "release", ref.MBID)
|
|
}
|
|
|
|
// FetchReleaseGroupCover retrieves the 500px front cover the Cover Art
|
|
// Archive chose for a release group: what a search result or a wanted album
|
|
// shows before any edition of it is held.
|
|
func (p *mbcaaProvider) FetchReleaseGroupCover(ctx context.Context, groupMBID string) ([]byte, error) {
|
|
return p.fetchFront(ctx, "release-group", groupMBID)
|
|
}
|
|
|
|
// fetchFront fetches /{entity}/{mbid}/front-500, honouring the rate limit.
|
|
func (p *mbcaaProvider) fetchFront(ctx context.Context, entity, mbid string) ([]byte, error) {
|
|
if !p.enabled.Load() {
|
|
return nil, ErrNotFound // defensive; enricher already filters on enabled
|
|
}
|
|
if mbid == "" {
|
|
return nil, ErrNotFound // MBCAA is MBID-only
|
|
}
|
|
|
|
// Rate limit guard.
|
|
p.mu.Lock()
|
|
cfg := p.cfg
|
|
if cfg.MinPeriod > 0 {
|
|
wait := time.Until(p.lastCall.Add(cfg.MinPeriod))
|
|
if wait > 0 {
|
|
timer := time.NewTimer(wait)
|
|
p.mu.Unlock()
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return nil, ctx.Err()
|
|
case <-timer.C:
|
|
}
|
|
p.mu.Lock()
|
|
}
|
|
}
|
|
p.lastCall = time.Now()
|
|
p.mu.Unlock()
|
|
|
|
url := fmt.Sprintf("%s/%s/%s/front-500", cfg.BaseURL, entity, mbid)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("coverart: build request: %w", err)
|
|
}
|
|
req.Header.Set("User-Agent", cfg.UserAgent)
|
|
req.Header.Set("Accept", "image/*")
|
|
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrTransient, err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
switch {
|
|
case resp.StatusCode == http.StatusNotFound:
|
|
return nil, ErrNotFound
|
|
case resp.StatusCode != http.StatusOK:
|
|
return nil, fmt.Errorf("%w: status %d", ErrTransient, resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: read body: %v", ErrTransient, err)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func (p *mbcaaProvider) TestConnection(ctx context.Context) error {
|
|
_, err := p.FetchAlbumCover(ctx, AlbumRef{MBID: mbcaaTestSampleMBID})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, ErrNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
var _ ReleaseGroupCoverProvider = (*mbcaaProvider)(nil)
|