189 lines
5.8 KiB
Go
189 lines
5.8 KiB
Go
// 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
|
|
}
|