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 }