Files
NaviWatcher/internal/navidrome/sync.go
Vladimir Zagainov 44f3b0a2a7 feat: add Web UI artist detail, archive, and ignore actions
Implements Task 8: artist detail page (local albums + found-missing
with ignore buttons), ignored-releases archive with restore, and POST
handlers toggling ignore flags and ignore_singles. Adds ErrArtistNotFound
sentinel so callers can distinguish missing artists, and wires routes via
the enhanced ServeMux path wildcard.
2026-07-19 22:43:47 +03:00

124 lines
3.8 KiB
Go

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.Monitored = existing.Monitored
settings.IgnoreSingles = existing.IgnoreSingles
settings.IgnoreCompilations = existing.IgnoreCompilations
} 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
}