package navidrome import ( "context" "errors" "fmt" "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 { return fmt.Errorf("sync albums: get albums for artist %s: %w", artist.ID, err) } // 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 }