// Package config загружает конфигурацию jellybit из TOML-файла. package config import ( "errors" "fmt" "os" "time" "github.com/pelletier/go-toml/v2" ) // Config — корневая конфигурация сервиса (см. config.example.toml). type Config struct { QBittorrent QBittorrent `toml:"qbittorrent"` Paths Paths `toml:"paths"` Storage Storage `toml:"storage"` LLM LLM `toml:"llm"` Metadata Metadata `toml:"metadata"` Worker Worker `toml:"worker"` Recognition Recognition `toml:"recognition"` Telegram Telegram `toml:"telegram"` HTTP HTTP `toml:"http"` Log Log `toml:"log"` } // QBittorrent — доступ к qBittorrent WebUI и раскладка путей загрузок. type QBittorrent struct { URL string `toml:"url"` Username string `toml:"username"` Password string `toml:"password"` Category string `toml:"category"` SavePath string `toml:"savepath"` PathMap map[string]string `toml:"path_map"` } // Paths — хост-пути медиа-песочницы (см. docs/specs/architecture.md). type Paths struct { Downloads string `toml:"downloads"` Movies string `toml:"movies"` Series string `toml:"series"` } // Storage — расположение БД. type Storage struct { DBPath string `toml:"db_path"` } // LLM — провайдер распознавания (дискриминатор type). type LLM struct { Type string `toml:"type"` BaseURL string `toml:"base_url"` APIKey string `toml:"api_key"` Model string `toml:"model"` Proxy string `toml:"proxy"` Timeout Duration `toml:"timeout"` MaxRetries int `toml:"max_retries"` } // Metadata — внешние базы метаданных (опциональны). type Metadata struct { TMDB MetadataProvider `toml:"tmdb"` TVDB MetadataProvider `toml:"tvdb"` TVMaze MetadataProvider `toml:"tvmaze"` // без ключа, только сериалы } // MetadataProvider — настройки одного провайдера метаданных. У keyless-баз // (TVMaze) поле api_key не используется. type MetadataProvider struct { Enabled bool `toml:"enabled"` APIKey string `toml:"api_key"` Proxy string `toml:"proxy"` Timeout Duration `toml:"timeout"` } // Worker — параметры фонового цикла. type Worker struct { PollInterval Duration `toml:"poll_interval"` StuckAfter Duration `toml:"stuck_after"` MagnetTimeout Duration `toml:"magnet_timeout"` } // Recognition — пороги распознавания. type Recognition struct { AutoConfidenceThreshold float64 `toml:"auto_confidence_threshold"` } // Telegram — настройки бота (Ф5). type Telegram struct { Enabled bool `toml:"enabled"` Token string `toml:"token"` AllowedUserIDs []int64 `toml:"allowed_user_ids"` WebBaseURL string `toml:"web_base_url"` // для deep-link «открыть в вебе» (опц.) } // HTTP — параметры веб-сервера. type HTTP struct { Listen string `toml:"listen"` TrustedSubnets []string `toml:"trusted_subnets"` } // Log — параметры логирования. type Log struct { Level string `toml:"level"` Format string `toml:"format"` } // Duration — time.Duration, читаемый из TOML-строки вида "5s". type Duration time.Duration // UnmarshalText разбирает строку длительности (encoding.TextUnmarshaler). func (d *Duration) UnmarshalText(text []byte) error { v, err := time.ParseDuration(string(text)) if err != nil { return err } *d = Duration(v) return nil } // Std возвращает обычный time.Duration. func (d Duration) Std() time.Duration { return time.Duration(d) } // Default возвращает конфиг с разумными умолчаниями; значения из файла // перекрывают их при загрузке. func Default() *Config { return &Config{ QBittorrent: QBittorrent{ URL: "http://qbit:8989", Username: "admin", Category: "jellybit", SavePath: "/srv/media/downloads", }, Paths: Paths{ Downloads: "/srv/media/downloads", Movies: "/srv/media/movies", Series: "/srv/media/series", }, Storage: Storage{DBPath: "/data/jellybit.db"}, LLM: LLM{ Type: "openai-compat", Timeout: Duration(120 * time.Second), MaxRetries: 3, }, Metadata: Metadata{ TMDB: MetadataProvider{Timeout: Duration(10 * time.Second)}, TVDB: MetadataProvider{Timeout: Duration(10 * time.Second)}, }, Worker: Worker{ PollInterval: Duration(5 * time.Second), StuckAfter: Duration(time.Hour), MagnetTimeout: Duration(30 * time.Minute), }, Recognition: Recognition{AutoConfidenceThreshold: 0.85}, HTTP: HTTP{Listen: ":8080"}, Log: Log{Level: "info", Format: "json"}, } } // Load читает и валидирует конфиг из path. func Load(path string) (*Config, error) { cfg := Default() data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config %q: %w", path, err) } if err := toml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parse config %q: %w", path, err) } if err := cfg.validate(); err != nil { return nil, fmt.Errorf("invalid config %q: %w", path, err) } return cfg, nil } func (c *Config) validate() error { if c.HTTP.Listen == "" { return errors.New("http.listen is empty") } if c.Storage.DBPath == "" { return errors.New("storage.db_path is empty") } if c.LLM.Type != "openai-compat" { return fmt.Errorf("unsupported llm.type %q (supported: openai-compat)", c.LLM.Type) } return nil }