Files
memby/server/internal/config/config.go
T
2026-07-29 15:26:27 +12:00

221 lines
8.3 KiB
Go

// Package config loads the gateway's settings from the environment.
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
_ "time/tzdata"
)
type Config struct {
ListenAddr string
// EmbyURL is how the gateway itself reaches Emby (may be a private/docker address).
EmbyURL string
// EmbyPublicURL is the address handed to TV clients for direct video playback.
// Defaults to EmbyURL; set it when the gateway talks to Emby over a network the
// TVs cannot reach.
EmbyPublicURL string
DatabaseURL string
RedisURL string
// ClientName is reported to Emby in the X-Emby-Authorization header, so sessions
// show up as this in Emby's dashboard.
ClientName string
HomeTTL time.Duration
ItemTTL time.Duration
SearchTTL time.Duration
ScreensaverTTL time.Duration
SessionTTL time.Duration
// SessionIdleExpiry retires gateway tokens that go unused for this long.
SessionIdleExpiry time.Duration
// MaxClientsPerUser is enforced transactionally when a new TV signs in.
MaxClientsPerUser int
// RecommendTTL is how long computed recommendation rows stay warm. Long, because
// taste moves slowly and each rebuild costs several Emby queries.
RecommendTTL time.Duration
// RecommendTimeout bounds a background rebuild, which fans out further than a
// normal request and so needs more headroom than UpstreamTimeout.
RecommendTimeout time.Duration
UpstreamTimeout time.Duration
// AdminToken guards the operator interface. Empty disables /admin entirely, so an
// unconfigured deployment cannot leave it exposed.
AdminToken string
// PublicURL is the externally reachable Memby gateway address used in update links.
PublicURL string
// ReleaseDir persists signed APKs published by CI.
ReleaseDir string
// ReleasePublishToken authorizes the CI-only release upload endpoint. It is separate
// from AdminToken so a compromised build runner cannot change maintenance settings.
ReleasePublishToken string
// SyncInterval is how often the library import runs. Zero disables the schedule.
SyncInterval time.Duration
// SyncTimeout bounds one import; a full pass over a large library is slow.
SyncTimeout time.Duration
// SyncOnStart triggers an incremental import at boot.
SyncOnStart bool
// SyncUserID / SyncAPIKey are an optional Emby service account for imports. Without
// them the newest TV session is borrowed instead.
SyncUserID string
SyncAPIKey string
// AnalyticsRetention is how long raw row events are kept before being pruned.
AnalyticsRetention time.Duration
// Sonarr is optional. When configured, its local calendar supplies the informational
// "Shows airing today" home row. The API key never leaves this server.
SonarrURL string
SonarrAPIKey string
SonarrTTL time.Duration
SonarrLocation *time.Location
// SonarrAlertWindow is how long after an episode airs the "aired, coming soon"
// banner keeps being offered to clients. Zero turns the banners off without
// touching the airing-today row.
SonarrAlertWindow time.Duration
// Tracearr is an optional, read-only source of completion, session-length and
// direct-play signals for the per-user For You area. The public API key stays in
// the gateway and is never returned to a TV.
TracearrURL string
TracearrAPIKey string
TracearrServerID string
// TracearrSyncInterval imports recent changed sessions. FullInterval reconciles
// late/out-of-order updates and deletions without needing a source cursor.
TracearrSyncInterval time.Duration
TracearrFullInterval time.Duration
// ForYouMinRebuildAge coalesces bursts of playback/library changes. RefreshInterval
// is the acceptable age of a prepared pool before it is refreshed.
ForYouMinRebuildAge time.Duration
ForYouRefreshInterval time.Duration
}
func Load() (Config, error) {
c := Config{
ListenAddr: env("MEMBY_LISTEN_ADDR", ":8080"),
EmbyURL: strings.TrimRight(os.Getenv("MEMBY_EMBY_URL"), "/"),
EmbyPublicURL: strings.TrimRight(os.Getenv("MEMBY_EMBY_PUBLIC_URL"), "/"),
DatabaseURL: os.Getenv("MEMBY_DATABASE_URL"),
RedisURL: env("MEMBY_REDIS_URL", "redis://localhost:6379/0"),
ClientName: env("MEMBY_CLIENT_NAME", "Memby"),
HomeTTL: duration("MEMBY_HOME_TTL", 60*time.Second),
ItemTTL: duration("MEMBY_ITEM_TTL", 10*time.Minute),
SearchTTL: duration("MEMBY_SEARCH_TTL", 5*time.Minute),
ScreensaverTTL: duration("MEMBY_SCREENSAVER_TTL", 10*time.Minute),
SessionTTL: duration("MEMBY_SESSION_CACHE_TTL", 5*time.Minute),
SessionIdleExpiry: duration("MEMBY_SESSION_IDLE_EXPIRY", 90*24*time.Hour),
MaxClientsPerUser: integer("MEMBY_MAX_CLIENTS_PER_USER", 1),
RecommendTTL: duration("MEMBY_RECOMMEND_TTL", 2*time.Hour),
RecommendTimeout: duration("MEMBY_RECOMMEND_TIMEOUT", 60*time.Second),
AdminToken: strings.TrimSpace(os.Getenv("MEMBY_ADMIN_TOKEN")),
PublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_PUBLIC_URL")), "/"),
ReleaseDir: env("MEMBY_RELEASE_DIR", "/data/releases"),
ReleasePublishToken: strings.TrimSpace(
os.Getenv("MEMBY_RELEASE_PUBLISH_TOKEN"),
),
SyncInterval: duration("MEMBY_SYNC_INTERVAL", time.Hour),
SyncTimeout: duration("MEMBY_SYNC_TIMEOUT", 30*time.Minute),
SyncOnStart: boolean("MEMBY_SYNC_ON_START", false),
SyncUserID: strings.TrimSpace(os.Getenv("MEMBY_SYNC_USER_ID")),
SyncAPIKey: strings.TrimSpace(os.Getenv("MEMBY_SYNC_API_KEY")),
AnalyticsRetention: duration("MEMBY_ANALYTICS_RETENTION", 90*24*time.Hour),
UpstreamTimeout: duration("MEMBY_UPSTREAM_TIMEOUT", 20*time.Second),
SonarrURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_SONARR_URL")), "/"),
SonarrAPIKey: strings.TrimSpace(os.Getenv("MEMBY_SONARR_API_KEY")),
SonarrTTL: duration("MEMBY_SONARR_TTL", 5*time.Minute),
SonarrAlertWindow: duration("MEMBY_SONARR_ALERT_WINDOW", 3*time.Hour),
TracearrURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_TRACEARR_URL")), "/"),
TracearrAPIKey: strings.TrimSpace(os.Getenv("MEMBY_TRACEARR_API_KEY")),
TracearrServerID: strings.TrimSpace(os.Getenv("MEMBY_TRACEARR_SERVER_ID")),
TracearrSyncInterval: duration("MEMBY_TRACEARR_SYNC_INTERVAL", 5*time.Minute),
TracearrFullInterval: duration("MEMBY_TRACEARR_FULL_INTERVAL", 24*time.Hour),
ForYouMinRebuildAge: duration("MEMBY_FOR_YOU_MIN_REBUILD_AGE", 10*time.Minute),
ForYouRefreshInterval: duration("MEMBY_FOR_YOU_REFRESH_INTERVAL", 30*time.Minute),
}
if c.EmbyURL == "" {
return c, fmt.Errorf("MEMBY_EMBY_URL is required")
}
if c.DatabaseURL == "" {
return c, fmt.Errorf("MEMBY_DATABASE_URL is required")
}
if c.MaxClientsPerUser < 1 {
return c, fmt.Errorf("MEMBY_MAX_CLIENTS_PER_USER must be at least 1")
}
if c.EmbyPublicURL == "" {
c.EmbyPublicURL = c.EmbyURL
}
if c.ReleasePublishToken != "" && c.PublicURL == "" {
return c, fmt.Errorf("MEMBY_PUBLIC_URL is required when release publishing is enabled")
}
if (c.SonarrURL == "") != (c.SonarrAPIKey == "") {
return c, fmt.Errorf("MEMBY_SONARR_URL and MEMBY_SONARR_API_KEY must be set together")
}
if (c.TracearrURL == "") != (c.TracearrAPIKey == "") {
return c, fmt.Errorf("MEMBY_TRACEARR_URL and MEMBY_TRACEARR_API_KEY must be set together")
}
location, err := time.LoadLocation(env("MEMBY_TIMEZONE", "Pacific/Auckland"))
if err != nil {
return c, fmt.Errorf("MEMBY_TIMEZONE: %w", err)
}
c.SonarrLocation = location
return c, nil
}
func env(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
func boolean(key string, fallback bool) bool {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
value, err := strconv.ParseBool(raw)
if err != nil {
return fallback
}
return value
}
func duration(key string, fallback time.Duration) time.Duration {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
if d, err := time.ParseDuration(raw); err == nil {
return d
}
// Bare numbers are read as seconds, which is friendlier in a compose file.
if secs, err := strconv.Atoi(raw); err == nil {
return time.Duration(secs) * time.Second
}
return fallback
}
func integer(key string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
value, err := strconv.Atoi(raw)
if err != nil {
return fallback
}
return value
}