config

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: GPL-3.0 Imports: 23 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrOIDCProviderExists   = errors.New("oidc provider name already exists")
	ErrOIDCProviderNotFound = errors.New("oidc provider not found")
	ErrOIDCDiscoveryFailed  = errors.New("oidc discovery failed")

	// ErrSecretFileManaged is returned when a UI/API edit tries to set a secret
	// inline while that field is sourced from a *_file path. The file is the
	// source of truth — change it (or git), not the UI.
	ErrSecretFileManaged = errors.New(
		"secret is file-managed; edit the file, not the UI",
	)
)

Named errors returned by the mutate layer. Handlers branch on these to decide between 404 / 409 / 422 responses.

View Source
var (
	ErrDownloadClientExists   = errors.New("download client name already exists")
	ErrDownloadClientNotFound = errors.New("download client not found")
)
View Source
var (
	ErrIndexerExists   = errors.New("indexer name already exists")
	ErrIndexerNotFound = errors.New("indexer not found")
)
View Source
var (
	ErrMediaServerExists   = errors.New("media server name already exists")
	ErrMediaServerNotFound = errors.New("media server not found")
)
View Source
var (
	ErrQualityProfileExists = errors.New(
		"quality profile name already exists",
	)
	ErrQualityProfileNotFound       = errors.New("quality profile not found")
	ErrQualityProfileInUseAsDefault = errors.New(
		"quality profile is the configured default",
	)
)
View Source
var (
	// ErrNoPath means the config was loaded from a reader (not a file path) so
	// Update has no file to write back to.
	ErrNoPath = errors.New("config has no backing file path")

	// ErrReadOnly means the config is declared read_only, so Update refuses
	// every write-back. Declarative/GitOps deploys mount config read-only and
	// change it through git, not the UI; runtime-generated state
	// (plex_client_id, session secret) is surfaced to the operator instead of
	// persisted here.
	ErrReadOnly = errors.New("config is read-only")
)

Functions

func AddDownloadClient

func AddDownloadClient(ctx context.Context, e DownloadClientEntry) error

func AddIndexer

func AddIndexer(ctx context.Context, e IndexerEntry) error

func AddMediaServer

func AddMediaServer(ctx context.Context, e MediaServerEntry) error

func AddOIDCProvider

func AddOIDCProvider(ctx context.Context, p OIDCConfig) error

AddOIDCProvider probes the issuer's discovery document, then appends the provider to the auth.oidc list. Returns ErrOIDCProviderExists if the name is already in use or ErrOIDCDiscoveryFailed if the issuer is unreachable.

Discovery runs against the caller's ctx (with a 5s timeout) so slow remotes can't hold the config lock — Update is called only after the probe returns.

func AddQualityProfile

func AddQualityProfile(ctx context.Context, e QualityProfileEntry) error

func DeleteDownloadClient

func DeleteDownloadClient(ctx context.Context, name string) error

func DeleteIndexer

func DeleteIndexer(ctx context.Context, name string) error

func DeleteMediaServer

func DeleteMediaServer(ctx context.Context, name string) error

func DeleteOIDCProvider

func DeleteOIDCProvider(ctx context.Context, name string) error

DeleteOIDCProvider removes the named provider from auth.oidc. Returns ErrOIDCProviderNotFound if no provider carries that name.

func DeleteQualityProfile

func DeleteQualityProfile(ctx context.Context, name string) error

func DumpDefaults

func DumpDefaults(w io.Writer) error

DumpDefaults writes the default configuration as YAML to w. Output is a ready-to-use config file; running `config validate` on it should succeed once data_dir exists on disk.

func EnsurePlexClientID

func EnsurePlexClientID(ctx context.Context) error

EnsurePlexClientID generates and persists a Plex client identifier when a Plex server is already configured but none exists yet (e.g. on boot from a config that lists a Plex server). No-op — and no disk write — when no Plex server is present or an id already exists.

func LoadReader

func LoadReader(r io.Reader) error

LoadReader reads YAML config from r, overlays env vars, validates, and stores the resulting Config in the singleton (access via Get).

func PublicURL

func PublicURL() string

PublicURL returns the public base URL used for OIDC redirect URIs, invite links and admin-visible display. Priority: STREAMLINE_PUBLIC_URL env var (not part of the config schema — read directly) then http://host:port from the config. Should be overridden to an https URL when running behind a reverse proxy.

func ResetForTest

func ResetForTest()

ResetForTest clears the singleton. Tests only.

func SecretValue

func SecretValue(inline, file string) string

SecretValue returns the effective secret for an inline/file pair: the cached contents of file when a *_file path is set, otherwise the inline value. The pair is validated mutually exclusive (excluded_with), so at most one is ever set. Use this instead of reading the inline field directly so file-backed secrets resolve.

func Update

func Update(ctx context.Context, fn func(*Config) error) error

Update deep-clones the current config, runs fn, validates the result, writes it atomically to the backing file, then swaps the singleton. Returns ErrNoPath if no file path was captured at Load time. Update holds mu.Lock across the full clone/validate/write/swap sequence. This blocks concurrent Get readers for the duration of disk I/O; acceptable because Update is expected to fire only on admin-triggered settings changes.

func UpdateDownloadClient

func UpdateDownloadClient(
	ctx context.Context,
	name string,
	p DownloadClientPatch,
) error

func UpdateIndexer

func UpdateIndexer(ctx context.Context, name string, p IndexerPatch) error

func UpdateMediaServer

func UpdateMediaServer(ctx context.Context, name string, p MediaServerPatch) error

func UpdateOIDCProvider

func UpdateOIDCProvider(
	ctx context.Context,
	name string,
	patch OIDCProviderPatch,
) error

UpdateOIDCProvider merges the patch into the named provider. An empty/nil ClientSecret is treated as "keep existing" so the UI doesn't have to re-surface the secret just to let the admin change the issuer. When Issuer is patched the new URL is probed before the Update is written.

func UpdateQualityProfile

func UpdateQualityProfile(
	ctx context.Context,
	name string,
	p QualityProfilePatch,
) error

Types

type AppLog

type AppLog struct {
	Enabled bool      `koanf:"enabled"`
	Level   string    `koanf:"level"   validate:"required,oneof=debug info warn error"`
	Format  string    `koanf:"format"  validate:"required,oneof=text json"`
	Output  string    `koanf:"output"  validate:"required"`
	Rotate  LogRotate `koanf:"rotate"  validate:"required"`
}

type AuthConfig

type AuthConfig struct {
	Mode              string        `koanf:"mode"                validate:"required,oneof=full trusted-network disabled"`
	TrustedNetworks   []string      `koanf:"trusted_networks"    validate:"dive,cidr"`
	TrustedRole       string        `koanf:"trusted_role"        validate:"required,oneof=admin member request_only"`
	SessionSecret     string        `koanf:"session_secret"      validate:"excluded_with=SessionSecretFile"`
	SessionSecretFile string        `koanf:"session_secret_file" validate:"omitempty,excluded_with=SessionSecret,filepath"`
	SessionTTL        string        `koanf:"session_ttl"         validate:"required"`
	RegistrationMode  string        `koanf:"registration_mode"   validate:"required,oneof=disabled open invite"`
	OIDCDefaultRole   string        `koanf:"oidc_default_role"   validate:"required,oneof=admin member request_only"`
	SeedAdmin         SeedAdminCfg  `koanf:"seed_admin"`
	OIDC              []OIDCConfig  `koanf:"oidc"                validate:"dive"`
	Lockout           LockoutConfig `koanf:"lockout"             validate:"required"`
}

func UpdateAuth

func UpdateAuth(ctx context.Context, patch AuthPatch) (AuthConfig, error)

UpdateAuth validates the patch, merges it into the auth section, and persists via Update. Returns the resulting AuthConfig on success so callers can echo the new state back to the client without a second Get.

type AuthPatch

type AuthPatch struct {
	RegistrationMode *string
	SessionTTL       *string
	OIDCDefaultRole  *string
}

AuthPatch carries optional field updates to the auth section. Nil fields are left untouched so callers only need to populate keys the user actually changed.

type Config

type Config struct {
	Server  ServerConfig `koanf:"server"   validate:"required"`
	DataDir string       `koanf:"data_dir" validate:"required,dir"`
	// ReadOnly rejects every config.Update write-back with ErrReadOnly. For
	// declarative/GitOps deploys where config is mounted read-only and changes
	// flow through git, not the UI.
	ReadOnly    bool              `koanf:"read_only"`
	Auth        AuthConfig        `koanf:"auth"         validate:"required"`
	Library     LibraryConfig     `koanf:"library"      validate:"required"`
	Schedule    ScheduleConfig    `koanf:"schedules"    validate:"required"`
	Metadata    MetadataConfig    `koanf:"metadata"`
	Log         LogConfig         `koanf:"log"          validate:"required"`
	OTel        OTelConfig        `koanf:"otel"`
	MediaServer MediaServerConfig `koanf:"media_server"`
	Events      EventsConfig      `koanf:"events"       validate:"required"`

	DownloadClients       []DownloadClientEntry `koanf:"download_clients"        validate:"unique=Name,dive"`
	Indexers              []IndexerEntry        `koanf:"indexers"                validate:"unique=Name,dive"`
	QualityProfiles       []QualityProfileEntry `koanf:"quality_profiles"        validate:"unique=Name,dive"`
	QualityDefaultProfile string                `koanf:"quality_default_profile"`
}

func Get

func Get() *Config

Get returns a pointer to the current singleton config. Callers must treat the returned value as read-only; use Update to mutate.

func Load

func Load(cfgFile string) (*Config, error)

func (Config) DatabasePath

func (c Config) DatabasePath() string

DatabasePath is the SQLite database location, derived from DataDir.

func (*Config) Validate

func (c *Config) Validate() error

type DownloadClientEntry

type DownloadClientEntry struct {
	Name         string `koanf:"name"          validate:"required"`
	ClientType   string `koanf:"client_type"   validate:"required,oneof=qbittorrent transmission deluge"`
	Host         string `koanf:"host"          validate:"required"`
	Port         uint16 `koanf:"port"          validate:"required,port"`
	AuthMethod   string `koanf:"auth_method"   validate:"required,oneof=password api_key"`
	Username     string `koanf:"username"`
	Password     string `koanf:"password"      validate:"excluded_with=PasswordFile"`
	PasswordFile string `koanf:"password_file" validate:"omitempty,excluded_with=Password,filepath"`
	APIKey       string `koanf:"api_key"       validate:"excluded_with=APIKeyFile"`
	APIKeyFile   string `koanf:"api_key_file"  validate:"omitempty,excluded_with=APIKey,filepath"`
	UseSSL       bool   `koanf:"use_ssl"`
	Priority     uint8  `koanf:"priority"`
	Enabled      bool   `koanf:"enabled"`
}

func EnabledDownloadClients

func EnabledDownloadClients() []DownloadClientEntry

EnabledDownloadClients returns every enabled download client. The adoption pass scans each for untracked managed-category torrents.

func FindDownloadClient

func FindDownloadClient(name string) (DownloadClientEntry, bool)

func PickDownloadClient

func PickDownloadClient() (DownloadClientEntry, bool)

PickDownloadClient returns the highest-priority enabled download client.

type DownloadClientPatch

type DownloadClientPatch struct {
	ClientType *string
	Host       *string
	Port       *uint16
	AuthMethod *string
	Username   *string
	Password   *string
	APIKey     *string
	UseSSL     *bool
	Priority   *uint8
	Enabled    *bool
}

DownloadClientPatch carries optional field updates. Blank Password/APIKey preserve the existing secret.

type EventsConfig

type EventsConfig struct {
	Retention string `koanf:"retention" validate:"required"`
}

EventsConfig governs the MovieEvent retention window. Old rows are deleted by the cleanup job after Retention.

type HTTPLog

type HTTPLog struct {
	Enabled bool      `koanf:"enabled"`
	Output  string    `koanf:"output"  validate:"required"`
	Format  string    `koanf:"format"  validate:"required,oneof=json combined"`
	Rotate  LogRotate `koanf:"rotate"  validate:"required"`
}

type IndexerEntry

type IndexerEntry struct {
	Name       string `koanf:"name"         validate:"required"`
	Host       string `koanf:"host"         validate:"required"`
	Port       uint16 `koanf:"port"         validate:"required,port"`
	Path       string `koanf:"path"`
	UseSSL     bool   `koanf:"use_ssl"`
	APIKey     string `koanf:"api_key"      validate:"required_without=APIKeyFile,excluded_with=APIKeyFile"`
	APIKeyFile string `koanf:"api_key_file" validate:"omitempty,excluded_with=APIKey,filepath"`
	Protocol   string `koanf:"protocol"     validate:"required,oneof=torznab prowlarr"`
	Priority   uint8  `koanf:"priority"`
	Enabled    bool   `koanf:"enabled"`
}

func EnabledIndexers

func EnabledIndexers() []IndexerEntry

func FindIndexer

func FindIndexer(name string) (IndexerEntry, bool)

type IndexerPatch

type IndexerPatch struct {
	Host     *string
	Port     *uint16
	Path     *string
	UseSSL   *bool
	APIKey   *string
	Protocol *string
	Priority *uint8
	Enabled  *bool
}

IndexerPatch carries optional field updates. A blank APIKey preserves the existing key.

type LibraryConfig

type LibraryConfig struct {
	MoviePath    string `koanf:"movie_path"    validate:"required"`
	MovieNaming  string `koanf:"movie_naming"  validate:"required"`
	SeriesPath   string `koanf:"series_path"   validate:"required"`
	SeriesNaming string `koanf:"series_naming" validate:"required"`
	// DownloadPath is the host-side directory where streamline reads
	// completed torrents from. qBittorrent (or any other client) decides
	// its own save location; this only tells the importer where to look,
	// per-torrent content lives at <DownloadPath>/<torrent.Name>. The
	// directory is not validated at boot — it may be a bind-mount that
	// comes up after streamline; the importer surfaces a clear stat error
	// if the path is wrong.
	DownloadPath         string   `koanf:"download_path"          validate:"required"`
	ImportMode           string   `koanf:"import_mode"            validate:"required,oneof=hardlink copy move"`
	NoMatchCooldown      string   `koanf:"no_match_cooldown"      validate:"required"`
	MaxGrabFailures      uint8    `koanf:"max_grab_failures"      validate:"required,min=1"`
	KeepTorrentSeeding   bool     `koanf:"keep_torrent_seeding"`
	ImportMaxAttempts    uint8    `koanf:"import_max_attempts"    validate:"required,min=1"`
	AllowedDownloadRoots []string `koanf:"allowed_download_roots"`
	// DriftGraceTicks is the number of consecutive drift_check ticks a
	// MediaFile may be absent from disk before its row is deleted and the
	// owning movie reverts to "wanted". Bounded to give operators a knob
	// for noisy mounts without unbounded patience.
	DriftGraceTicks uint8 `koanf:"drift_grace_ticks" validate:"required,min=1,max=20"`
}

type LockoutConfig

type LockoutConfig struct {
	Threshold uint8  `koanf:"threshold" validate:"required,min=1,max=255"`
	Window    string `koanf:"window"    validate:"required"`
	Duration  string `koanf:"duration"  validate:"required"`
}

LockoutConfig governs the per-account login-failure lockout. Threshold is the failed-attempt count that locks the account; Window is the sliding window over which failures accumulate; Duration is how long the lockout holds before auto-expiry.

type LogConfig

type LogConfig struct {
	App  AppLog  `koanf:"app"  validate:"required"`
	HTTP HTTPLog `koanf:"http" validate:"required"`
}

type LogRotate

type LogRotate struct {
	MaxSizeMB  int  `koanf:"max_size_mb"  validate:"min=0"`
	MaxBackups int  `koanf:"max_backups"  validate:"min=0"`
	MaxAgeDays int  `koanf:"max_age_days" validate:"min=0"`
	Compress   bool `koanf:"compress"`
}

type MediaServerConfig

type MediaServerConfig struct {
	PlexClientID string             `koanf:"plex_client_id"`
	Servers      []MediaServerEntry `koanf:"servers"        validate:"unique=Name,dive"`
}

MediaServerConfig holds media-server integration identifiers. PlexClientID is generated and persisted the first time a Plex server is configured (see EnsurePlexClientID); required by the Plex PIN OAuth flow as the X-Plex-Client-Identifier header value.

type MediaServerEntry

type MediaServerEntry struct {
	Name       string `koanf:"name"         validate:"required"`
	ServerType string `koanf:"server_type"  validate:"required,oneof=plex jellyfin emby"`
	Host       string `koanf:"host"         validate:"required"`
	APIKey     string `koanf:"api_key"      validate:"excluded_with=APIKeyFile"`
	APIKeyFile string `koanf:"api_key_file" validate:"omitempty,excluded_with=APIKey,filepath"`
	Enabled    bool   `koanf:"enabled"`
	// LibrarySection is the Plex movie-library section key (play-on hint).
	LibrarySection *string `koanf:"library_section"`
	// LibrarySectionTV is the Plex TV-library section key (play-on hint).
	LibrarySectionTV *string `koanf:"library_section_tv"`
}

MediaServerEntry is one media-server integration. Secrets (api_key) are stored plaintext in YAML, consistent with auth.oidc[].client_secret.

func EnabledMediaServers

func EnabledMediaServers() []MediaServerEntry

func FindMediaServer

func FindMediaServer(name string) (MediaServerEntry, bool)

type MediaServerPatch

type MediaServerPatch struct {
	ServerType     *string
	Host           *string
	APIKey         *string
	Enabled        *bool
	LibrarySection *string
}

MediaServerPatch carries optional field updates. A blank APIKey preserves the existing token.

type MetadataConfig

type MetadataConfig struct {
	TMDBAPIKey     string `koanf:"tmdb_api_key"      validate:"excluded_with=TMDBAPIKeyFile"`
	TMDBAPIKeyFile string `koanf:"tmdb_api_key_file" validate:"omitempty,excluded_with=TMDBAPIKey,filepath"`
	TVDBAPIKey     string `koanf:"tvdb_api_key"      validate:"excluded_with=TVDBAPIKeyFile"`
	TVDBAPIKeyFile string `koanf:"tvdb_api_key_file" validate:"omitempty,excluded_with=TVDBAPIKey,filepath"`
	Language       string `koanf:"language"          validate:"omitempty,bcp47_language_tag"`
	TMDBRegion     string `koanf:"tmdb_region"       validate:"omitempty,len=2,uppercase"`
}

type OIDCConfig

type OIDCConfig struct {
	Name         string `koanf:"name"          validate:"required"`
	Issuer       string `koanf:"issuer"        validate:"required,url"`
	ClientID     string `koanf:"client_id"     validate:"required"`
	ClientSecret string `koanf:"client_secret" validate:"required_without=ClientSecretFile,excluded_with=ClientSecretFile"`
	// ClientSecretFile, when set, is read into ClientSecret after validation.
	// Mutually exclusive with ClientSecret (set exactly one). Lets a GitOps
	// deploy mount the secret from a k8s Secret instead of inlining it.
	ClientSecretFile string `koanf:"client_secret_file" validate:"omitempty,excluded_with=ClientSecret,filepath"`
	// RoleClaim is the ID-token claim (e.g. "groups", "roles") consulted for
	// claim-based role mapping; RoleMapping maps a claim value to a Streamline
	// role. When both are set and a value matches, the mapped role is
	// authoritative — applied at signup and re-synced on every login (the
	// highest-privilege role wins if several values map). Leave empty to give
	// OIDC users auth.oidc_default_role.
	RoleClaim   string            `koanf:"role_claim"`
	RoleMapping map[string]string `koanf:"role_mapping" validate:"omitempty,dive,oneof=admin member request_only"`
}

type OIDCProviderPatch

type OIDCProviderPatch struct {
	Issuer       *string
	ClientID     *string
	ClientSecret *string
}

OIDCProviderPatch carries optional field updates to a single OIDC provider. A nil ClientSecret (or empty string) preserves the existing secret — the UI never shows the current value, so blank means "unchanged."

type OTelConfig

type OTelConfig struct {
	Endpoint string `koanf:"endpoint"`
}

type QualityProfileEntry

type QualityProfileEntry struct {
	Name                string `koanf:"name"                 validate:"required"`
	PreferredResolution string `koanf:"preferred_resolution" validate:"required,oneof=720p 1080p 2160p"`
	MinResolution       string `koanf:"min_resolution"       validate:"required,oneof=720p 1080p 2160p"`
	UpgradeAllowed      bool   `koanf:"upgrade_allowed"`
}

func ResolveQualityProfile

func ResolveQualityProfile(name string) (QualityProfileEntry, bool)

ResolveQualityProfile returns the profile named by name, falling back to QualityDefaultProfile when name is empty or unknown. ok is false only when no profiles are configured at all.

type QualityProfilePatch

type QualityProfilePatch struct {
	PreferredResolution *string
	MinResolution       *string
	UpgradeAllowed      *bool
}

QualityProfilePatch carries optional field updates.

type ScheduleConfig

type ScheduleConfig struct {
	RSSSync         string `koanf:"rss_sync"         validate:"required"`
	MetadataRefresh string `koanf:"metadata_refresh" validate:"required"`
	DownloadMonitor string `koanf:"download_monitor" validate:"required"`
	MissingSearch   string `koanf:"missing_search"   validate:"required"`
	Cleanup         string `koanf:"cleanup"          validate:"required"`
	ImportScan      string `koanf:"import_scan"      validate:"required"`
	OrphanScan      string `koanf:"orphan_scan"      validate:"required"`
	DriftCheck      string `koanf:"drift_check"      validate:"required"`
}

type SeedAdminCfg

type SeedAdminCfg struct {
	Email        string `koanf:"email"         validate:"omitempty,email"`
	Password     string `koanf:"password"      validate:"excluded_with=PasswordFile"`
	PasswordFile string `koanf:"password_file" validate:"omitempty,excluded_with=Password,filepath"`
}

type ServerConfig

type ServerConfig struct {
	Host string `koanf:"host" validate:"required,ip|hostname"`
	Port uint16 `koanf:"port" validate:"required,port"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL