Files
minstrel/internal/musicbrainz/pace.go
T
bvandeusenandClaude Opus 5.5 77389f88bc
release / govulncheck (push) Successful in 22s
release / web (push) Failing after 24s
release / go (push) Successful in 1m57s
release / integration (push) Successful in 4m1s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 4m38s
release / Build signed APK (releases and dev) (push) Canceled after 4m37s
feat: a release catalogue from MusicBrainz; request search asks it directly (M502 #5519)
Minstrel keeps its own record of what artists have released, read from
MusicBrainz, so the wanted list, search and import no longer need Lidarr.

- internal/musicbrainz: searches (artists, release groups, recordings) and
  browses (an artist's release groups; a group's releases with track lists).
  One process-wide pacer at 1.1s, shared with tag enrichment, because
  MusicBrainz's limit is per address. A 503 throttle is retried.
- Migration 0080: mb_artists, mb_release_groups, mb_artist_release_groups
  (a collaboration belongs to each credited artist), mb_releases,
  mb_release_tracks.
- internal/catalogue: Discography and Releases serve the held copy and
  re-read it when older than 7 or 30 days. An unreachable MusicBrainz
  serves the stale copy, marked. An hourly pass re-reads due artists.
- Art for what isn't held comes through /api/catalogue/art from the
  enabled cover-art sources (Cover Art Archive by release group, then the
  name-based sources) and is kept on disk, so the browser never fetches a
  third-party host. A miss is a 404 and the card shows its icon.
- Request search moves from /api/lidarr/search to /api/catalogue/search,
  with the same response shape. Albums are release groups, and "in
  library" matches albums.release_group_mbid. A 10-minute cache absorbs
  search-as-you-type. Discover suggestions point at the art endpoint
  instead of looking up Lidarr.
- Android calls the new path and loads Discover art through ServerImage.
  Installed builds older than this one lose Discover search until they
  update (no compatibility path, rule 22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:08:22 -04:00

60 lines
1.4 KiB
Go

// Package musicbrainz is Minstrel's client for the MusicBrainz web service:
// searches and browses for the release catalogue (M502 #5519), and the one
// pacer every MusicBrainz request in the process waits on.
package musicbrainz
import (
"context"
"sync"
"time"
)
// MinInterval honours MusicBrainz's 1 request/second ceiling, with headroom.
// The limit is per client address, not per caller, which is why there is one
// pacer for the whole process.
const MinInterval = 1100 * time.Millisecond
// Pacer spaces requests at least Interval apart. Safe for concurrent use;
// callers queue in the order they claim slots.
type Pacer struct {
Interval time.Duration
mu sync.Mutex
next time.Time
}
// Wait blocks until this caller's slot comes up, or ctx ends. A zero
// Interval never waits.
func (p *Pacer) Wait(ctx context.Context) error {
if p == nil || p.Interval <= 0 {
return ctx.Err()
}
p.mu.Lock()
now := time.Now()
slot := p.next
if slot.Before(now) {
slot = now
}
p.next = slot.Add(p.Interval)
p.mu.Unlock()
wait := time.Until(slot)
if wait <= 0 {
return ctx.Err()
}
timer := time.NewTimer(wait)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
var shared = &Pacer{Interval: MinInterval}
// Shared is the pacer every MusicBrainz request in the process goes through:
// the catalogue's and tag enrichment's alike.
func Shared() *Pacer { return shared }