142 lines
4.7 KiB
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
|
|
}
|