diff --git a/internal/database/database.go b/internal/database/database.go index 848512f..2ac5adc 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -121,6 +121,10 @@ func (db *DB) migrate() error { name: "006_add_secondary_types_to_external_releases", sql: `ALTER TABLE external_releases ADD COLUMN secondary_types TEXT;`, }, + { + name: "007_index_external_releases_artist_id", + sql: `CREATE INDEX IF NOT EXISTS idx_external_releases_artist_id ON external_releases(artist_id);`, + }, } for _, m := range migrations { diff --git a/internal/database/database_test.go b/internal/database/database_test.go index a894b0e..b3b08df 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -205,9 +205,10 @@ func TestMigrationTracking(t *testing.T) { t.Fatalf("query migrations count: %v", err) } - // We have 6 recorded migrations: artist_settings, external_releases, - // local_albums, notifications_sent, cached_at column, secondary_types column. - if count != 6 { - t.Errorf("expected 6 applied migrations, got %d", count) + // We have 7 recorded migrations: artist_settings, external_releases, + // local_albums, notifications_sent, cached_at column, secondary_types + // column, and the external_releases.artist_id index. + if count != 7 { + t.Errorf("expected 7 applied migrations, got %d", count) } } diff --git a/internal/database/external_releases.go b/internal/database/external_releases.go index 120af09..2de2fae 100644 --- a/internal/database/external_releases.go +++ b/internal/database/external_releases.go @@ -175,11 +175,10 @@ func GetExternalReleasesByArtistWithCache(db *DB, artistID string, ttl time.Dura if ttl <= 0 { return nil, nil } - // cached_at is stored in the "2006-01-02 15:04:05" UTC layout via - // FormatCachedAt. Compare against an explicitly formatted cutoff string in - // the same layout so the lexicographic comparison is a valid time ordering. - const layout = "2006-01-02 15:04:05" - cutoff := time.Now().UTC().Add(-ttl).Format(layout) + // cached_at is stored in utcLayout via FormatCachedAt. Compare against an + // explicitly formatted cutoff string in the same layout so the + // lexicographic comparison is a valid time ordering. + cutoff := time.Now().UTC().Add(-ttl).Format(utcLayout) rows, err := db.Conn().Query( "SELECT rgid, artist_id, title, type, release_date, is_ignored, cached_at, secondary_types FROM external_releases WHERE artist_id = ? AND cached_at >= ?", artistID, cutoff, diff --git a/internal/musicbrainz/api.go b/internal/musicbrainz/api.go index 044d043..03cfead 100644 --- a/internal/musicbrainz/api.go +++ b/internal/musicbrainz/api.go @@ -46,10 +46,14 @@ func (c *MusicBrainzClient) GetArtistReleaseGroups(ctx context.Context, artistMB allGroups = append(allGroups, parsed.ReleaseGroups...) - // If we've fetched all results, we've reached the end. - // Also break on empty page to prevent infinite loop if API - // returns fewer items than advertised by count. - if len(parsed.ReleaseGroups) == 0 || offset+len(parsed.ReleaseGroups) >= parsed.Count { + // Stop when a page is empty (no more results) or when the page + // returned fewer items than the request limit — a reliable end-of-data + // signal. We intentionally do NOT trust parsed.Count for the cutoff: + // MusicBrainz occasionally reports an inaccurate count, which would + // prematurely truncate an artist's discography and hide missing + // releases. The empty-page check also prevents an infinite loop if the + // API keeps returning a non-empty page past the reported count. + if len(parsed.ReleaseGroups) == 0 || len(parsed.ReleaseGroups) < limit { break }