// 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 } 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), } 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") } 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 }