- New Go backend binary (backend + runner) replacing old scraper/ - Rename SCRAPER_API_URL → BACKEND_API_URL in UI env and docker-compose - Rename scraperFetch → backendFetch across all 19 UI server files - Remove SCRAPER_PROXY env var and proxy transport from browser.Config - Add Meilisearch, Valkey, Caddy to docker-compose - Add docs/: api-endpoints.md, request-flow.mermaid.md, data-flow.mermaid.md
220 lines
7.0 KiB
Go
220 lines
7.0 KiB
Go
// 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
|
|
}
|
|
|
|
// Meilisearch holds connection settings for the Meilisearch full-text search service.
|
|
type Meilisearch struct {
|
|
// URL is the base URL of the Meilisearch instance, e.g. http://localhost:7700
|
|
// An empty string disables Meilisearch indexing and search.
|
|
URL string
|
|
// APIKey is the Meilisearch master/search API key.
|
|
APIKey string
|
|
}
|
|
|
|
// Valkey holds connection settings for the Valkey/Redis presign URL cache.
|
|
type Valkey struct {
|
|
// Addr is the host:port of the Valkey instance, e.g. localhost:6379
|
|
// An empty string disables the Valkey cache (falls through to MinIO directly).
|
|
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
|
|
// MetricsAddr is the listen address for the runner /metrics HTTP endpoint.
|
|
// Defaults to ":9091". Set to "" to disable.
|
|
MetricsAddr string
|
|
// CatalogueRefreshInterval is how often the runner walks the full catalogue,
|
|
// scrapes per-book metadata, downloads covers, and re-indexes in Meilisearch.
|
|
// Defaults to 24h. Set to 0 to use the default.
|
|
CatalogueRefreshInterval time.Duration
|
|
}
|
|
|
|
// Config is the top-level configuration struct consumed by both binaries.
|
|
type Config struct {
|
|
PocketBase PocketBase
|
|
MinIO MinIO
|
|
Kokoro Kokoro
|
|
HTTP HTTP
|
|
Runner Runner
|
|
Meilisearch Meilisearch
|
|
Valkey Valkey
|
|
// 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),
|
|
MetricsAddr: envOr("RUNNER_METRICS_ADDR", ":9091"),
|
|
CatalogueRefreshInterval: envDuration("RUNNER_CATALOGUE_REFRESH_INTERVAL", 0),
|
|
},
|
|
|
|
Meilisearch: Meilisearch{
|
|
URL: envOr("MEILI_URL", ""),
|
|
APIKey: envOr("MEILI_API_KEY", ""),
|
|
},
|
|
|
|
Valkey: Valkey{
|
|
Addr: envOr("VALKEY_ADDR", ""),
|
|
},
|
|
}
|
|
}
|
|
|
|
// ── 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
|
|
}
|