feat: implement MusicBrainz sync orchestration with caching and upsert
Creates internal/musicbrainz/sync.go with SyncArtistDiscography function that orchestrates the full flow: cache check via GetCachedReleases, fetch from MusicBrainz API on cache miss, filter via FilterReleaseGroups, and upsert into external_releases via database.SaveExternalRelease. Includes SyncArtistDiscographyWithFilter variant for per-artist type filtering. All functions support context cancellation. 14 new tests cover cache hit/miss, status/type filtering, context cancellation, idempotency, API errors, and full XML pipeline integration.
This commit is contained in:
130
internal/musicbrainz/sync.go
Normal file
130
internal/musicbrainz/sync.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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. Upsert each filtered release group into external_releases with current timestamp.
|
||||
// 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.
|
||||
cached, 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
// Step 4: Apply filtering.
|
||||
filtered := FilterReleaseGroups(groups)
|
||||
|
||||
// Step 5: Upsert each release group into the database.
|
||||
now := time.Now()
|
||||
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
|
||||
|
||||
if err := database.SaveExternalRelease(db, ext); err != nil {
|
||||
return nil, fmt.Errorf("sync artist discography: save 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)
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
Reference in New Issue
Block a user