Files
NaviWatcher/internal/musicbrainz/resolve.go
Vladimir Zagainov e73b17673e
Some checks failed
Build and Push Docker Image / build (pull_request) Failing after 38s
feat: implement Live/Remix filtering and add CI/CD pipeline
This commit includes:

1. Live/Remix Filtering Feature:
   - Added ignore_live and ignore_remix columns to artist_settings table (migration 010)
   - Updated ArtistSettings struct with IgnoreLive and IgnoreRemix fields
   - Modified SaveArtistSettings and UpdateArtistSettings to handle new fields
   - Extended FilterOptions struct with IgnoreLive and IgnoreRemix
   - Updated ApplyTypeToggles and ApplyTypeTogglesToReleaseGroups to filter Live/Remix types
   - Added toggleIgnoreLive and toggleIgnoreRemix handlers in web layer
   - Updated ArtistData view model and artist.html template with new toggle UI
   - Comprehensive test coverage for all new functionality

2. CI/CD Pipeline with Gitea Actions:
   - Added .gitea/workflows/docker-build.yml for automated Docker builds
   - Workflow triggers on pushes to main/master and tags, plus PRs
   - Runs Go tests before building
   - Builds and pushes multi-architecture Docker images to gitea.mrixs.me
   - Includes caching for faster subsequent builds
   - Proper tagging strategy (branch, semver, SHA)
   - CI-CD-GUIDE.md documentation

3. Cleanup:
   - Removed temporary build artifacts and coverage files
2026-08-05 22:55:40 +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
}