Files
Bellhop/internal/config/config.go
prosolis 5a706fedc4 Begin Go rewrite: remove Python, scaffold module and config
Pivot from FastAPI web portal to a Matrix command bot (modeled on Pete).
Users will issue !movie / !tv / !music commands in allowlisted rooms; the
bot performs a top-hit search against Radarr/Sonarr/Lidarr and adds it.

This commit is session 1 of a multi-session rewrite (see SESSION_PLAN.md):
  - Delete app/, requirements.txt, old Dockerfile, .env.example
  - Add go.mod (mautrix-go + yaml.v3)
  - Add internal/config: YAML loader with ${ENV} expansion, validates
    matrix creds, allowed_rooms, and per-service *arr config
  - Reset .gitignore / .dockerignore for the Go layout
2026-05-24 20:09:52 -07:00

128 lines
3.2 KiB
Go

package config
import (
"fmt"
"log/slog"
"os"
"regexp"
"gopkg.in/yaml.v3"
)
var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`)
type Config struct {
Matrix MatrixConfig `yaml:"matrix"`
Services ServicesConfig `yaml:"services"`
}
type MatrixConfig struct {
Homeserver string `yaml:"homeserver"`
UserID string `yaml:"user_id"`
Password string `yaml:"password"`
PickleKey string `yaml:"pickle_key"`
DisplayName string `yaml:"display_name"`
DataDir string `yaml:"data_dir"`
CommandPrefix string `yaml:"command_prefix"`
// AllowedRooms is the set of room IDs the bot listens for commands in.
// Messages in any other room are ignored.
AllowedRooms []string `yaml:"allowed_rooms"`
}
type ServicesConfig struct {
Radarr *ArrConfig `yaml:"radarr"`
Sonarr *ArrConfig `yaml:"sonarr"`
Lidarr *ArrConfig `yaml:"lidarr"`
}
type ArrConfig struct {
URL string `yaml:"url"`
APIKey string `yaml:"api_key"`
QualityProfileID int `yaml:"quality_profile_id"`
RootFolder string `yaml:"root_folder"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
expanded := envBracketRe.ReplaceAllStringFunc(string(data), func(match string) string {
varName := match[2 : len(match)-1]
val := os.Getenv(varName)
if val == "" {
slog.Warn("config: env var referenced but not set", "var", varName)
}
return val
})
var cfg Config
if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
cfg.applyDefaults()
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
}
return &cfg, nil
}
func (c *Config) validate() error {
if c.Matrix.Homeserver == "" {
return fmt.Errorf("matrix.homeserver is required")
}
if c.Matrix.UserID == "" {
return fmt.Errorf("matrix.user_id is required")
}
if c.Matrix.Password == "" {
return fmt.Errorf("matrix.password is required")
}
if len(c.Matrix.AllowedRooms) == 0 {
return fmt.Errorf("matrix.allowed_rooms must have at least one entry")
}
if c.Services.Radarr == nil && c.Services.Sonarr == nil && c.Services.Lidarr == nil {
return fmt.Errorf("at least one of services.radarr/sonarr/lidarr must be configured")
}
for name, svc := range map[string]*ArrConfig{
"radarr": c.Services.Radarr,
"sonarr": c.Services.Sonarr,
"lidarr": c.Services.Lidarr,
} {
if svc == nil {
continue
}
if svc.URL == "" {
return fmt.Errorf("services.%s.url is required when service is set", name)
}
if svc.APIKey == "" {
return fmt.Errorf("services.%s.api_key is required when service is set", name)
}
if svc.RootFolder == "" {
return fmt.Errorf("services.%s.root_folder is required when service is set", name)
}
if svc.QualityProfileID == 0 {
return fmt.Errorf("services.%s.quality_profile_id is required when service is set", name)
}
}
return nil
}
func (c *Config) applyDefaults() {
if c.Matrix.DataDir == "" {
c.Matrix.DataDir = "./data"
}
if c.Matrix.DisplayName == "" {
c.Matrix.DisplayName = "Bellhop"
}
if c.Matrix.PickleKey == "" {
c.Matrix.PickleKey = "bellhop_pickle_key"
}
if c.Matrix.CommandPrefix == "" {
c.Matrix.CommandPrefix = "!"
}
}