Some checks failed
CI / v3 / Check ui (pull_request) Failing after 15s
CI / v3 / Test backend (pull_request) Failing after 16s
CI / v3 / Docker / backend (pull_request) Has been skipped
CI / v3 / Docker / runner (pull_request) Has been skipped
CI / v3 / Docker / ui (pull_request) Has been skipped
- Remove all pre-v3 code: scraper, ui-v2, backend v1, ios v1+v2, legacy CI workflows - Flatten v3/ contents to repo root - Add Doppler secrets management (project=libnovel, config=prd) - Add justfile with doppler run wrappers for all docker compose commands - Strip hardcoded env fallbacks from docker-compose.yml - Add minimal README.md - Clean up .gitignore
226 lines
7.4 KiB
Go
226 lines
7.4 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
|
|
// SkipInitialCatalogueRefresh prevents the runner from running a full
|
|
// catalogue walk on startup. Useful for quick restarts where the catalogue
|
|
// is already indexed and a 24h walk would be wasteful.
|
|
// Controlled by RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH=true.
|
|
SkipInitialCatalogueRefresh bool
|
|
}
|
|
|
|
// 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", "chapters"),
|
|
BucketAudio: envOr("MINIO_BUCKET_AUDIO", "audio"),
|
|
BucketAvatars: envOr("MINIO_BUCKET_AVATARS", "avatars"),
|
|
BucketBrowse: envOr("MINIO_BUCKET_BROWSE", "catalogue"),
|
|
},
|
|
|
|
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),
|
|
SkipInitialCatalogueRefresh: envBool("RUNNER_SKIP_INITIAL_CATALOGUE_REFRESH", false),
|
|
},
|
|
|
|
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
|
|
}
|