- Prevent infinite pagination loop when API returns empty release groups page - Move NormalizeString regexes to package level to avoid recompilation on every call - Use UTC consistently for cached_at timestamps to avoid DST-related TTL skew
118 lines
3.8 KiB
Go
118 lines
3.8 KiB
Go
package musicbrainz
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"naviwatcher/internal/database"
|
|
)
|
|
|
|
// SyncArtistDiscography synchronizes an artist's discography from MusicBrainz
|
|
// into the local external_releases table. It follows this flow:
|
|
// 1. Check if cached data exists and is within TTL.
|
|
// 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. 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
|
|
// to allow graceful interruption.
|
|
func SyncArtistDiscography(
|
|
ctx context.Context,
|
|
client *MusicBrainzClient,
|
|
db *database.DB,
|
|
artistMBID string,
|
|
ttl time.Duration,
|
|
) ([]database.ExternalRelease, error) {
|
|
// Check context before starting.
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, fmt.Errorf("sync artist discography: %w", err)
|
|
}
|
|
|
|
// Step 1: Check cache.
|
|
cachedReleases, 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 len(cachedReleases) > 0 {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, fmt.Errorf("sync artist discography: %w", err)
|
|
}
|
|
return cachedReleases, nil
|
|
}
|
|
|
|
// 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 for artist %s: %w", artistMBID, err)
|
|
}
|
|
|
|
// Step 4: Apply filtering.
|
|
filtered := FilterReleaseGroups(groups)
|
|
|
|
// Step 5: Upsert within a transaction — delete old entries first, then insert new ones.
|
|
now := time.Now().UTC()
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sync artist discography: begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Read existing ignore states before deleting to preserve user-set flags.
|
|
ignoredMap := map[string]bool{}
|
|
rows, err := tx.Query("SELECT rgid, is_ignored FROM external_releases WHERE artist_id = ?", artistMBID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sync artist discography: query existing releases: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var rgid string
|
|
var ignored bool
|
|
if err := rows.Scan(&rgid, &ignored); err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("sync artist discography: scan existing release: %w", err)
|
|
}
|
|
ignoredMap[rgid] = ignored
|
|
}
|
|
rows.Close()
|
|
|
|
// 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.
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, fmt.Errorf("sync artist discography: %w", err)
|
|
}
|
|
|
|
ext := rg.ToExternalRelease()
|
|
ext.CachedAt = now
|
|
// Preserve user-set ignore flag from previous sync.
|
|
if ignored, ok := ignoredMap[ext.RGID]; ok {
|
|
ext.IsIgnored = ignored
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, fmt.Errorf("sync artist discography: commit transaction: %w", err)
|
|
}
|
|
|
|
return releases, nil
|
|
}
|