0.2.64 update
This commit is contained in:
@@ -81,6 +81,9 @@ type Config struct {
|
||||
// ReleasePublishToken authorizes the CI-only release upload endpoint. It is separate
|
||||
// from AdminToken so a compromised build runner cannot change maintenance settings.
|
||||
ReleasePublishToken string
|
||||
// ReleaseBuilderURL is the private Compose address of the Android release controller.
|
||||
// It is never given to the browser; the authenticated admin API relays requests to it.
|
||||
ReleaseBuilderURL string
|
||||
|
||||
// SyncInterval is how often the library import runs. Zero disables the schedule.
|
||||
SyncInterval time.Duration
|
||||
@@ -144,6 +147,26 @@ type Config struct {
|
||||
TracearrURL string
|
||||
TracearrAPIKey string
|
||||
TracearrServerID string
|
||||
// Credits detection discovers where an episode's closing credits begin, for the small
|
||||
// number of episodes the household is about to watch. It is demand-driven — Tracearr
|
||||
// says what is worth scanning — so these settings shape how far ahead of a viewer it
|
||||
// prepares, never how much of the library it reads.
|
||||
//
|
||||
// CreditsEnabled is off by default: it is the only thing in the gateway that reads media
|
||||
// bytes, and switching that on is an operator's decision rather than a default.
|
||||
CreditsEnabled bool
|
||||
// CreditsFFmpeg is the decoder. Absent, the subsystem still runs and still writes
|
||||
// markers, on behavioural evidence alone — which on a well-watched show is the better
|
||||
// signal anyway.
|
||||
CreditsFFmpeg string
|
||||
// CreditsPrefetchEpisodes is the look-ahead for an ordinary viewer; velocity moves the
|
||||
// actual depth either side of it, and CreditsMaxPrefetch is the ceiling nothing exceeds.
|
||||
CreditsPrefetchEpisodes int
|
||||
CreditsMaxPrefetch int
|
||||
// CreditsQueueLimit bounds pending candidates. Past it, low-priority speculation is
|
||||
// discarded rather than queued.
|
||||
CreditsQueueLimit int
|
||||
|
||||
// TracearrSyncInterval imports recent changed sessions. FullInterval reconciles
|
||||
// late/out-of-order updates and deletions without needing a source cursor.
|
||||
TracearrSyncInterval time.Duration
|
||||
@@ -160,6 +183,10 @@ func Load() (Config, error) {
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
releasePublishToken, err := secret("MEMBY_RELEASE_PUBLISH_TOKEN")
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
c := Config{
|
||||
ListenAddr: env("MEMBY_LISTEN_ADDR", ":8080"),
|
||||
EmbyURL: strings.TrimRight(os.Getenv("MEMBY_EMBY_URL"), "/"),
|
||||
@@ -181,42 +208,46 @@ func Load() (Config, error) {
|
||||
),
|
||||
RemoteConfig: remoteConfig,
|
||||
|
||||
AdminToken: strings.TrimSpace(os.Getenv("MEMBY_ADMIN_TOKEN")),
|
||||
AdminUIURL: env("MEMBY_ADMIN_UI_URL", "http://memby-admin:80"),
|
||||
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),
|
||||
BazarrURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_BAZARR_URL")), "/"),
|
||||
BazarrAPIKey: strings.TrimSpace(os.Getenv("MEMBY_BAZARR_API_KEY")),
|
||||
BazarrTTL: duration("MEMBY_BAZARR_TTL", 5*time.Minute),
|
||||
BazarrTimeout: duration("MEMBY_BAZARR_TIMEOUT", 45*time.Second),
|
||||
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),
|
||||
AdminToken: strings.TrimSpace(os.Getenv("MEMBY_ADMIN_TOKEN")),
|
||||
AdminUIURL: env("MEMBY_ADMIN_UI_URL", "http://memby-admin:80"),
|
||||
PublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_PUBLIC_URL")), "/"),
|
||||
ReleaseDir: env("MEMBY_RELEASE_DIR", "/data/releases"),
|
||||
ReleasePublishToken: releasePublishToken,
|
||||
ReleaseBuilderURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_RELEASE_BUILDER_URL")), "/"),
|
||||
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),
|
||||
BazarrURL: strings.TrimRight(strings.TrimSpace(os.Getenv("MEMBY_BAZARR_URL")), "/"),
|
||||
BazarrAPIKey: strings.TrimSpace(os.Getenv("MEMBY_BAZARR_API_KEY")),
|
||||
BazarrTTL: duration("MEMBY_BAZARR_TTL", 5*time.Minute),
|
||||
BazarrTimeout: duration("MEMBY_BAZARR_TIMEOUT", 45*time.Second),
|
||||
CreditsEnabled: boolean("MEMBY_CREDITS_ENABLED", false),
|
||||
CreditsFFmpeg: strings.TrimSpace(os.Getenv("MEMBY_CREDITS_FFMPEG")),
|
||||
CreditsPrefetchEpisodes: integer("MEMBY_CREDITS_PREFETCH_EPISODES", 3),
|
||||
CreditsMaxPrefetch: integer("MEMBY_CREDITS_MAX_PREFETCH", 5),
|
||||
CreditsQueueLimit: integer("MEMBY_CREDITS_QUEUE_LIMIT", 20),
|
||||
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.AnalyticsRetention < 30*24*time.Hour {
|
||||
c.AnalyticsRetention = 30 * 24 * time.Hour
|
||||
@@ -268,6 +299,26 @@ func env(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// secret reads a Docker/Kubernetes-style file-backed secret when KEY_FILE is set,
|
||||
// falling back to KEY for existing non-Compose deployments. The file's contents are
|
||||
// never included in an error, and Compose uses only the file form so `docker inspect`
|
||||
// cannot reveal the release-publish credential.
|
||||
func secret(key string) (string, error) {
|
||||
path := strings.TrimSpace(os.Getenv(key + "_FILE"))
|
||||
if path == "" {
|
||||
return strings.TrimSpace(os.Getenv(key)), nil
|
||||
}
|
||||
value, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s_FILE: %w", key, err)
|
||||
}
|
||||
trimmed := strings.TrimSpace(string(value))
|
||||
if trimmed == "" {
|
||||
return "", fmt.Errorf("%s_FILE is empty", key)
|
||||
}
|
||||
return trimmed, nil
|
||||
}
|
||||
|
||||
func boolean(key string, fallback bool) bool {
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
|
||||
@@ -1,10 +1,42 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReleasePublishTokenCanComeFromSecretFile(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_PUBLIC_URL", "https://memby.example")
|
||||
t.Setenv("MEMBY_RELEASE_PUBLISH_TOKEN", "legacy-environment-value")
|
||||
path := filepath.Join(t.TempDir(), "release-token")
|
||||
if err := os.WriteFile(path, []byte("file-backed-token\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("MEMBY_RELEASE_PUBLISH_TOKEN_FILE", path)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.ReleasePublishToken != "file-backed-token" {
|
||||
t.Fatalf("release token = %q, want the file-backed value", cfg.ReleasePublishToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredReleaseSecretFileMustBeReadable(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
t.Setenv("MEMBY_RELEASE_PUBLISH_TOKEN_FILE", filepath.Join(t.TempDir(), "missing"))
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected a missing release token secret to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracearrURLAndKeyMustBeConfiguredTogether(t *testing.T) {
|
||||
t.Setenv("MEMBY_EMBY_URL", "http://emby")
|
||||
t.Setenv("MEMBY_DATABASE_URL", "postgres://memby")
|
||||
|
||||
Reference in New Issue
Block a user