Publish current app and server
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -34,18 +35,21 @@ type Config struct {
|
||||
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
|
||||
// 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
|
||||
@@ -73,7 +77,7 @@ type Config struct {
|
||||
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.
|
||||
// Five-day "Shows airing" home row. The API key never leaves this server.
|
||||
SonarrURL string
|
||||
SonarrAPIKey string
|
||||
SonarrTTL time.Duration
|
||||
@@ -81,9 +85,24 @@ type Config struct {
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
@@ -94,10 +113,11 @@ type Config struct {
|
||||
// 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.
|
||||
// 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) {
|
||||
@@ -114,9 +134,11 @@ func Load() (Config, error) {
|
||||
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),
|
||||
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")), "/"),
|
||||
@@ -131,17 +153,24 @@ func Load() (Config, error) {
|
||||
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", 10*time.Minute),
|
||||
ForYouRefreshInterval: duration("MEMBY_FOR_YOU_REFRESH_INTERVAL", 30*time.Minute),
|
||||
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 == "" {
|
||||
@@ -150,9 +179,6 @@ func Load() (Config, error) {
|
||||
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
|
||||
}
|
||||
@@ -162,14 +188,24 @@ func Load() (Config, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultClientAllowanceIsOne(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_MAX_CLIENTS_PER_USER", "")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.MaxClientsPerUser != 1 {
|
||||
t.Fatalf("MaxClientsPerUser = %d, want 1", cfg.MaxClientsPerUser)
|
||||
}
|
||||
}
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTracearrURLAndKeyMustBeConfiguredTogether(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
@@ -26,3 +15,51 @@ func TestTracearrURLAndKeyMustBeConfiguredTogether(t *testing.T) {
|
||||
t.Fatal("expected incomplete Tracearr configuration to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRadarrURLAndKeyMustBeConfiguredTogether(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_RADARR_URL", "http://radarr")
|
||||
t.Setenv("MEMBY_RADARR_API_KEY", "")
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected incomplete Radarr configuration to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForYouDefaultsToDailyOffPeakRebuild(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_FOR_YOU_MIN_REBUILD_AGE", "")
|
||||
t.Setenv("MEMBY_FOR_YOU_REFRESH_INTERVAL", "")
|
||||
t.Setenv("MEMBY_FOR_YOU_REBUILD_HOUR", "")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.ForYouMinRebuildAge != 24*time.Hour ||
|
||||
cfg.ForYouRefreshInterval != 24*time.Hour ||
|
||||
cfg.ForYouRebuildHour != 4 {
|
||||
t.Fatalf("For You schedule = %v/%v hour %d",
|
||||
cfg.ForYouMinRebuildAge, cfg.ForYouRefreshInterval, cfg.ForYouRebuildHour)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForYouRebuildHourIsValidated(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_FOR_YOU_REBUILD_HOUR", "24")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected invalid rebuild hour to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecommendationWeightsMustBeJSON(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_RECOMMENDATION_WEIGHTS", "{broken")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected invalid recommendation weights to fail")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user