Files
NaviWatcher/internal/navidrome/sync.go

131 lines
4.0 KiB
Go

package navidrome
import (
"context"
"errors"
"fmt"
"log"
"naviwatcher/internal/database"
)
// SyncAlbums fetches all albums from Navidrome for each monitored artist and
// stores them in the local_albums table. For each artist, existing local albums
// are deleted before inserting the fresh set, so the table always reflects the
// current Navidrome state. Unmonitored artists are skipped.
// Context cancellation is checked before each artist's album fetch.
func SyncAlbums(ctx context.Context, client *NavidromeClient, db *database.DB) error {
if err := ctx.Err(); err != nil {
return fmt.Errorf("sync albums: %w", err)
}
// Get all artists from the local database.
artists, err := database.GetAllArtistSettings(db)
if err != nil {
return fmt.Errorf("sync albums: get artists: %w", err)
}
for _, artist := range artists {
if err := ctx.Err(); err != nil {
return fmt.Errorf("sync albums: %w", err)
}
// Skip unmonitored artists.
if !artist.Monitored {
continue
}
albums, err := client.GetArtistAlbums(artist.ID)
if err != nil {
// A transient failure for one artist must not abort the whole pull
// and take down the daemon; log it and continue with the remaining
// artists, matching the resilience of SyncAll/ScanAll.
log.Printf("sync albums: skip artist %s: %v", artist.ID, err)
continue
}
// Delete existing albums for this artist and insert fresh set within
// a transaction to prevent partial sync state on failure.
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("sync albums: begin transaction for artist %s: %w", artist.ID, err)
}
if err := database.DeleteLocalAlbumsByArtistTx(tx, artist.ID); err != nil {
tx.Rollback()
return fmt.Errorf("sync albums: delete existing for artist %s: %w", artist.ID, err)
}
for _, album := range albums {
if err := ctx.Err(); err != nil {
tx.Rollback()
return fmt.Errorf("sync albums: %w", err)
}
localAlbum := &database.LocalAlbum{
ID: album.ID,
ArtistID: artist.ID,
Title: album.Name,
}
if err := database.SaveLocalAlbumTx(tx, localAlbum); err != nil {
tx.Rollback()
return fmt.Errorf("sync albums: save album %s: %w", album.ID, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("sync albums: commit for artist %s: %w", artist.ID, err)
}
}
return nil
}
// SyncArtists fetches all artists from Navidrome and upserts them into the
// local artist_settings table. New artists are inserted with monitored=true.
// Existing artists have their name refreshed but their monitored/ignore
// settings are preserved.
// Context cancellation is checked before the API call and between individual
// artist upserts.
func SyncArtists(ctx context.Context, client *NavidromeClient, db *database.DB) error {
// Check context before making the API call.
if err := ctx.Err(); err != nil {
return fmt.Errorf("sync artists: %w", err)
}
artists, err := client.GetArtists()
if err != nil {
return fmt.Errorf("sync artists: %w", err)
}
for _, artist := range artists {
// Check context cancellation between each upsert to allow
// graceful interruption on large libraries.
if err := ctx.Err(); err != nil {
return fmt.Errorf("sync artists: %w", err)
}
// Preserve existing user settings (monitored, ignore_singles,
// ignore_compilations) if the row already exists.
settings := &database.ArtistSettings{
ID: artist.ID,
Name: artist.Name,
Monitored: true,
}
existing, err := database.GetArtistSettings(db, artist.ID)
if err == nil {
settings.MBID = existing.MBID
settings.Monitored = existing.Monitored
settings.IgnoreSingles = existing.IgnoreSingles
settings.IgnoreCompilations = existing.IgnoreCompilations
settings.LastSynced = existing.LastSynced
} else if !errors.Is(err, database.ErrArtistNotFound) {
return fmt.Errorf("sync artists: get settings for artist %s: %w", artist.ID, err)
}
if err := database.SaveArtistSettings(db, settings); err != nil {
return fmt.Errorf("sync artists: save artist %s: %w", artist.ID, err)
}
}
return nil
}