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

@@ -14,7 +14,7 @@ import (
// 2. If cache hit, return the cached releases immediately.
// 3. If cache miss or expired, fetch release groups from MusicBrainz API.
// 4. Apply status and type filtering.
// 5. Upsert each filtered release group into external_releases with current timestamp.
// 5. Within a transaction: delete old entries, then upsert each filtered release group.
// 6. Return the list of external releases.
//
// Context cancellation is checked before the API call and between each upsert
@@ -32,27 +32,38 @@ func SyncArtistDiscography(
}
// Step 1: Check cache.
cached, err := GetCachedReleases(db, artistMBID, ttl)
cachedCount, err := GetCachedReleases(db, artistMBID, ttl)
if err != nil {
return nil, fmt.Errorf("sync artist discography: cache check failed: %w", err)
}
// Step 2: If we have cached data, return it.
if cached.CacheHitCount > 0 {
if cachedCount > 0 {
return database.GetExternalReleasesByArtistWithCache(db, artistMBID, ttl)
}
// Step 3: Cache miss — fetch from MusicBrainz API.
groups, err := client.GetArtistReleaseGroups(ctx, artistMBID)
if err != nil {
return nil, fmt.Errorf("sync artist discography: fetch release groups: %w", err)
return nil, fmt.Errorf("sync artist discography: fetch release groups for artist %s: %w", artistMBID, err)
}
// Step 4: Apply filtering.
filtered := FilterReleaseGroups(groups)
// Step 5: Upsert each release group into the database.
// Step 5: Upsert within a transaction — delete old entries first, then insert new ones.
now := time.Now()
tx, err := db.Begin()
if err != nil {
return nil, fmt.Errorf("sync artist discography: begin transaction: %w", err)
}
defer tx.Rollback()
// Delete old entries for this artist to avoid stale records.
if _, err := tx.Exec("DELETE FROM external_releases WHERE artist_id = ?", artistMBID); err != nil {
return nil, fmt.Errorf("sync artist discography: delete old releases: %w", err)
}
var releases []database.ExternalRelease
for _, rg := range filtered {
// Check context cancellation between each upsert.
@@ -63,67 +74,19 @@ func SyncArtistDiscography(
ext := rg.ToExternalRelease()
ext.CachedAt = now
if err := database.SaveExternalRelease(db, ext); err != nil {
return nil, fmt.Errorf("sync artist discography: save release %s: %w", rg.ID, err)
cachedAtStr := ext.CachedAt.Format("2006-01-02 15:04:05")
if _, err := tx.Exec(
"INSERT INTO external_releases (rgid, artist_id, title, type, release_date, is_ignored, cached_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
ext.RGID, ext.ArtistID, ext.Title, ext.Type, ext.ReleaseDate, ext.IsIgnored, cachedAtStr,
); err != nil {
return nil, fmt.Errorf("sync artist discography: insert release %s: %w", rg.ID, err)
}
releases = append(releases, *ext)
}
return releases, nil
}
// SyncArtistDiscographyWithFilter works like SyncArtistDiscography but applies
// per-artist type filtering preferences in addition to the base filters.
func SyncArtistDiscographyWithFilter(
ctx context.Context,
client *MusicBrainzClient,
db *database.DB,
artistMBID string,
ttl time.Duration,
artistFilter *ArtistTypeFilter,
) ([]database.ExternalRelease, error) {
// Check context before starting.
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("sync artist discography with filter: %w", err)
}
// Step 1: Check cache.
cached, err := GetCachedReleases(db, artistMBID, ttl)
if err != nil {
return nil, fmt.Errorf("sync artist discography with filter: cache check failed: %w", err)
}
// Step 2: If we have cached data, return it.
if cached.CacheHitCount > 0 {
return database.GetExternalReleasesByArtistWithCache(db, artistMBID, ttl)
}
// Step 3: Cache miss — fetch from MusicBrainz API.
groups, err := client.GetArtistReleaseGroups(ctx, artistMBID)
if err != nil {
return nil, fmt.Errorf("sync artist discography with filter: fetch release groups: %w", err)
}
// Step 4: Apply filtering with artist-specific type preferences.
filtered := FilterReleaseGroupsWithArtistFilter(groups, artistFilter)
// Step 5: Upsert each release group into the database.
now := time.Now()
var releases []database.ExternalRelease
for _, rg := range filtered {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("sync artist discography with filter: %w", err)
}
ext := rg.ToExternalRelease()
ext.CachedAt = now
if err := database.SaveExternalRelease(db, ext); err != nil {
return nil, fmt.Errorf("sync artist discography with filter: save release %s: %w", rg.ID, err)
}
releases = append(releases, *ext)
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("sync artist discography: commit transaction: %w", err)
}
return releases, nil