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
|
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
|
// normalizeMBTags drops non-positive-vote tags (net-downvoted or zero) and
|
||||||
// scales the remaining vote counts into [0,1] relative to the strongest tag
|
// 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.
|
// 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)
|
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