package config import ( "encoding/json" "fmt" "net/url" "os" "path/filepath" "strconv" "strings" "sync" ) // Runtime configuration keys stored in the runtime config file. const ( keyMatomoURL = "matomo_url" keyMatomoSiteID = "matomo_site_id" keyHLSSegmentDur = "hls_segment_duration" keyHLSWindowSize = "hls_window_size" ) // Default HLS values used when neither env nor persisted config is set. const ( defaultHLSSegmentDuration = 6 defaultHLSWindowSize = 24 ) var ( runtimeMu sync.RWMutex runtimeStore = map[string]string{} ) // ConfigPath returns the runtime config JSON file path from env or default. // The default lives in the shared playlist volume so the playout service can // read the same file (chat has read-write, playout read-only). func ConfigPath() string { p := strings.TrimSpace(os.Getenv("CONFIG_PATH")) if p == "" { return "/app/playlist/config.json" } return p } // GetRuntime returns a value from the runtime store and whether it is present. func GetRuntime(key string) (string, bool) { runtimeMu.RLock() defer runtimeMu.RUnlock() v, ok := runtimeStore[key] return v, ok } // SetRuntime stores a value in the runtime store (in-memory only). func SetRuntime(key, value string) { runtimeMu.Lock() defer runtimeMu.Unlock() runtimeStore[key] = value } // LoadRuntimeConfig loads the persisted runtime config from disk. If the file does // not exist, it seeds the store from environment variables. func LoadRuntimeConfig() error { data, err := os.ReadFile(ConfigPath()) if err != nil { if os.IsNotExist(err) { runtimeMu.Lock() runtimeStore = map[string]string{} runtimeMu.Unlock() seedRuntimeDefaults() return nil } return fmt.Errorf("read runtime config: %w", err) } values := map[string]string{} if err := json.Unmarshal(data, &values); err != nil { return fmt.Errorf("parse runtime config: %w", err) } runtimeMu.Lock() defer runtimeMu.Unlock() runtimeStore = values return nil } // SaveRuntimeConfig persists the runtime store to disk as JSON. func SaveRuntimeConfig() error { runtimeMu.RLock() data, err := json.MarshalIndent(runtimeStore, "", " ") runtimeMu.RUnlock() if err != nil { return fmt.Errorf("marshal runtime config: %w", err) } if dir := filepath.Dir(ConfigPath()); dir != "" && dir != "." { if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create runtime config dir: %w", err) } } if err := os.WriteFile(ConfigPath(), append(data, '\n'), 0o644); err != nil { return fmt.Errorf("write runtime config: %w", err) } return nil } // seedRuntimeDefaults populates the store from environment variables. func seedRuntimeDefaults() { if v := strings.TrimSpace(os.Getenv("MATOMO_URL")); v != "" { SetRuntime(keyMatomoURL, v) } if v := strings.TrimSpace(os.Getenv("MATOMO_SITE_ID")); v != "" { SetRuntime(keyMatomoSiteID, v) } if v := strings.TrimSpace(os.Getenv("HLS_SEGMENT_DURATION")); v != "" { SetRuntime(keyHLSSegmentDur, v) } if v := strings.TrimSpace(os.Getenv("HLS_WINDOW_SIZE")); v != "" { SetRuntime(keyHLSWindowSize, v) } } // MatomoURL returns the Matomo analytics URL. func MatomoURL() string { if v, ok := GetRuntime(keyMatomoURL); ok && strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } return strings.TrimSpace(os.Getenv("MATOMO_URL")) } // MatomoSiteID returns the Matomo site ID. func MatomoSiteID() string { if v, ok := GetRuntime(keyMatomoSiteID); ok && strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } return strings.TrimSpace(os.Getenv("MATOMO_SITE_ID")) } // HLSSegmentDuration returns the HLS segment duration in seconds. func HLSSegmentDuration() int { raw := "" if v, ok := GetRuntime(keyHLSSegmentDur); ok { raw = strings.TrimSpace(v) } if raw == "" { raw = strings.TrimSpace(os.Getenv("HLS_SEGMENT_DURATION")) } n, err := strconv.Atoi(raw) if err != nil || n <= 0 { return defaultHLSSegmentDuration } return n } // HLSWindowSize returns the HLS playlist window size in segments. func HLSWindowSize() int { raw := "" if v, ok := GetRuntime(keyHLSWindowSize); ok { raw = strings.TrimSpace(v) } if raw == "" { raw = strings.TrimSpace(os.Getenv("HLS_WINDOW_SIZE")) } n, err := strconv.Atoi(raw) if err != nil || n <= 0 { return defaultHLSWindowSize } return n } // SetMatomoURL validates and stores the Matomo URL. func SetMatomoURL(raw string) error { raw = strings.TrimSpace(raw) if raw == "" { SetRuntime(keyMatomoURL, "") return nil } u, err := url.Parse(raw) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return fmt.Errorf("matomo URL must be an absolute http(s) URL") } SetRuntime(keyMatomoURL, raw) return nil } // SetMatomoSiteID validates and stores the Matomo site ID. func SetMatomoSiteID(raw string) error { raw = strings.TrimSpace(raw) if raw != "" { if _, err := strconv.Atoi(raw); err != nil { return fmt.Errorf("matomo site ID must be numeric") } } SetRuntime(keyMatomoSiteID, raw) return nil } // SetHLSSegmentDuration validates (2–15s) and stores the HLS segment duration. func SetHLSSegmentDuration(seconds int) error { if seconds < 2 || seconds > 15 { return fmt.Errorf("HLS segment duration must be between 2 and 15 seconds") } SetRuntime(keyHLSSegmentDur, strconv.Itoa(seconds)) return nil } // SetHLSWindowSize validates (3–100) and stores the HLS window size. func SetHLSWindowSize(size int) error { if size < 3 || size > 100 { return fmt.Errorf("HLS window size must be between 3 and 100 segments") } SetRuntime(keyHLSWindowSize, strconv.Itoa(size)) return nil } // RuntimeConfigSnapshot returns the resolved runtime configuration values. func RuntimeConfigSnapshot() map[string]any { return map[string]any{ "matomoUrl": MatomoURL(), "matomoSiteId": MatomoSiteID(), "hlsSegmentDuration": HLSSegmentDuration(), "hlsWindowSize": HLSWindowSize(), } }