feat: wire main loop with periodic sync+scan and sync_interval config
Replace compute-only run() with an immediate sync+scan followed by a ticker-driven periodic loop, add the sync.interval config field (default 6h) with defaults and validation, and add tests for the scheduling logic.
This commit is contained in:
@@ -15,6 +15,7 @@ type Config struct {
|
||||
MusicBrainz MusicBrainzConfig `yaml:"musicbrainz"`
|
||||
Telegram TelegramConfig `yaml:"telegram"`
|
||||
Scanner ScannerConfig `yaml:"scanner"`
|
||||
Sync SyncConfig `yaml:"sync"`
|
||||
}
|
||||
|
||||
// ServerConfig holds HTTP server settings.
|
||||
@@ -46,6 +47,15 @@ type TelegramConfig struct {
|
||||
CronSchedule string `yaml:"cron_schedule"`
|
||||
}
|
||||
|
||||
// SyncConfig holds periodic sync pipeline settings.
|
||||
type SyncConfig struct {
|
||||
Interval time.Duration `yaml:"interval"`
|
||||
}
|
||||
|
||||
// DefaultSyncInterval is the default period between full sync+scan runs when
|
||||
// sync.interval is not specified in the config file.
|
||||
const DefaultSyncInterval = 6 * time.Hour
|
||||
|
||||
// ScannerConfig holds scanner engine parameters.
|
||||
//
|
||||
// Type filtering is handled in musicbrainz/api.go, not here: only Album/Single/EP
|
||||
@@ -95,6 +105,9 @@ func applyDefaults(cfg *Config) {
|
||||
if cfg.MusicBrainz.CacheTTL == 0 {
|
||||
cfg.MusicBrainz.CacheTTL = 24 * time.Hour
|
||||
}
|
||||
if cfg.Sync.Interval == 0 {
|
||||
cfg.Sync.Interval = DefaultSyncInterval
|
||||
}
|
||||
}
|
||||
|
||||
// validate checks that required fields are set and values are within acceptable ranges.
|
||||
@@ -117,5 +130,8 @@ func validate(cfg *Config) error {
|
||||
if cfg.MusicBrainz.UserAgent == "" {
|
||||
return fmt.Errorf("musicbrainz.user_agent is required")
|
||||
}
|
||||
if cfg.Sync.Interval <= 0 {
|
||||
return fmt.Errorf("sync.interval must be positive, got %v", cfg.Sync.Interval)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadConfig_Valid(t *testing.T) {
|
||||
@@ -338,3 +339,83 @@ func TestValidate_BoundaryPort(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_SyncIntervalDefault(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
yaml := `
|
||||
navidrome:
|
||||
url: "http://localhost:4533"
|
||||
user: "u"
|
||||
password: "p"
|
||||
|
||||
musicbrainz:
|
||||
user_agent: "NaviWatcher/1.0 ( test@example.com )"
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig returned error: %v", err)
|
||||
}
|
||||
if cfg.Sync.Interval != DefaultSyncInterval {
|
||||
t.Errorf("expected default sync interval %v, got %v", DefaultSyncInterval, cfg.Sync.Interval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_SyncIntervalParsed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
yaml := `
|
||||
navidrome:
|
||||
url: "http://localhost:4533"
|
||||
user: "u"
|
||||
password: "p"
|
||||
|
||||
musicbrainz:
|
||||
user_agent: "NaviWatcher/1.0 ( test@example.com )"
|
||||
|
||||
sync:
|
||||
interval: 30m
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig returned error: %v", err)
|
||||
}
|
||||
if cfg.Sync.Interval != 30*time.Minute {
|
||||
t.Errorf("expected sync interval 30m, got %v", cfg.Sync.Interval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_InvalidSyncInterval(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
yaml := `
|
||||
navidrome:
|
||||
url: "http://localhost:4533"
|
||||
user: "u"
|
||||
password: "p"
|
||||
|
||||
musicbrainz:
|
||||
user_agent: "NaviWatcher/1.0 ( test@example.com )"
|
||||
|
||||
sync:
|
||||
interval: -1s
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
if _, err := LoadConfig(path); err == nil {
|
||||
t.Fatal("expected error for negative sync interval, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ func (s *albumSyncer) SyncAlbums(ctx context.Context, db *database.DB) error {
|
||||
return s.syncAlbums(ctx, db)
|
||||
}
|
||||
|
||||
// NewAlbumSyncer adapts the given SyncAlbums function (typically
|
||||
// navidrome.SyncAlbums) into an AlbumSyncer for injection into SyncAll.
|
||||
func NewAlbumSyncer(syncAlbums func(ctx context.Context, db *database.DB) error) AlbumSyncer {
|
||||
return &albumSyncer{syncAlbums: syncAlbums}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user