fix: address code review findings

Remove dead code: duplicate ExternalRelease/Artist/ParsedArtist structs in
model.go, ParseArtist/mbArtist/mbArtistData in client.go, ArtistTypeFilter
and related filtering functions in api.go, SyncArtistDiscographyWithFilter
in sync.go, and CacheStats/IsArtistCacheValid in cache.go.

Fix bugs: SaveExternalRelease now stores NULL instead of empty string for
zero CachedAt; sync upserts are now transactional with stale release cleanup;
getCachedReleases returns int instead of *CacheStats; doGet uses url.Values
for proper query encoding of MBID.

Fix tests: removed duplicate TestRun_GracefulShutdown, removed dead code
(_ = dbPath) from TestNewApp, fixed assertions in httptest handler goroutine
to avoid data race, increased rate limiter timing tolerance, removed
Client.Close() calls (no-op removed), fixed sync test cache expiry to use
UPDATE instead of 0 TTL races.

Fix formatting: cancel()}() formatting in main.go, error format string in sync.go.
This commit is contained in:
2026-05-26 14:10:22 +03:00
parent 34ea84fc77
commit a5911c257c
13 changed files with 199 additions and 864 deletions

View File

@@ -3,6 +3,7 @@ package musicbrainz
import (
"context"
"fmt"
"net/url"
"regexp"
"strings"
"unicode"
@@ -12,41 +13,17 @@ import (
// excludedStatuses contains release-group statuses that should be filtered out.
var excludedStatuses = map[string]bool{
"Bootleg": true,
"Promotion": true,
"Pseudo-Release": true,
"Bootleg": true,
"Promotion": true,
"Pseudo-Release": true,
}
// includedTypes contains release-group types that should be included.
var includedTypes = map[string]bool{
"Album": true,
"Single": true,
"EP": true,
"Compilation": true,
}
// ArtistTypeFilter holds per-artist type filtering preferences.
// These are placeholders for Web UI integration where users can
// toggle which release types to monitor per artist.
type ArtistTypeFilter struct {
// ArtistID is the MusicBrainz artist ID.
ArtistID string
// IncludeSingles whether to include Single-type release groups.
IncludeSingles bool
// IncludeCompilations whether to include Compilation-type release groups.
IncludeCompilations bool
// IncludeEP whether to include EP-type release groups.
IncludeEP bool
}
// DefaultArtistTypeFilter returns an ArtistTypeFilter with all types enabled.
func DefaultArtistTypeFilter(artistID string) *ArtistTypeFilter {
return &ArtistTypeFilter{
ArtistID: artistID,
IncludeSingles: true,
IncludeCompilations: true,
IncludeEP: true,
}
"Album": true,
"Single": true,
"EP": true,
"Compilation": true,
}
// GetArtistReleaseGroups fetches all release groups for a given artist from MusicBrainz.
@@ -61,7 +38,12 @@ func (c *MusicBrainzClient) GetArtistReleaseGroups(ctx context.Context, artistMB
limit := 100 // MusicBrainz max limit per request
for {
path := fmt.Sprintf("/release-group?artist=%s&limit=%d&offset=%d", artistMBID, limit, offset)
params := url.Values{}
params.Set("artist", artistMBID)
params.Set("limit", fmt.Sprintf("%d", limit))
params.Set("offset", fmt.Sprintf("%d", offset))
path := buildPath("/release-group", params)
body, err := c.doGet(ctx, path)
if err != nil {
return nil, fmt.Errorf("fetch release groups for artist %s: %w", artistMBID, err)
@@ -101,22 +83,6 @@ func FilterReleaseGroups(groups []ReleaseGroup) []ReleaseGroup {
return filtered
}
// FilterReleaseGroupsWithArtistFilter applies status filtering, base type filtering,
// and per-artist type filtering preferences.
func FilterReleaseGroupsWithArtistFilter(groups []ReleaseGroup, artistFilter *ArtistTypeFilter) []ReleaseGroup {
var filtered []ReleaseGroup
for _, rg := range groups {
if IsStatusExcluded(rg.Status) {
continue
}
if !IsTypeIncludedForArtist(rg.Type, artistFilter) {
continue
}
filtered = append(filtered, rg)
}
return filtered
}
// IsStatusExcluded returns true if the given status should be excluded.
func IsStatusExcluded(status string) bool {
return excludedStatuses[status]
@@ -127,34 +93,13 @@ func IsTypeIncluded(releaseType string) bool {
return includedTypes[releaseType]
}
// IsTypeIncludedForArtist checks whether a release type should be included
// based on per-artist type filtering preferences.
func IsTypeIncludedForArtist(releaseType string, filter *ArtistTypeFilter) bool {
if filter == nil {
return IsTypeIncluded(releaseType)
}
switch releaseType {
case "Album":
return true // Albums are always included
case "Single":
return filter.IncludeSingles
case "EP":
return filter.IncludeEP
case "Compilation":
return filter.IncludeCompilations
default:
return false
}
}
// NormalizeString normalizes a string for fuzzy matching by:
// - Converting to lowercase
// - Removing special characters (keeping only letters, digits, and spaces)
// - Removing years (4-digit numbers that look like years)
// - Removing bracketed keywords (e.g., [Deluxe], [Remastered])
// - Collapsing multiple spaces into one
// - Trimming leading/trailing whitespace
// - Converting to lowercase
// - Removing special characters (keeping only letters, digits, and spaces)
// - Removing years (4-digit numbers that look like years)
// - Removing bracketed keywords (e.g., [Deluxe], [Remastered])
// - Collapsing multiple spaces into one
// - Trimming leading/trailing whitespace
func NormalizeString(s string) string {
// Convert to lowercase
s = strings.ToLower(s)
@@ -212,7 +157,7 @@ func NormalizeArtistName(name string) string {
}
// ToExternalRelease converts a ReleaseGroup to an ExternalRelease
// with the current timestamp as CachedAt.
// for database persistence.
func (rg *ReleaseGroup) ToExternalRelease() *database.ExternalRelease {
return &database.ExternalRelease{
RGID: rg.ID,