// Package config loads the gateway's settings from the environment. package config import ( "fmt" "os" "strconv" "strings" "time" ) 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 // 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 // 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 } 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), RecommendTTL: duration("MEMBY_RECOMMEND_TTL", 2*time.Hour), RecommendTimeout: duration("MEMBY_RECOMMEND_TIMEOUT", 60*time.Second), AdminToken: strings.TrimSpace(os.Getenv("MEMBY_ADMIN_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), } 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.EmbyPublicURL == "" { c.EmbyPublicURL = c.EmbyURL } 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 }