Files
NaviWatcher/internal/musicbrainz/syncall.go

142 lines
4.7 KiB
Go

package musicbrainz
import (
"context"
"fmt"
"time"
"naviwatcher/internal/database"
)
// MBIDResolver resolves a MusicBrainz artist ID for an artist name.
// The real *MusicBrainzClient satisfies this interface.
type MBIDResolver interface {
ResolveArtistMBID(ctx context.Context, name string) (string, error)
}
// ArtistDiscographySyncer syncs one artist's MusicBrainz discography into the
// external_releases table. The real implementation (musicbrainz.SyncArtistDiscography)
// is wrapped by discographySyncer so the concrete *MusicBrainzClient dependency
// is injectable in tests.
type ArtistDiscographySyncer interface {
SyncArtistDiscography(ctx context.Context, db *database.DB, artistID, artistMBID string, ttl time.Duration) ([]database.ExternalRelease, error)
}
// discographySyncer adapts the package-level SyncArtistDiscography function to
// the ArtistDiscographySyncer interface, binding a concrete *MusicBrainzClient.
type discographySyncer struct {
client *MusicBrainzClient
}
// NewDiscographySyncer wraps a *MusicBrainzClient as an ArtistDiscographySyncer.
func NewDiscographySyncer(client *MusicBrainzClient) ArtistDiscographySyncer {
return &discographySyncer{client: client}
}
func (s *discographySyncer) SyncArtistDiscography(
ctx context.Context,
db *database.DB,
artistID, artistMBID string,
ttl time.Duration,
) ([]database.ExternalRelease, error) {
return SyncArtistDiscography(ctx, s.client, db, artistID, artistMBID, ttl)
}
// AlbumSyncer copies each monitored artist's albums from Navidrome into the
// local_albums table. The real implementation (navidrome.SyncAlbums) is wrapped
// so the concrete *navidrome.NavidromeClient dependency is injectable in tests.
type AlbumSyncer interface {
SyncAlbums(ctx context.Context, db *database.DB) error
}
// albumSyncer adapts navidrome.SyncAlbums to the AlbumSyncer interface.
type albumSyncer struct {
syncAlbums func(ctx context.Context, db *database.DB) error
}
func (s *albumSyncer) SyncAlbums(ctx context.Context, db *database.DB) error {
return s.syncAlbums(ctx, db)
}
// SyncAll orchestrates the data pipeline for every monitored artist:
// 1. MusicBrainz artist-ID resolution — for each artist with no cached MBID,
// resolve it by name and persist it on the artist_settings row. Artists that
// already have an MBID reuse it (no extra rate-limited MusicBrainz call).
// 2. MusicBrainz discography sync into external_releases.
// 3. Navidrome album sync into local_albums.
//
// Ordering matters: Navidrome's artist/album tables are populated by the caller
// before SyncAll (via navidrome.SyncArtists / SyncAlbums as appropriate); here we
// focus on the per-artist MBID + discography + album refresh. Unmonitored artists
// are skipped.
//
// Resolution failures for a single artist are logged and skipped (the artist is
// left for the next sync) rather than aborting the whole run; the error is still
// returned so the caller can decide whether to surface it.
func SyncAll(
ctx context.Context,
db *database.DB,
resolver MBIDResolver,
discography ArtistDiscographySyncer,
albums AlbumSyncer,
ttl time.Duration,
) error {
if err := ctx.Err(); err != nil {
return fmt.Errorf("sync all: %w", err)
}
artists, err := database.GetAllArtistSettings(db)
if err != nil {
return fmt.Errorf("sync all: get artists: %w", err)
}
var resolutionErr error
for _, artist := range artists {
if err := ctx.Err(); err != nil {
return fmt.Errorf("sync all: %w", err)
}
// Skip unmonitored artists entirely.
if !artist.Monitored {
continue
}
// Ensure we have an MBID; resolve and persist if missing.
mbid := artist.MBID
if mbid == "" {
resolved, rerr := resolver.ResolveArtistMBID(ctx, artist.Name)
if rerr != nil {
// Skip this artist but remember the first resolution error.
if resolutionErr == nil {
resolutionErr = fmt.Errorf("resolve MBID for artist %q: %w", artist.Name, rerr)
}
continue
}
mbid = resolved
if perr := database.UpdateArtistSettings(db, artist.ID, map[string]interface{}{"mbid": mbid}); perr != nil {
if resolutionErr == nil {
resolutionErr = fmt.Errorf("persist MBID for artist %q: %w", artist.Name, perr)
}
continue
}
}
// Sync the artist's MusicBrainz discography.
if _, derr := discography.SyncArtistDiscography(ctx, db, artist.ID, mbid, ttl); derr != nil {
if resolutionErr == nil {
resolutionErr = fmt.Errorf("sync discography for artist %q: %w", artist.Name, derr)
}
continue
}
}
// Album sync operates over all monitored artists in one pass.
if aerr := albums.SyncAlbums(ctx, db); aerr != nil {
if resolutionErr == nil {
resolutionErr = fmt.Errorf("sync albums: %w", aerr)
}
}
return resolutionErr
}