Files
NaviWatcher/internal/musicbrainz/resolve.go
Vladimir Zagainov 7cdb473d9c fix: address code review findings
- notifier: show artist display names (not internal IDs) in digest; resolve
  names from artist_settings and fall back to ID when unavailable
- notifier: skip sending an empty digest to avoid daily spam
- config: require telegram token/chat_id when enabled
- web: warn loudly when auth is disabled on a non-loopback bind; add HTTP
  server timeouts
- web: treat SetReleaseIgnored "release not found" as benign redirect (0 rows)
- musicbrainz: reject low-score/name-mismatched MBID resolutions instead of
  silently caching the wrong artist
- database: remove dead duplicate err check; harden DSN param appending
- musicbrainz: check rows.Err() after iterating existing releases
2026-07-19 23:46:09 +03:00

71 lines
2.6 KiB
Go

package musicbrainz
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"naviwatcher/internal/normalize"
)
// mbArtistSearchResult models the JSON response of the MusicBrainz artist
// search endpoint (/ws/2/artist?query=artist:<name>&fmt=json). Only the
// fields we need for MBID resolution are decoded.
type mbArtistSearchResult struct {
Artists []struct {
ID string `json:"id"`
Name string `json:"name"`
Score int `json:"score"`
} `json:"artists"`
}
// minResolutionScore is the minimum MusicBrainz search score (0-100) we accept
// for an MBID resolution. Below this, the best hit is too weak a match to
// trust, and caching it would silently pollute an artist's discography with
// the wrong MusicBrainz data.
const minResolutionScore = 80
// ResolveArtistMBID resolves a MusicBrainz artist ID (MBID) for the given
// artist name by querying the MusicBrainz artist search endpoint. It returns
// the ID of the highest-scoring matching artist, but only when that artist's
// normalized name actually matches the requested name (and its search score is
// at or above minResolutionScore). An error is returned if the search yields
// no usable match, the response cannot be parsed, or the underlying request
// fails. Rejecting a low-confidence hit lets the caller surface the problem
// instead of caching a wrong MBID.
func (c *MusicBrainzClient) ResolveArtistMBID(ctx context.Context, name string) (string, error) {
params := url.Values{}
params.Set("query", fmt.Sprintf("artist:%s", name))
params.Set("fmt", "json")
path := "/artist?" + params.Encode()
body, err := c.doGet(ctx, path)
if err != nil {
return "", fmt.Errorf("resolve MBID for artist %q: %w", name, err)
}
var result mbArtistSearchResult
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("parse artist search response for %q: %w", name, err)
}
if len(result.Artists) == 0 {
return "", fmt.Errorf("no MusicBrainz artist found for %q", name)
}
best := result.Artists[0]
if best.Score < minResolutionScore {
return "", fmt.Errorf("no confident MusicBrainz match for %q (best candidate %q scored %d, need >= %d)", name, best.Name, best.Score, minResolutionScore)
}
// Even with a high score, require the normalized name to match, guarding
// against score inflation on name collisions (e.g. tribute acts).
if normalize.NormalizeArtistName(best.Name) != normalize.NormalizeArtistName(name) {
log.Printf("MusicBrainz MBID resolution skipped for %q: best candidate %q did not match by name", name, best.Name)
return "", fmt.Errorf("best MusicBrainz candidate %q does not match %q by name", best.Name, name)
}
return best.ID, nil
}