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.
138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config represents the top-level configuration for NaviWatcher.
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
Navidrome NavidromeConfig `yaml:"navidrome"`
|
|
MusicBrainz MusicBrainzConfig `yaml:"musicbrainz"`
|
|
Telegram TelegramConfig `yaml:"telegram"`
|
|
Scanner ScannerConfig `yaml:"scanner"`
|
|
Sync SyncConfig `yaml:"sync"`
|
|
}
|
|
|
|
// ServerConfig holds HTTP server settings.
|
|
type ServerConfig struct {
|
|
Host string `yaml:"host"`
|
|
Port int `yaml:"port"`
|
|
Username string `yaml:"username"`
|
|
Password string `yaml:"password"`
|
|
}
|
|
|
|
// NavidromeConfig holds Subsonic API connection details.
|
|
type NavidromeConfig struct {
|
|
URL string `yaml:"url"`
|
|
User string `yaml:"user"`
|
|
Password string `yaml:"password"`
|
|
}
|
|
|
|
// MusicBrainzConfig holds MusicBrainz API settings.
|
|
type MusicBrainzConfig struct {
|
|
UserAgent string `yaml:"user_agent"`
|
|
CacheTTL time.Duration `yaml:"cache_ttl"`
|
|
}
|
|
|
|
// TelegramConfig holds Telegram bot notification settings.
|
|
type TelegramConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Token string `yaml:"token"`
|
|
ChatID string `yaml:"chat_id"`
|
|
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
|
|
// primary types (and release groups whose secondary types include Single/EP/Compilation)
|
|
// are included. Bootlegs are not explicitly excluded — a release group whose primary
|
|
// type is an included type but whose secondary types include "Bootleg" will still pass
|
|
// through and may be reported as missing. Compilations are included by default but can
|
|
// be excluded per-artist via artist_settings.ignore_compilations. These behaviours are
|
|
// not user-toggleable at the global config level, so there are no corresponding config fields.
|
|
type ScannerConfig struct {
|
|
FuzzyThreshold float64 `yaml:"fuzzy_threshold"`
|
|
}
|
|
|
|
// LoadConfig reads a YAML file from path, parses it, applies defaults,
|
|
// and validates the configuration.
|
|
func LoadConfig(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config file: %w", err)
|
|
}
|
|
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parse config: %w", err)
|
|
}
|
|
|
|
applyDefaults(&cfg)
|
|
|
|
if err := validate(&cfg); err != nil {
|
|
return nil, fmt.Errorf("validate config: %w", err)
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
// applyDefaults sets zero-value defaults for optional fields.
|
|
func applyDefaults(cfg *Config) {
|
|
if cfg.Server.Host == "" {
|
|
cfg.Server.Host = "0.0.0.0"
|
|
}
|
|
if cfg.Server.Port == 0 {
|
|
cfg.Server.Port = 8080
|
|
}
|
|
if cfg.Scanner.FuzzyThreshold == 0 {
|
|
cfg.Scanner.FuzzyThreshold = 0.85
|
|
}
|
|
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.
|
|
func validate(cfg *Config) error {
|
|
if cfg.Navidrome.URL == "" {
|
|
return fmt.Errorf("navidrome.url is required")
|
|
}
|
|
if cfg.Navidrome.User == "" {
|
|
return fmt.Errorf("navidrome.user is required")
|
|
}
|
|
if cfg.Navidrome.Password == "" {
|
|
return fmt.Errorf("navidrome.password is required")
|
|
}
|
|
if cfg.Server.Port < 1 || cfg.Server.Port > 65535 {
|
|
return fmt.Errorf("server.port must be between 1 and 65535, got %d", cfg.Server.Port)
|
|
}
|
|
if cfg.Scanner.FuzzyThreshold < 0.0 || cfg.Scanner.FuzzyThreshold > 1.0 {
|
|
return fmt.Errorf("scanner.fuzzy_threshold must be between 0.0 and 1.0, got %f", cfg.Scanner.FuzzyThreshold)
|
|
}
|
|
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
|
|
}
|