Add internal/config package with Config struct (Server, Navidrome, MusicBrainz, Telegram, Scanner sections), LoadConfig function for YAML parsing, config validation (required fields, port range, threshold range), and defaults. Include config.yaml.example matching spec. Update main.go to use real config package instead of placeholders. Add comprehensive tests covering valid configs, defaults, missing files, malformed YAML, and validation boundaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
3.0 KiB
Go
113 lines
3.0 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"`
|
|
}
|
|
|
|
// 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
|
|
}
|