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
This commit is contained in:
@@ -4,7 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"naviwatcher/internal/normalize"
|
||||
)
|
||||
|
||||
// mbArtistSearchResult models the JSON response of the MusicBrainz artist
|
||||
@@ -18,11 +21,20 @@ type mbArtistSearchResult struct {
|
||||
} `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 first (best-scoring) matching artist. An error is returned if
|
||||
// the search yields no matches, the response cannot be parsed, or the
|
||||
// underlying request fails.
|
||||
// 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))
|
||||
@@ -43,5 +55,16 @@ func (c *MusicBrainzClient) ResolveArtistMBID(ctx context.Context, name string)
|
||||
return "", fmt.Errorf("no MusicBrainz artist found for %q", name)
|
||||
}
|
||||
|
||||
return result.Artists[0].ID, nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -114,6 +114,10 @@ func SyncArtistDiscography(
|
||||
}
|
||||
ignoredMap[rgid] = ignored
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("sync artist discography: iterate existing releases: %w", err)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Build the set of RGIDs present in this sync so we can drop only the rows
|
||||
|
||||
Reference in New Issue
Block a user