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"` } // 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"` } // ScannerConfig holds scanner engine parameters. type ScannerConfig struct { FuzzyThreshold float64 `yaml:"fuzzy_threshold"` IgnoreBootlegs bool `yaml:"ignore_bootlegs"` IncludeCompilations bool `yaml:"include_compilations"` } // 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 } } // 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") } return nil }