257 lines
10 KiB
Go
257 lines
10 KiB
Go
// Package config loads the gateway's settings from the environment.
|
|
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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
|
|
// 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
|
|
// RecommendationWeights is an optional JSON overlay on the weighted defaults.
|
|
RecommendationWeights string
|
|
|
|
UpstreamTimeout time.Duration
|
|
|
|
// EmbyHealthInterval paces the reachability probe behind the "server not responding"
|
|
// and "back online" banners. One probe per gateway, not per TV. Zero disables it.
|
|
EmbyHealthInterval 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
|
|
// Five-day "Shows airing" 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 five-day schedule row.
|
|
SonarrAlertWindow time.Duration
|
|
|
|
// Radarr is optional. Its calendar supplies the five-day digital movie release row.
|
|
RadarrURL string
|
|
RadarrAPIKey string
|
|
RadarrTTL time.Duration
|
|
RadarrLocation *time.Location
|
|
|
|
// RadarrWebhookToken guards the Radarr "on import" webhook. Empty means the hook
|
|
// 404s, so an unconfigured deployment cannot be fed alerts by anyone who finds it.
|
|
RadarrWebhookToken string
|
|
// RadarrAlertWindow is how long an imported movie keeps being announced to clients.
|
|
// It is a window rather than a one-shot push because the gateway has no connection
|
|
// to a TV that is switched off: a set turned on inside the window still gets the
|
|
// news, and each TV dedupes by alert id. Zero turns the banners off.
|
|
RadarrAlertWindow 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
|
|
// Prepared pools rebuild daily at RebuildHour in the configured timezone. The age
|
|
// settings are safety bounds for manual/background fallback paths.
|
|
ForYouMinRebuildAge time.Duration
|
|
ForYouRefreshInterval time.Duration
|
|
ForYouRebuildHour int
|
|
}
|
|
|
|
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", 24*time.Hour),
|
|
RecommendTimeout: duration("MEMBY_RECOMMEND_TIMEOUT", 60*time.Second),
|
|
RecommendationWeights: strings.TrimSpace(
|
|
os.Getenv("MEMBY_RECOMMENDATION_WEIGHTS"),
|
|
),
|
|
|
|
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),
|
|
EmbyHealthInterval: duration("MEMBY_EMBY_HEALTH_INTERVAL", 60*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),
|
|
RadarrURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_RADARR_URL")), "/"),
|
|
RadarrAPIKey: strings.TrimSpace(os.Getenv("MEMBY_RADARR_API_KEY")),
|
|
RadarrTTL: duration("MEMBY_RADARR_TTL", 5*time.Minute),
|
|
RadarrWebhookToken: strings.TrimSpace(os.Getenv("MEMBY_RADARR_WEBHOOK_TOKEN")),
|
|
RadarrAlertWindow: duration("MEMBY_RADARR_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", 24*time.Hour),
|
|
ForYouRefreshInterval: duration("MEMBY_FOR_YOU_REFRESH_INTERVAL", 24*time.Hour),
|
|
ForYouRebuildHour: integer("MEMBY_FOR_YOU_REBUILD_HOUR", 4),
|
|
}
|
|
|
|
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
|
|
}
|
|
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.RadarrURL == "") != (c.RadarrAPIKey == "") {
|
|
return c, fmt.Errorf("MEMBY_RADARR_URL and MEMBY_RADARR_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")
|
|
}
|
|
if c.ForYouRebuildHour < 0 || c.ForYouRebuildHour > 23 {
|
|
return c, fmt.Errorf("MEMBY_FOR_YOU_REBUILD_HOUR must be between 0 and 23")
|
|
}
|
|
if c.RecommendationWeights != "" && !json.Valid([]byte(c.RecommendationWeights)) {
|
|
return c, fmt.Errorf("MEMBY_RECOMMENDATION_WEIGHTS must be valid JSON")
|
|
}
|
|
location, err := time.LoadLocation(env("MEMBY_TIMEZONE", "Pacific/Auckland"))
|
|
if err != nil {
|
|
return c, fmt.Errorf("MEMBY_TIMEZONE: %w", err)
|
|
}
|
|
c.SonarrLocation = location
|
|
c.RadarrLocation = 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
|
|
}
|