// Package config loads all service configuration from environment variables. // Both the runner and backend binaries call config.Load() at startup; each // uses only the sub-struct relevant to it. // // Every field has a documented default so the service starts sensibly without // any environment configuration (useful for local development). package config import ( "os" "strconv" "strings" "time" ) // PocketBase holds connection settings for the remote PocketBase instance. type PocketBase struct { // URL is the base URL of the PocketBase instance, e.g. https://pb.libnovel.cc URL string // AdminEmail is the admin account email used for API authentication. AdminEmail string // AdminPassword is the admin account password. AdminPassword string } // MinIO holds connection settings for the remote MinIO / S3-compatible store. type MinIO struct { // Endpoint is the host:port of the MinIO S3 API, e.g. storage.libnovel.cc:443 Endpoint string // PublicEndpoint is the browser-visible endpoint used for presigned URLs. // Falls back to Endpoint when empty. PublicEndpoint string // AccessKey is the MinIO access key. AccessKey string // SecretKey is the MinIO secret key. SecretKey string // UseSSL enables TLS for the internal MinIO connection. UseSSL bool // PublicUseSSL enables TLS for presigned URL generation. PublicUseSSL bool // BucketChapters is the bucket that holds chapter markdown objects. BucketChapters string // BucketAudio is the bucket that holds generated audio MP3 objects. BucketAudio string // BucketAvatars is the bucket that holds user avatar images. BucketAvatars string // BucketBrowse is the bucket that holds cached browse page snapshots (JSON). BucketBrowse string } // Kokoro holds connection settings for the Kokoro-FastAPI TTS service. type Kokoro struct { // URL is the base URL of the Kokoro service, e.g. https://kokoro.libnovel.cc // An empty string disables TTS generation. URL string // DefaultVoice is the voice used when none is specified. DefaultVoice string } // HTTP holds settings for the HTTP server (backend only). type HTTP struct { // Addr is the listen address, e.g. ":8080" Addr string } // Runner holds settings specific to the runner/worker binary. type Runner struct { // PollInterval is how often the runner checks PocketBase for pending tasks. PollInterval time.Duration // MaxConcurrentScrape limits simultaneous book-scrape goroutines. MaxConcurrentScrape int // MaxConcurrentAudio limits simultaneous audio-generation goroutines. MaxConcurrentAudio int // WorkerID is a unique identifier for this runner instance. // Defaults to the system hostname. WorkerID string // Workers is the number of chapter-scraping goroutines per book. Workers int // Timeout is the per-request HTTP timeout for scraping. Timeout time.Duration // ProxyURL is an optional outbound proxy for scraper HTTP requests. ProxyURL string } // Config is the top-level configuration struct consumed by both binaries. type Config struct { PocketBase PocketBase MinIO MinIO Kokoro Kokoro HTTP HTTP Runner Runner // LogLevel is one of "debug", "info", "warn", "error". LogLevel string } // Load reads all configuration from environment variables and returns a // populated Config. Missing variables fall back to documented defaults. func Load() Config { workerID, _ := os.Hostname() if workerID == "" { workerID = "runner-default" } return Config{ LogLevel: envOr("LOG_LEVEL", "info"), PocketBase: PocketBase{ URL: envOr("POCKETBASE_URL", "http://localhost:8090"), AdminEmail: envOr("POCKETBASE_ADMIN_EMAIL", "admin@libnovel.local"), AdminPassword: envOr("POCKETBASE_ADMIN_PASSWORD", "changeme123"), }, MinIO: MinIO{ Endpoint: envOr("MINIO_ENDPOINT", "localhost:9000"), PublicEndpoint: envOr("MINIO_PUBLIC_ENDPOINT", ""), AccessKey: envOr("MINIO_ACCESS_KEY", "admin"), SecretKey: envOr("MINIO_SECRET_KEY", "changeme123"), UseSSL: envBool("MINIO_USE_SSL", false), PublicUseSSL: envBool("MINIO_PUBLIC_USE_SSL", true), BucketChapters: envOr("MINIO_BUCKET_CHAPTERS", "libnovel-chapters"), BucketAudio: envOr("MINIO_BUCKET_AUDIO", "libnovel-audio"), BucketAvatars: envOr("MINIO_BUCKET_AVATARS", "libnovel-avatars"), BucketBrowse: envOr("MINIO_BUCKET_BROWSE", "libnovel-browse"), }, Kokoro: Kokoro{ URL: envOr("KOKORO_URL", ""), DefaultVoice: envOr("KOKORO_VOICE", "af_bella"), }, HTTP: HTTP{ Addr: envOr("BACKEND_HTTP_ADDR", ":8080"), }, Runner: Runner{ PollInterval: envDuration("RUNNER_POLL_INTERVAL", 30*time.Second), MaxConcurrentScrape: envInt("RUNNER_MAX_CONCURRENT_SCRAPE", 1), MaxConcurrentAudio: envInt("RUNNER_MAX_CONCURRENT_AUDIO", 1), WorkerID: envOr("RUNNER_WORKER_ID", workerID), Workers: envInt("RUNNER_WORKERS", 0), // 0 → runtime.NumCPU() Timeout: envDuration("RUNNER_TIMEOUT", 90*time.Second), ProxyURL: envOr("SCRAPER_PROXY", ""), }, } } // ── helpers ─────────────────────────────────────────────────────────────────── func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func envBool(key string, fallback bool) bool { v := os.Getenv(key) if v == "" { return fallback } return strings.ToLower(v) == "true" } func envInt(key string, fallback int) int { v := os.Getenv(key) if v == "" { return fallback } n, err := strconv.Atoi(v) if err != nil || n < 0 { return fallback } return n } func envDuration(key string, fallback time.Duration) time.Duration { v := os.Getenv(key) if v == "" { return fallback } d, err := time.ParseDuration(v) if err != nil { return fallback } return d }