feat(tags): resolve a MusicBrainz release to its release group (M483 #5243)
ReleaseGroupForRelease asks /ws/2/release/<id>?inc=release-groups through the registered MusicBrainz provider, so it shares that provider's client and 1 req/s limiter with tag enrichment and respects its on/off switch. ErrNotFound when switched off or MusicBrainz has no such release (an id that is already a release group included); ErrTransient to retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,54 @@ func (p *musicbrainzProvider) TestConnection(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ReleaseGroupForRelease names the MusicBrainz release group a release
|
||||
// belongs to (#5241). Minstrel's albums.mbid is the release id from the tags,
|
||||
// while Lidarr knows albums only by release group, so re-acquiring a lost
|
||||
// album needs this translation when its files (and their release-group tag)
|
||||
// are already gone.
|
||||
//
|
||||
// It goes through the registered MusicBrainz provider, so the lookup shares
|
||||
// that provider's client and 1 req/s limiter with tag enrichment rather than
|
||||
// doubling Minstrel's request rate, and it respects the provider's on/off
|
||||
// switch. ErrNotFound: MusicBrainz is switched off, or has no release under
|
||||
// that id (an id that is already a release group lands here too).
|
||||
// ErrTransient: retry later.
|
||||
func ReleaseGroupForRelease(ctx context.Context, releaseMBID string) (string, error) {
|
||||
p, err := ProviderByID("musicbrainz")
|
||||
if err != nil {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
mb, ok := p.(*musicbrainzProvider)
|
||||
if !ok {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return mb.releaseGroupForRelease(ctx, releaseMBID)
|
||||
}
|
||||
|
||||
// mbReleaseResponse models the subset of /ws/2/release/{mbid}?inc=release-groups
|
||||
// we read.
|
||||
type mbReleaseResponse struct {
|
||||
ReleaseGroup struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"release-group"`
|
||||
}
|
||||
|
||||
func (p *musicbrainzProvider) releaseGroupForRelease(ctx context.Context, releaseMBID string) (string, error) {
|
||||
if !p.enabled.Load() || releaseMBID == "" {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
q := url.Values{"inc": {"release-groups"}, "fmt": {"json"}}
|
||||
full := mbBaseURL + "/release/" + url.PathEscape(releaseMBID) + "?" + q.Encode()
|
||||
var resp mbReleaseResponse
|
||||
if err := p.client.getJSON(ctx, full, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.ReleaseGroup.ID == "" {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return resp.ReleaseGroup.ID, nil
|
||||
}
|
||||
|
||||
// normalizeMBTags drops non-positive-vote tags (net-downvoted or zero) and
|
||||
// scales the remaining vote counts into [0,1] relative to the strongest tag
|
||||
// on this recording, so one recording's raw counts don't dominate another's.
|
||||
|
||||
@@ -176,3 +176,37 @@ func TestMusicBrainzFetch_EmptyTags(t *testing.T) {
|
||||
t.Errorf("empty tags: err = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
// #5241: a release id resolves to its release group through
|
||||
// /release/<id>?inc=release-groups; an unknown id (or one that is already a
|
||||
// group) and a switched-off source both say ErrNotFound.
|
||||
func TestMusicBrainzReleaseGroupForRelease(t *testing.T) {
|
||||
const release, group = "rel-1", "group-1"
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/release/"+release {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"error":"Not Found"}`))
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("inc"); got != "release-groups" {
|
||||
t.Errorf("inc = %q, want release-groups", got)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"id":"` + release + `","title":"X","release-group":{"id":"` + group + `","title":"X"}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
old := mbBaseURL
|
||||
mbBaseURL = srv.URL
|
||||
defer func() { mbBaseURL = old }()
|
||||
|
||||
ctx := context.Background()
|
||||
got, err := newMBProvider(true).releaseGroupForRelease(ctx, release)
|
||||
if err != nil || got != group {
|
||||
t.Fatalf("resolve = (%q, %v), want (%q, nil)", got, err, group)
|
||||
}
|
||||
if _, err := newMBProvider(true).releaseGroupForRelease(ctx, "group-1"); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("unknown release: err = %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := newMBProvider(false).releaseGroupForRelease(ctx, release); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("switched off: err = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user