Files
unkin-agent c129cb99fc
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Add the initial mediamark app
Single Go binary serving the API and an embedded keyboard-first UI for
promoting fafflix titles into the cheeztv kids tree via hardlinks.

- internal/library: hardlink sync, idempotent re-runs, drift reporting,
  strict single-path-element name validation as the traversal guard
- internal/arr: minimal sonarr/radarr v3 client with a 60s list cache and
  a key-brokered poster proxy
- internal/auth: server-side Authentik group enforcement on every route
- internal/server: library JSON API, art proxy, health probes, SPA
- ui: two-tile landing page, fuzzy-filtered title list, detail panel
- Makefile, Dockerfile, .woodpecker pipelines, pre-commit config
2026-08-29 21:27:09 +10:00

84 lines
2.9 KiB
Go

// Package config loads mediamark runtime configuration from the environment.
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Config is the fully-resolved mediamark configuration.
type Config struct {
// Listen is the HTTP listen address, e.g. ":8080".
Listen string
// MediaRoot holds the fafflix/ (source) and cheeztv/ (kids) library trees.
MediaRoot string
// KeysDir holds per-app *arr api keys as files named "sonarr"/"radarr",
// projected by the Vault Secrets Operator and re-read on every use.
KeysDir string
// SonarrURL and RadarrURL include the *arr instance's UrlBase; the API
// lives at <url>/api/v3/...
SonarrURL string
RadarrURL string
// GroupsHeader is the oauth2-proxy header carrying Authentik group names.
GroupsHeader string
// AllowedGroups gates every page load and API call. Never empty.
AllowedGroups []string
}
// Load resolves configuration from the environment, failing closed on an empty
// allow-list (an empty list would authorize nobody or, worse, be read as
// "anyone" by a future refactor).
func Load() (*Config, error) {
c := &Config{
Listen: envOr("MEDIAMARK_LISTEN", ":8080"),
MediaRoot: envOr("MEDIAMARK_MEDIA_ROOT", "/media"),
KeysDir: envOr("MEDIAMARK_KEYS_DIR", "/etc/mediamark/keys"),
SonarrURL: envOr("MEDIAMARK_SONARR_URL", "http://sonarr.arrstack.svc.cluster.local:8989/3aa168/sonarr"),
RadarrURL: envOr("MEDIAMARK_RADARR_URL", "http://radarr.arrstack.svc.cluster.local:7878/3aa168/radarr"),
GroupsHeader: envOr("MEDIAMARK_GROUPS_HEADER", "X-Forwarded-Groups"),
AllowedGroups: ParseGroups(envOr("MEDIAMARK_ALLOWED_GROUPS", "akP-mediamark-user")),
}
if len(c.AllowedGroups) == 0 {
return nil, fmt.Errorf("MEDIAMARK_ALLOWED_GROUPS must name at least one group")
}
if strings.TrimSpace(c.GroupsHeader) == "" {
return nil, fmt.Errorf("MEDIAMARK_GROUPS_HEADER must not be empty")
}
if !filepath.IsAbs(c.MediaRoot) {
return nil, fmt.Errorf("MEDIAMARK_MEDIA_ROOT %q must be an absolute path", c.MediaRoot)
}
c.MediaRoot = filepath.Clean(c.MediaRoot)
for _, u := range []struct{ name, val string }{{"MEDIAMARK_SONARR_URL", c.SonarrURL}, {"MEDIAMARK_RADARR_URL", c.RadarrURL}} {
if !strings.HasPrefix(u.val, "http://") && !strings.HasPrefix(u.val, "https://") {
return nil, fmt.Errorf("%s %q must be an http(s) URL", u.name, u.val)
}
}
return c, nil
}
// ParseGroups splits a group list tolerating both comma and whitespace
// separation, dropping empties. oauth2-proxy emits comma-separated groups but
// deployments hand-write the allow-list.
func ParseGroups(s string) []string {
fields := strings.FieldsFunc(s, func(r rune) bool {
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == ';'
})
out := make([]string, 0, len(fields))
for _, f := range fields {
if f = strings.TrimSpace(f); f != "" {
out = append(out, f)
}
}
return out
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}