Files
NaviWatcher/internal/config/config.go
Vladimir Zagainov 389d177d85 fix: address code review findings
- Fix duplicate Telegram notifications: SyncArtistDiscography no longer wipes
  notifications_sent for the whole artist on every cache-miss re-sync; only
  markers for releases that disappear are pruned (FK-safe via INSERT OR REPLACE
  + rgid NOT IN (...)).
- Cache empty MusicBrainz discographies via a new artist_settings.last_synced
  column (migration 009) so zero-release artists honor the TTL instead of being
  re-fetched every cycle.
- Wire the Web UI server and Telegram notifier scheduler into main.run/NewApp.
- Guard startPeriodicSync against overlapping syncs with a done-channel slot.
- Add server.public_url config; NewServerWithConfig derives reachable links
  and no longer advertises the 0.0.0.0 bind address.
- Web handlers: use scanner.ScanArtist per artist, drop always-false
  releaseIgnored lookup and dead endsWith, thread configured threshold.
- Limit :memory: DB pool to one connection so migrations and queries share the
  same in-memory store.
2026-07-19 23:38:33 +03:00

139 lines
4.2 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"`
PublicURL string `yaml:"public_url"`
}
// 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
}