Help us shape the future of Go! Take the 2026 Go Developer Survey

config

package
v0.2.49 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: GPL-3.0 Imports: 26 Imported by: 0

Documentation

Overview

Package config is Ether's owner-editable settings, stored as JSON in the data directory. The web UI edits it through internal/admin; components get a snapshot at construction and subscribe for changes.

Index

Constants

View Source
const (
	IndexerKindNewznab   = "newznab"
	IndexerKindCardigann = "cardigann"
	IndexerKindStremio   = "stremio"
)

Indexer kinds (Indexer.Kind).

View Source
const (
	DatabaseSQLite   = "sqlite"
	DatabasePostgres = "postgres"
)
View Source
const (
	DebridRealDebrid = "realdebrid"
	DebridTorBox     = "torbox"
	DebridAllDebrid  = "alldebrid"
	DebridPremiumize = "premiumize"
	DebridDebridLink = "debridlink"
	DebridOffcloud   = "offcloud"
)

Debrid kinds (DebridProvider.Kind).

View Source
const (
	LLMAPIOpenAI    = "openai"    // POST {base}/chat/completions, GET {base}/models
	LLMAPIAnthropic = "anthropic" // POST {base}/messages, GET {base}/models
	// LLMAPIGoogle chats through Gemini's OpenAI-compatible surface
	// ({base}/openai/chat/completions) and lists models natively.
	LLMAPIGoogle = "google"
)

LLM wire protocols.

View Source
const (
	// SharedTopic is published (payload: the new version, decimal) after a
	// node changes the shared settings.
	SharedTopic = "config:changed"
	// ResyncTopic is the event bus's synthetic "you may have missed
	// events" notice after a reconnect; the store re-reads its version.
	ResyncTopic = "bus:resync"
	// DefaultSharedPoll is the fallback poll interval.
	DefaultSharedPoll = 30 * time.Second
)
View Source
const CredentialKeyFile = ".credentials-key"
View Source
const DatabaseURLEnv = "ETHER_DATABASE_URL"

DatabaseURLEnv overrides Database for this process: a postgres:// (or postgresql://) URL selects PostgreSQL with that URL. It lets container and service deployments keep credentials out of config.json.

View Source
const DefaultPort = 32400

DefaultPort is the HTTP port of a fresh install: Plex Media Server's. When it is taken, the server moves to a fallback port and saves it.

View Source
const LLMProviderCustom = "custom"

LLMProviderCustom is any OpenAI-compatible endpoint the owner supplies.

Variables

View Source
var (
	// ErrSharedConflict is returned by SharedBackend.SaveShared when the
	// stored version is not the expected one.
	ErrSharedConflict = errors.New("config: shared settings were changed by another node")
	// ErrKeyMismatch means this node's credential key is not the cluster's.
	ErrKeyMismatch = errors.New("config: this node's credential key does not match the cluster's")
	// ErrSharedMissing means the database holds no shared settings yet.
	ErrSharedMissing = errors.New("config: the cluster database holds no shared settings")
)

DebridKinds lists the supported debrid services.

View Source
var ErrSealedWithoutKey = errors.New("config: sealed value but no credential key")

ErrSealedWithoutKey is returned when a sealed field is read without a sealer.

View Source
var LLMProviders = []LLMProvider{
	{ID: "anthropic", Label: "Anthropic — Claude", API: LLMAPIAnthropic, BaseURL: "https://api.anthropic.com/v1", NeedsKey: true, KeyHint: "sk-ant-…"},
	{ID: "openai", Label: "OpenAI — GPT", API: LLMAPIOpenAI, BaseURL: "https://api.openai.com/v1", NeedsKey: true, KeyHint: "sk-…"},
	{ID: "google", Label: "Google — Gemini", API: LLMAPIGoogle, BaseURL: "https://generativelanguage.googleapis.com/v1beta", NeedsKey: true, KeyHint: "AIza…"},
	{ID: "openrouter", Label: "OpenRouter — any model, one key", API: LLMAPIOpenAI, BaseURL: "https://openrouter.ai/api/v1", NeedsKey: true, KeyHint: "sk-or-…",
		ModelHint: "Models are vendor/model slugs, e.g. openai/gpt-4o."},
	{ID: "groq", Label: "Groq — fast inference", API: LLMAPIOpenAI, BaseURL: "https://api.groq.com/openai/v1", NeedsKey: true, KeyHint: "gsk_…"},
	{ID: "xai", Label: "xAI — Grok", API: LLMAPIOpenAI, BaseURL: "https://api.x.ai/v1", NeedsKey: true, KeyHint: "xai-…"},
	{ID: "deepseek", Label: "DeepSeek", API: LLMAPIOpenAI, BaseURL: "https://api.deepseek.com", NeedsKey: true, KeyHint: "sk-…"},
	{ID: "mistral", Label: "Mistral", API: LLMAPIOpenAI, BaseURL: "https://api.mistral.ai/v1", NeedsKey: true, KeyHint: "Your Mistral API key"},
	{ID: "ollama", Label: "Ollama — on your network", API: LLMAPIOpenAI, BaseURL: "http://localhost:11434/v1", Local: true, KeyHint: "Not needed"},
	{ID: "lmstudio", Label: "LM Studio — on your network", API: LLMAPIOpenAI, BaseURL: "http://localhost:1234/v1", Local: true, KeyHint: "Not needed"},
	{ID: LLMProviderCustom, Label: "Custom (OpenAI-compatible)", API: LLMAPIOpenAI, Local: true, RequiresBaseURL: true, KeyHint: "Your provider API key",
		ModelHint: "Any OpenAI-compatible endpoint, such as Together, Fireworks, vLLM or llama.cpp."},
}

LLMProviders lists the supported services, hosted first.

Functions

func ContainsSealed added in v0.2.4

func ContainsSealed(b []byte) bool

ContainsSealed reports whether b holds a sealed value of any format, so that it needs the data directory's credential key to be read.

func DataDirWarning added in v0.2.4

func DataDirWarning(dataDir string) string

DataDirWarning returns a warning when the data directory or the credential key can be read by other local accounts, or "" (always on Windows, where the installer and service install manage ACLs). It never changes permissions: an existing directory keeps the mode its owner or a package postinstall chose. The message names modes, not paths.

func DefaultDataDir

func DefaultDataDir() string

DefaultDataDir is where Ether keeps config, database and cache. On Windows that is %ProgramData%\Ether: the server runs as a LocalSystem service, so a per-user directory would depend on who installed it.

func EnsureDataDir added in v0.2.4

func EnsureDataDir(dataDir string) error

EnsureDataDir creates dataDir owner-only (0700) when it does not exist: it holds config.json, the credential key and ether.db. Parents are made as usual, and a directory that already exists keeps the mode its owner or a package postinstall gave it.

func FieldContext added in v0.2.4

func FieldContext(id int64) string

FieldContext joins an int64 row identity for a field context.

func InstallCredentialKey

func InstallCredentialKey(dir string, key []byte) error

InstallCredentialKey writes key as dir's credential key. It refuses to replace an existing key file: a node joining a cluster moves its old configuration aside first.

func IsSealed added in v0.2.4

func IsSealed(stored string) bool

IsSealed reports whether a stored value is sealed (any format).

func KeyFingerprint

func KeyFingerprint(key []byte) string

KeyFingerprint identifies a credential key without revealing it. Cluster nodes compare fingerprints to prove they hold the same key.

func LocalJSON

func LocalJSON(c Config) []byte

LocalJSON is the node-local part of c as JSON (unsealed).

func OpenWith added in v0.2.4

func OpenWith(s FieldSealer, stored string, context ...string) (string, bool, error)

OpenWith is s.OpenField; with a nil s, a sealed value is ErrSealedWithoutKey and anything else is returned as is.

func ProtectedValues added in v0.2.49

func ProtectedValues(cfg Config) []string

ProtectedValues returns the plaintext config strings the credential walker treats as secrets, including usernames, tokens, passwords, API keys, URL templates, secret-settings values, and the database URL. The result is for redaction. Callers must not write it into an archive.

func ReadCredentialKey

func ReadCredentialKey(dir string) ([]byte, error)

ReadCredentialKey returns dir's credential key, or os.ErrNotExist.

func RedactDatabaseURL

func RedactDatabaseURL(raw string) string

RedactDatabaseURL masks the password in a connection URL.

func SealWith added in v0.2.4

func SealWith(s FieldSealer, plain string, context ...string) (string, error)

SealWith is s.SealField, or plain unchanged when s is nil (components built without a credential key, such as in tests).

func SharedJSON

func SharedJSON(c Config) []byte

SharedJSON is the cluster-shared part of c as JSON (unsealed).

Types

type Availability added in v0.2.8

type Availability struct {
	DisableMovieEvidenceGate bool     `json:"disableMovieEvidenceGate"`
	MovieEvidenceDays        int      `json:"movieEvidenceDays"` // zero preserves the 60-day default
	Monitor                  bool     `json:"monitor"`
	Verify                   bool     `json:"verify"`
	PreferVerified           bool     `json:"preferVerified"`
	NewlyAvailable           bool     `json:"newlyAvailable"`
	IndexerIDs               []string `json:"indexerIds"` // empty = all supported enabled indexers
	Debrid                   bool     `json:"debrid"`
	PollMinutes              int      `json:"pollMinutes"`
	MaxPages                 int      `json:"maxPages"`
	FeedRequests             int      `json:"feedRequests"`  // total per UTC day, across indexers
	CheckRequests            int      `json:"checkRequests"` // indexer + provider API calls per day
	Grabs                    int      `json:"grabs"`
	Checks                   int      `json:"checks"`
	CheckBytes               int64    `json:"checkBytes"`     // conservative wire reservations for validation
	ReservePercent           int      `json:"reservePercent"` // preserve this share of configured indexer caps
}

Availability bounds automatic recent-release monitoring and optional checks. Zero budgets disable the corresponding background work.

func DefaultAvailability added in v0.2.8

func DefaultAvailability() Availability

func (Availability) EffectiveMovieEvidenceDays added in v0.2.35

func (a Availability) EffectiveMovieEvidenceDays() int

EffectiveMovieEvidenceDays preserves legacy configurations.

type Cache

type Cache struct {
	Storage string `json:"storage,omitempty"` // local (default) or shared; restart required

	PreloadEnabled         bool   `json:"preloadEnabled,omitempty"`
	PreloadBudgetBytes     int64  `json:"preloadBudgetBytes,omitempty"`
	PreloadMaxTitles       int    `json:"preloadMaxTitles,omitempty"`
	PreloadRateMiB         int    `json:"preloadRateMiB,omitempty"`
	PreloadDuringPlayback  bool   `json:"preloadDuringPlayback,omitempty"`
	PreloadMinFreeBytes    int64  `json:"preloadMinFreeBytes,omitempty"`
	PreloadSource          string `json:"preloadSource,omitempty"`
	PreloadContent         string `json:"preloadContent,omitempty"`         // mixed (default), movies, tv
	PreloadHouseholdWeight *int   `json:"preloadHouseholdWeight,omitempty"` // 0–100; nil = 65
	PreloadHouseholdScope  string `json:"preloadHouseholdScope,omitempty"`  // home (default), all
	PreloadEpisodesAhead   int    `json:"preloadEpisodesAhead,omitempty"`   // per-show shelf window; 0 = 3
	PreloadHistoryDays     int    `json:"preloadHistoryDays,omitempty"`     // 0 = 90

	// Mode: empty/demand retains fetched bytes; adaptive favours replayed titles;
	// off bypasses persistent media writes. Zero budget remains unlimited.
	Mode               string `json:"mode,omitempty"`
	RetentionDays      int    `json:"retentionDays,omitempty"`
	ReplayBoostHours   int    `json:"replayBoostHours,omitempty"`
	DisableSpeculation bool   `json:"disableSpeculation,omitempty"`
	DetailWarmup       string `json:"detailWarmup,omitempty"` // empty/on or off
	DetailBytes        int64  `json:"detailBytes,omitempty"`  // zero warms 40s of source media, capped at 256 MiB

	Dir         string `json:"dir"`
	BudgetBytes int64  `json:"budgetBytes"`
	// Prefetch
	PrefetchNextEpisode bool    `json:"prefetchNextEpisode"`
	PrefetchBytes       int64   `json:"prefetchBytes"` // opening bytes pulled ahead
	KeepInProgressWarm  bool    `json:"keepInProgressWarm"`
	NextEpisodeAt       float64 `json:"nextEpisodeAt"` // fraction of current episode, e.g. 0.8
	// InProgressPinMaxAge: an in-progress title nobody has watched for this
	// long stops pinning its release in the cache (0 = default 30 days).
	InProgressPinMaxAge Duration `json:"inProgressPinMaxAge,omitempty"`
	// InProgressPinPercent caps the share of BudgetBytes that in-progress
	// pins may hold; the least recently watched titles lose their pin first
	// (0 = default 50, max 100).
	InProgressPinPercent int `json:"inProgressPinPercent,omitempty"`
}

type Catalog

type Catalog struct {
	// ShowUpcomingEpisodes adds metadata-only scheduled episodes to airing seasons.
	ShowUpcomingEpisodes bool `json:"showUpcomingEpisodes"`
	// GateMoviesByHomeRelease waits for a worldwide digital or physical release.
	GateMoviesByHomeRelease bool `json:"gateMoviesByHomeRelease"`
	// HideFranchiseCollections hides automatic TMDB groups from Plex's
	// Collections tab. The zero value preserves existing libraries.
	HideFranchiseCollections bool                    `json:"hideFranchiseCollections"`
	Exclusions               []core.LibraryExclusion `json:"exclusions,omitempty"`
	TMDBKey                  string                  `json:"tmdbKey"`  // owner override; empty uses the bundled key
	Language                 string                  `json:"language"` // TMDB language, e.g. "en-US"
	Region                   string                  `json:"region"`
	MinPopularity            float64                 `json:"minPopularity"`
	MinVotes                 int                     `json:"minVotes"`
	IncludeAdult             bool                    `json:"includeAdult"`
	RefreshInterval          Duration                `json:"refreshInterval"`
}

type Config

type Config struct {
	Posters      Posters      `json:"posters"`
	Availability Availability `json:"availability"`
	// SharedCache is the cluster-wide NAS policy. Mount paths stay in Cache.
	SharedCache   *Cache        `json:"sharedCache,omitempty"`
	Server        Server        `json:"server"`
	Plex          Plex          `json:"plex"`
	Usenet        Usenet        `json:"usenet"`
	Indexers      []Indexer     `json:"indexers"`
	Cache         Cache         `json:"cache"`
	Catalog       Catalog       `json:"catalog"`
	Resolver      Resolver      `json:"resolver"`
	Quality       Quality       `json:"quality"`
	Transcode     Transcode     `json:"transcode"`
	Trakt         Trakt         `json:"trakt"`
	Simkl         Simkl         `json:"simkl"`
	OpenSubtitles OpenSubtitles `json:"openSubtitles"`
	LLM           LLM           `json:"llm"`
	Updates       Updates       `json:"updates"`
	Spike         Spike         `json:"spike"`
	Downloads     Downloads     `json:"downloads"`
	Markers       Markers       `json:"markers"`
	Personalize   Personalize   `json:"personalize"`
	Extras        Extras        `json:"extras"`
	Debrid        Debrid        `json:"debrid"`
	// Database is node-local storage selection (SQLite or PostgreSQL).
	Database      Database     `json:"database"`
	IndexerSites  IndexerSites `json:"indexerSites"`
	SetupComplete bool         `json:"setupComplete"`
}

func Defaults

func Defaults(dataDir string) Config

Defaults returns a fresh config for a new install.

func MergeShared

func MergeShared(local Config, sharedJSON []byte) (Config, error)

MergeShared returns local with every shared setting taken from the shared JSON document (as produced by SharedJSON). Shared settings the document lacks (written by an older version) take their defaults; node-local keys in the document are ignored.

func Read added in v0.2.4

func Read(dataDir string) (Config, error)

Read loads dataDir's settings without creating or rewriting anything: no directory, no credential key and no re-sealed config.json, unlike Open. It is for other processes (the tray) that only look at the server's settings and must not race its writes. Sealed credentials need the existing credential key.

func (Config) EffectiveCache added in v0.2.4

func (c Config) EffectiveCache() Cache

EffectiveCache combines the shared pool policy with this node's mount path.

func (*Config) SetCachePolicy added in v0.2.4

func (c *Config) SetCachePolicy(p Cache)

SetCachePolicy updates the shared policy only when this node joins the NAS pool.

type Database

type Database struct {
	// Driver is "sqlite" (default) or "postgres".
	Driver string `json:"driver,omitempty"`
	// URL is the PostgreSQL connection URL, e.g.
	// postgres://ether:secret@db.lan:5432/ether?sslmode=verify-full
	URL string `json:"url,omitempty"`
	// DeferMigrations makes this node wait, at startup, instead of applying
	// new schema migrations to the shared database while another live node
	// runs a version of Ether that does not know them. Off by default:
	// migrations are additive, so older nodes keep working on a newer
	// schema and a rolling update never stalls on it.
	DeferMigrations bool `json:"deferMigrations,omitempty"`
}

Database is this node's storage backend. It is node-local: a standalone server keeps SQLite in its data directory; a cluster node points at the owner-supplied PostgreSQL database. The URL carries credentials, so it is sealed in config.json like other secrets and must only ever be logged through Redacted.

func (Database) Effective

func (d Database) Effective(getenv func(string) string) Database

Effective applies the ETHER_DATABASE_URL override (read through getenv) and normalises the driver name.

func (Database) IsPostgres

func (d Database) IsPostgres() bool

IsPostgres reports whether these (effective) settings select PostgreSQL.

func (Database) Redacted

func (d Database) Redacted() Database

Redacted returns a copy safe to log or display: the URL's password is masked.

type Debrid

type Debrid struct {
	Providers []DebridProvider `json:"providers"`
	// CachedOnly accepts a torrent only when a provider can serve it
	// instantly. When false, an uncached torrent is left on the account to
	// download and the release is skipped for now; playback never waits.
	CachedOnly bool `json:"cachedOnly"`
	// RemoveUncached deletes torrents Ether added that turned out not to be
	// cached, keeping the owner's account clean (CachedOnly only).
	RemoveUncached bool `json:"removeUncached"`
	// ChunkBytes caps one HTTP range request (0 = 8 MiB). Requests start
	// small for a fast first byte and grow towards this size.
	ChunkBytes int64 `json:"chunkBytes,omitempty"`
	// ReadAheadChunks fixes the sequential read-ahead window in chunks
	// (0 = adaptive to measured throughput; negative disables read-ahead).
	ReadAheadChunks int `json:"readAheadChunks,omitempty"`
	// MaxConnections caps concurrent range requests per open file (0 = 4).
	MaxConnections int `json:"maxConnections,omitempty"`
}

Debrid configures owner-supplied debrid accounts. Torrent releases from Torznab indexers are added to a debrid service that already holds them and streamed over HTTPS range requests; nothing is downloaded locally.

func (Debrid) EnabledProviders

func (d Debrid) EnabledProviders() bool

EnabledProviders reports whether any provider is enabled with a key.

type DebridProvider

type DebridProvider struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Kind     string `json:"kind"` // realdebrid, torbox, alldebrid, premiumize, debridlink, offcloud
	APIKey   string `json:"apiKey"`
	Priority int    `json:"priority"` // lower is tried first
	Enabled  bool   `json:"enabled"`
}

DebridProvider is one debrid account.

type Downloads

type Downloads struct {
	Enabled bool `json:"enabled"`
	// AllowFriends lets shared (non-owner) users download. Off by
	// default, matching PMS sharing where sync is a separate grant; an
	// AccessPolicy AllowDownloads decision overrides this per user.
	AllowFriends bool `json:"allowFriends"`
	// MaxConcurrent bounds simultaneous remuxes server-wide (default 2),
	// MaxConcurrentPerUser per viewer (default 1).
	MaxConcurrent        int `json:"maxConcurrent"`
	MaxConcurrentPerUser int `json:"maxConcurrentPerUser"`
	// DiskBudgetBytes bounds the temporary remuxed files on disk at any
	// time (default 20 GiB). 0 uses the default.
	DiskBudgetBytes int64 `json:"diskBudgetBytes"`
	// Expiry is how long a finished download stays fetchable before its
	// temp file is deleted (default 24h).
	Expiry Duration `json:"expiry"`
	// MaxHeight caps the download profile's resolution (default 1080).
	MaxHeight int `json:"maxHeight"`
}

Downloads controls Plex offline downloads (allowSync + the Download Queue API). A download resolves a compact mobile-native release and remuxes it (stream copy) to a temporary MP4; nothing is stored permanently. Enabled by default for the owner: it is bounded by the disk budget, the concurrency caps and the normal stream limits, and the owner can turn it off entirely here.

type Duration

type Duration time.Duration

Duration marshals as a Go duration string ("6h").

func (Duration) D

func (d Duration) D() time.Duration

func (Duration) MarshalJSON

func (d Duration) MarshalJSON() ([]byte, error)

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(b []byte) error

type Extras

type Extras struct {
	// Enabled turns extras (and Cinema Trailers) on.
	Enabled bool `json:"enabled"`
	// ResolverCommand is the path (or PATH name) of a yt-dlp-compatible
	// tool that prints a direct media URL for a video page. Empty disables
	// the command source. Ether ships no resolver binary.
	ResolverCommand string `json:"resolverCommand,omitempty"`
	// ResolverArgs is the fixed argument template passed to
	// ResolverCommand. The placeholders {url}, {site}, {key} and {id} are
	// substituted; the command always runs without a shell. Empty uses a
	// yt-dlp-compatible default ("-g -f b[ext=mp4]/b --no-playlist {url}").
	ResolverArgs []string `json:"resolverArgs,omitempty"`
	// ResolverTimeout bounds one resolver invocation (default 20s).
	ResolverTimeout Duration `json:"resolverTimeout,omitempty"`
	// PrewarmBytes is how much of the feature to pre-buffer while a
	// trailer plays (clamped to 32–64 MiB, default 48 MiB).
	PrewarmBytes int64 `json:"prewarmBytes,omitempty"`
}

Extras controls trailers and other clips attached to titles (Plex "Extras" and Cinema Trailers). TMDB only provides YouTube/Vimeo keys, which Plex apps cannot play, so a playable source must be configured by the owner as a yt-dlp-compatible resolver command. Off by default; with no working command Ether advertises no extras at all.

func (Extras) PrewarmBytesOr

func (e Extras) PrewarmBytesOr() int64

PrewarmBytesOr returns the trailer pre-buffer size, clamped to 32–64 MiB.

func (Extras) ResolverTimeoutOr

func (e Extras) ResolverTimeoutOr(dflt time.Duration) time.Duration

ResolverTimeoutOr returns the configured resolver timeout or dflt.

type FieldSealer added in v0.2.4

type FieldSealer interface {
	SealField(plain string, context ...string) (string, error)
	OpenField(stored string, context ...string) (plain string, legacy bool, err error)
}

FieldSealer protects secrets stored outside config.json (*Store implements it).

type Indexer

type Indexer struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	URL        string `json:"url"` // base URL; /api is appended
	APIKey     string `json:"apiKey"`
	DailyLimit int    `json:"dailyLimit"` // optional personal API cap per UTC day; 0 = automatic service limits
	DailyGrabs int    `json:"dailyGrabs"` // optional personal NZB cap per UTC day; 0 = automatic service limits
	Priority   int    `json:"priority"`
	Enabled    bool   `json:"enabled"`
	// Categories override; empty uses Newznab defaults (2000 movies, 5000 TV).
	MovieCategories []int `json:"movieCategories"`
	TVCategories    []int `json:"tvCategories"`
	// Protocol is "usenet" (Newznab, the default when empty) or "torrent"
	// (Torznab; results stream through a debrid provider).
	Protocol string `json:"protocol,omitempty"`
	// Kind selects how the indexer is searched: "" or IndexerKindNewznab
	// for a Newznab/Torznab API at URL, IndexerKindCardigann for a built-in
	// site run from an owner-installed definition, IndexerKindStremio for a
	// Stremio stream addon. For a built-in site URL is the site address and
	// Protocol follows the definition; for an addon URL is its install URL
	// and results are torrents.
	Kind string `json:"kind,omitempty"`
	// Definition is the installed definition id of a built-in site.
	Definition string `json:"definition,omitempty"`
	// Settings are the definition's setting values by name.
	Settings map[string]string `json:"settings,omitempty"`
	// SecretSettings are the definition's password-type settings. They are
	// sealed at rest and redacted in API responses like API keys.
	SecretSettings map[string]string `json:"secretSettings,omitempty"`
}

func (Indexer) IsBuiltin

func (ix Indexer) IsBuiltin() bool

IsBuiltin reports whether the indexer is a built-in site.

func (Indexer) IsStremio added in v0.2.17

func (ix Indexer) IsStremio() bool

IsStremio reports whether the indexer is a Stremio stream addon.

func (Indexer) IsTorrent

func (ix Indexer) IsTorrent() bool

IsTorrent reports whether the indexer's results are torrents (Torznab or a Stremio addon).

func (Indexer) UsesBackend added in v0.2.17

func (ix Indexer) UsesBackend() bool

UsesBackend reports whether the indexer is searched through a backend rather than the Newznab API: a built-in site or a Stremio addon.

type IndexerSites

type IndexerSites struct {
	// DefinitionsURL is an optional source of definitions the owner trusts
	// and has the right to use: a GitHub contents API URL, a GitHub folder
	// or raw folder URL, or a plain list of definition URLs. Empty = none;
	// definitions are then added only by upload.
	DefinitionsURL string `json:"definitionsUrl,omitempty"`
	// FlareSolverrURL is an optional FlareSolverr service used to pass
	// Cloudflare browser challenges, e.g. http://localhost:8191.
	FlareSolverrURL string `json:"flareSolverrUrl,omitempty"`
}

IndexerSites configures built-in indexer sites: owner-installed definitions in the Cardigann YAML format, run by internal/cardigann. Ether bundles no definitions and no definitions source.

type LLM

type LLM struct {
	Enabled bool `json:"enabled"` // off by default
	// Provider is an LLMProviders ID; empty means LLMProviderCustom.
	Provider string `json:"provider"`
	// BaseURL overrides the provider's endpoint for local servers and is
	// required for a custom OpenAI-compatible endpoint, e.g.
	// http://localhost:11434/v1. Hosted providers ignore it.
	BaseURL string `json:"baseUrl"`
	APIKey  string `json:"apiKey"`
	Model   string `json:"model"`
}

func (LLM) Endpoint added in v0.2.2

func (l LLM) Endpoint() (LLMProvider, string)

Endpoint resolves the provider and the base URL requests go to: the owner's BaseURL for local and custom servers when set, otherwise the provider default. An unknown provider resolves to an empty base URL.

type LLMProvider added in v0.2.2

type LLMProvider struct {
	ID    string `json:"id"`
	Label string `json:"label"`
	// API is the wire protocol (LLMAPIOpenAI, LLMAPIAnthropic, LLMAPIGoogle).
	API string `json:"api"`
	// BaseURL is the default endpoint; empty for LLMProviderCustom.
	BaseURL string `json:"baseUrl"`
	// NeedsKey is set for hosted services that reject anonymous calls.
	NeedsKey bool `json:"needsKey"`
	// Local servers run on the owner's network, so their endpoint is
	// editable (BaseURL is only the usual default).
	Local bool `json:"local"`
	// RequiresBaseURL is set when the owner must supply the endpoint.
	RequiresBaseURL bool `json:"requiresBaseUrl"`
	// KeyHint is the API-key placeholder; ModelHint explains model IDs.
	KeyHint   string `json:"keyHint,omitempty"`
	ModelHint string `json:"modelHint,omitempty"`
}

LLMProvider is one language-model service the owner can pick. The hosted entries mirror @braedonsaunders/appkit-ai's AI_PROVIDER_SPECS (same IDs, labels and endpoints) so Ether and AppKit apps configure providers the same way; Ollama and LM Studio are Ether's local additions.

func LookupLLMProvider added in v0.2.2

func LookupLLMProvider(id string) (LLMProvider, bool)

LookupLLMProvider finds a provider by ID; empty means LLMProviderCustom.

type Markers

type Markers struct {
	TheIntroDB bool   `json:"theIntroDb"`
	APIKey     string `json:"apiKey"`
}

type Notifier

type Notifier interface {
	Publish(ctx context.Context, topic string, payload []byte) error
	Subscribe(topic string, fn func([]byte)) (unsubscribe func())
}

Notifier is the cluster event bus as the config store uses it. A nil Notifier means no cross-node push; polling still picks changes up.

type OpenSubtitles

type OpenSubtitles struct {
	Enabled  bool   `json:"enabled"`
	APIKey   string `json:"apiKey"`
	Username string `json:"username"`
	Password string `json:"password"`
}

type Personalize

type Personalize struct {
	// PostPlay ranks the post-play "hook queue" per viewer instead of
	// returning generic related rows (#30). The ranked endpoint itself is
	// cheap; the switch exists for owners who prefer plain PMS behaviour.
	PostPlay bool `json:"postPlay"`
	// PostPlayPrebuffer pre-warms and pre-buffers the #1 post-play pick in
	// the last minutes of a movie or finale (#30).
	PostPlayPrebuffer bool `json:"postPlayPrebuffer"`
	// MicroReasons decorates single-item detail pages with the viewer's
	// decision header ("96% Match · … · Ends at 10:45 PM") (#31).
	MicroReasons bool `json:"microReasons"`
	// SessionIntent adapts /hubs to what the viewer is browsing tonight
	// (detail opens that did not turn into playback) (#32).
	SessionIntent bool `json:"sessionIntent"`
	// ClockFormat is the finish-time style of the decision header:
	// "12h", "24h", or "" to derive it from Catalog.Language.
	ClockFormat string `json:"clockFormat"`
}

Personalize holds the owner's switches for the Netflix-style per-viewer hooks (roadmap items #30–#32). All are on by default (see Defaults); a config written before this section existed keeps those defaults because loading unmarshals over them.

type Plex

type Plex struct {
	CompatibilityVersion       string `json:"compatibilityVersion,omitempty"`
	LatestCompatibilityVersion string `json:"latestCompatibilityVersion,omitempty"`
	// TrackLatestVersion advertises the newest public PMS release instead of
	// the version Ether's responses were verified against. Plex apps enable
	// features by server version, so tracking can reach endpoints Ether lacks.
	TrackLatestVersion bool `json:"trackLatestVersion,omitempty"`
	// Deprecated: retained for config compatibility; latest tracking is now opt-in.
	DisableVersionTracking bool `json:"disableVersionTracking,omitempty"`
	// Token is the owner's plex.tv token from sign-in; the server token.
	Token            string `json:"token"`
	ClientIdentifier string `json:"clientIdentifier"` // X-Plex-Client-Identifier Ether uses towards plex.tv
	OwnerPlexID      int64  `json:"ownerPlexId"`
	OwnerUsername    string `json:"ownerUsername"`
	Claimed          bool   `json:"claimed"`
	// Remote access
	RemoteAccess bool     `json:"remoteAccess"`
	UseUPnP      bool     `json:"useUpnp"`
	PublicPort   int      `json:"publicPort"` // manual port forward; 0 = same as Server.Port
	PublicHost   string   `json:"publicHost"` // override detected WAN IP
	CustomURLs   []string `json:"customUrls"`
	// TLS certificate for *.plex.direct (or a self-signed fallback).
	CertFile          string `json:"certFile"`
	KeyFile           string `json:"keyFile"`
	CertHash          string `json:"certHash"`          // plex.direct subdomain hash
	SecureConnections string `json:"secureConnections"` // "required", "preferred", "disabled"
}

type Posters added in v0.2.34

type Posters struct {
	Enabled     bool   `json:"enabled"`
	Source      string `json:"source"`
	BaseURL     string `json:"baseUrl"`
	APIKey      string `json:"apiKey"`
	URLTemplate string `json:"urlTemplate"`
	Profile     string `json:"profile"`
}

Posters configures optional owner-supplied artwork. Templates are secrets: exported provider URLs can embed account configuration and credentials.

func (Posters) Validate added in v0.2.34

func (p Posters) Validate() error

type Prowlarr

type Prowlarr struct {
	URL        string `json:"url"`
	APIKey     string `json:"apiKey"`
	DailyLimit int    `json:"dailyLimit"`
	DailyGrabs int    `json:"dailyGrabs"`
	Priority   int    `json:"priority"`
}

Prowlarr is an owner-managed source of Newznab indexers. Discovery is explicit and does not search or download from the indexers.

type Quality

type Quality struct {
	// Profiles by name. Rules pick a profile per client.
	Profiles []core.QualityProfile `json:"profiles"`
	Rules    []QualityRule         `json:"rules"`
	Default  string                `json:"default"`
	Remote   string                `json:"remote"` // profile for remote clients, "" = same as default
}

func DefaultQuality

func DefaultQuality() Quality

type QualityRule

type QualityRule struct {
	Product  string `json:"product,omitempty"`  // substring of X-Plex-Product
	Platform string `json:"platform,omitempty"` // substring of X-Plex-Platform
	UserID   int64  `json:"userId,omitempty"`
	Profile  string `json:"profile"`
}

QualityRule selects a profile when all non-empty matchers match.

type RTXVideo added in v0.2.4

type RTXVideo struct {
	Mode            string `json:"mode"`            // off, auto (LAN), on (also remote)
	ExecutablePath  string `json:"executablePath"`  // optional NVEncC64.exe, otherwise managed runtime or PATH
	SourceMaxHeight int    `json:"sourceMaxHeight"` // default 720
	TargetMaxHeight int    `json:"targetMaxHeight"` // default 1080, maximum 2160
	Quality         int    `json:"quality"`         // default 2, NGX quality 1..4 or streaming 21/23 (Ampere+); 0 uses default
	MaxConcurrent   int    `json:"maxConcurrent"`   // default 1, maximum 8
}

RTXVideo controls the optional NVEncC RTX Video backend. Empty mode is off. Enabling installs the pinned runtime on supported Windows nodes when no executable is configured. Paths remain node-local. Numeric zero values use the documented defaults; invalid values disable enhancement rather than silently substituting a non-AI scaler.

type Repair added in v0.2.8

type Repair struct {
	AdaptiveSummaries bool     `json:"adaptiveSummaries"`
	Enabled           bool     `json:"enabled"`
	WholeSlices       bool     `json:"wholeSlices,omitempty"`
	SummaryRows       int      `json:"summaryRows"`
	MaxDownloadBytes  int64    `json:"maxDownloadBytes"`
	LiveWait          Duration `json:"liveWait"`
	AheadWait         Duration `json:"aheadWait"`
}

Repair bounds opportunistic PAR2 reconstruction. Disabled until the owner opts in; the live benchmark enables it explicitly without editing config.

type Resolver

type Resolver struct {
	HealthRace           int                 `json:"healthRace"`      // 0 = ranked checks; >0 = first healthy of bounded concurrent candidates
	OptimisticStart      bool                `json:"optimisticStart"` // return top NZB before background health check
	CustomFormats        []core.CustomFormat `json:"customFormats,omitempty"`
	CustomFormatScores   map[string]int      `json:"customFormatScores,omitempty"`
	MinCustomFormatScore *int                `json:"minCustomFormatScore,omitempty"`
	AllowCam             bool                `json:"allowCam"` // owner setting, off by default
	HealthSamples        int                 `json:"healthSamples"`
	SearchCacheTTL       Duration            `json:"searchCacheTtl"`
	UnavailableCooloff   Duration            `json:"unavailableCooloff"` // hide titles with no release this long
	MaxCandidates        int                 `json:"maxCandidates"`
	BlockedGroups        []string            `json:"blockedGroups"`
	BlockedTerms         []string            `json:"blockedTerms"`
	// SourcePreference orders releases of equivalent quality by source:
	// "auto" (or empty) puts the source that measurably starts faster
	// first, and cached debrid first for popular titles until both
	// sources have measurements; "usenet" or "debrid" always prefer one.
	// Quality is never traded for speed (see resolver/routing.go).
	SourcePreference string `json:"sourcePreference,omitempty"`
	// SourceScoreMargin is how far below the leader's score a release of
	// the same quality tier may be and still be moved first for its
	// source (0 = 25).
	SourceScoreMargin float64 `json:"sourceScoreMargin,omitempty"`
}

type Scope

type Scope int

Scope says where a setting lives when nodes form a cluster.

A standalone server keeps every setting in config.json. In a cluster the shared part lives in the database (one document every node reads) and the node-local part stays in each node's config.json. The classification is expressed over the JSON keys of Config so that it can split and merge the documents without reflection; TestScopeClassifiesEveryField fails when a field is added without being classified here.

const (
	// Shared settings are identical on every node of a cluster.
	Shared Scope = iota
	// Local settings belong to one node: its listener, storage paths and
	// sizes, hardware, logging and database connection.
	Local
)

func ScopeOf

func ScopeOf(path string) Scope

ScopeOf reports the scope of a setting by its JSON path: a top-level key ("usenet") or a section and key ("server.port"). Unknown paths are shared, the safe default for a cluster (one value everywhere).

func (Scope) String

func (s Scope) String() string

type Server

type Server struct {
	// Port for the Plex API and web UI. PMS uses 32400.
	Port         int    `json:"port"`
	BindAddress  string `json:"bindAddress"`
	FriendlyName string `json:"friendlyName"`
	// MoviesLibraryName is the Movies section title shown in Plex apps.
	// Empty falls back to "Movies".
	MoviesLibraryName string `json:"moviesLibraryName"`
	// TVShowsLibraryName is the TV Shows section title shown in Plex apps.
	// Empty falls back to "TV Shows".
	TVShowsLibraryName string `json:"tvShowsLibraryName"`
	// MachineIdentifier is generated on first run and never changes.
	MachineIdentifier string `json:"machineIdentifier"`
	LogLevel          string `json:"logLevel"`
	// DefaultStreamLimit applies to users without their own limit.
	DefaultStreamLimit int `json:"defaultStreamLimit"`
	MaxStreams         int `json:"maxStreams"` // server-wide cap, 0 = derived from Usenet connections
	// ClusterWeight is this node's weight when a cluster divides
	// account-wide budgets (Usenet provider connections) among live nodes;
	// a node with weight 2 gets twice the share of a node with weight 1.
	// 0 means 1. Node-local; ignored on a standalone server.
	ClusterWeight int `json:"clusterWeight,omitempty"`
	// WebFrameOrigins permits embedding the owner UI in these exact HTTP(S)
	// origins (for example a dashboard). Empty denies framing everywhere.
	WebFrameOrigins []string `json:"webFrameOrigins,omitempty"`
}

type SharedBackend

type SharedBackend interface {
	// LoadShared returns the stored document; found is false when none
	// has been stored yet.
	LoadShared(ctx context.Context) (doc SharedDoc, found bool, err error)
	// SaveShared stores doc if the stored version is expectVersion (0:
	// nothing stored yet) and returns the new version, or
	// ErrSharedConflict.
	SaveShared(ctx context.Context, doc SharedDoc, expectVersion int64) (int64, error)
	// SharedVersion is the stored version (0 when none).
	SharedVersion(ctx context.Context) (int64, error)
}

SharedBackend stores the shared settings document (implemented over the database by internal/clusterconfig).

type SharedDoc

type SharedDoc struct {
	// Doc is the shared settings JSON with secrets sealed by the cluster's
	// credential key.
	Doc string
	// Version increases by one on every write.
	Version int64
	// KeyFingerprint is KeyFingerprint(credential key) of the cluster.
	KeyFingerprint string
}

SharedDoc is the stored form of the shared settings.

type SharedOptions

type SharedOptions struct {
	// Notifier pushes changes between nodes; may also be set later with
	// SetNotifier. Nil: polling only.
	Notifier Notifier
	// PollInterval re-reads the stored version (default
	// DefaultSharedPoll; negative disables polling and the watcher).
	PollInterval time.Duration
	// Timeout bounds each database call (default 10s).
	Timeout time.Duration
	// NoSeed makes AttachShared fail with ErrSharedMissing instead of
	// seeding an empty database from this node's settings.
	NoSeed bool
	Logger *slog.Logger
}

SharedOptions configure AttachShared.

type Simkl added in v0.2.41

type Simkl struct {
	Enabled      bool   `json:"enabled"`
	ClientID     string `json:"clientId"`
	ClientSecret string `json:"clientSecret"`
}

type Spike

type Spike struct {
	Enabled    bool     `json:"enabled"`
	LocalFiles []string `json:"localFiles"`
}

Spike serves local files as a library so the Plex front end can be verified against official apps without Usenet.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store loads, saves and broadcasts the config.

func Open

func Open(dataDir string) (*Store, error)

func (*Store) AttachShared

func (s *Store) AttachShared(ctx context.Context, b SharedBackend, o SharedOptions) error

AttachShared switches the store to cluster mode: it loads the shared settings from b (seeding b from this node's settings when it holds none, unless NoSeed) and overlays them on the node-local ones, refusing with ErrKeyMismatch when the cluster was sealed with another credential key. Until ctx ends it then follows changes made by other nodes.

func (*Store) Close

func (s *Store) Close()

Close stops following other nodes' changes (the watcher started by AttachShared and the notifier subscriptions) and waits for an in-flight refresh to finish. The store keeps working; it no longer refreshes.

func (*Store) Clustered

func (s *Store) Clustered() bool

Clustered reports whether shared settings come from the database.

func (*Store) CredentialKey

func (s *Store) CredentialKey() []byte

CredentialKey returns a copy of this node's credential key. Only the cluster join-token issuer needs it; never log or store it.

func (*Store) CredentialsUpgraded added in v0.2.4

func (s *Store) CredentialsUpgraded() bool

CredentialsUpgraded reports whether this node completed UpgradeCredentials (config.json is written in v2).

func (*Store) Get

func (s *Store) Get() Config

func (*Store) KeyCreated added in v0.2.4

func (s *Store) KeyCreated() bool

KeyCreated reports whether Open generated this data directory's credential key, i.e. nothing stored before this start was sealed with it. Database rows sealed under another key mean the key file was lost.

func (*Store) KeyFingerprint

func (s *Store) KeyFingerprint() string

KeyFingerprint identifies this node's credential key.

func (*Store) OpenField added in v0.2.4

func (s *Store) OpenField(stored string, context ...string) (string, bool, error)

OpenField reverses SealField for the same context. A value stored unsealed (written before the upgrade) is returned as is with legacy set, so the caller can re-seal it. A sealed value that does not open — wrong key, damaged, or moved to another context — is an error.

func (*Store) Path

func (s *Store) Path() string

func (*Store) RefreshShared

func (s *Store) RefreshShared(ctx context.Context) error

RefreshShared applies the stored shared settings if another node changed them, notifying subscribers. It is a no-op on a standalone server.

func (*Store) Seal

func (s *Store) Seal(name string, plain []byte) (string, error)

Seal encrypts plain with the credential key, bound to name.

func (*Store) SealField added in v0.2.4

func (s *Store) SealField(plain string, context ...string) (string, error)

SealField protects plain for storage, bound to context. Empty values stay empty. Until the store seals fields (SealsFields) it returns plain unchanged.

func (*Store) SealsFields added in v0.2.4

func (s *Store) SealsFields() bool

SealsFields reports whether SealField seals (database fields and shared settings are written in v2), which can precede CredentialsUpgraded on a cluster node that loaded settings another node upgraded.

func (*Store) SetNotifier

func (s *Store) SetNotifier(n Notifier)

SetNotifier connects (or, with nil, disconnects) the cluster event bus. It may be called before or after AttachShared.

func (*Store) SharedVersion

func (s *Store) SharedVersion() int64

SharedVersion is the version of the shared settings this node holds (0 on a standalone server).

func (*Store) Subscribe

func (s *Store) Subscribe(fn func(Config))

func (*Store) Unseal

func (s *Store) Unseal(name, sealed string) ([]byte, error)

Unseal reverses Seal for the same name.

func (*Store) Update

func (s *Store) Update(fn func(*Config) error) error

Update applies fn to a copy of the config, saves it and notifies subscribers.

On a cluster node the shared part is written to the database with an optimistic version check; when another node changed it first, fn is applied again to the fresh settings, so fn must only mutate its argument.

func (*Store) UpgradeCredentials added in v0.2.4

func (s *Store) UpgradeCredentials(ctx context.Context) error

UpgradeCredentials switches the store to v2 credentials (see above): config.json is rewritten in v2 and, on a cluster node, the shared settings are re-sealed in v2 unless another node already did. It is idempotent and resumable: config.json is rewritten atomically when the store switches (a configuration opened in v2 already has), and the shared settings are checked on every call, so a call interrupted between the two steps completes on the next.

type Trakt

type Trakt struct {
	Enabled      bool   `json:"enabled"`
	ClientID     string `json:"clientId"`
	ClientSecret string `json:"clientSecret"`
}

type Transcode

type Transcode struct {
	RTXVideo RTXVideo `json:"rtxVideo"`

	FFmpegPath           string `json:"ffmpegPath"` // empty = bundled, then PATH
	FFprobePath          string `json:"ffprobePath"`
	HWAccel              string `json:"hwAccel"` // "auto", "none", "videotoolbox", "nvenc", "qsv", "vaapi"
	VAAPIDevice          string `json:"vaapiDevice"`
	TempDir              string `json:"tempDir"`
	MaxTranscodes        int    `json:"maxTranscodes"`
	RemoteMaxBitrateKbps int    `json:"remoteMaxBitrateKbps"`

	// Adaptive HLS engine (additive). Numeric zero values select the
	// transcode package defaults; see Defaults for the shipped values.
	FastStart          bool     `json:"fastStart"`          // short, quick opening segments
	FastStartSegments  int      `json:"fastStartSegments"`  // opening segments in the ramp (default 2)
	FastStartSegment   Duration `json:"fastStartSegment"`   // ramp segment length (default 2s)
	FastStartMaxHeight int      `json:"fastStartMaxHeight"` // ramp encode height cap (default 720)
	ABRLadder          bool     `json:"abrLadder"`          // multi-rung master playlist / MPD
	ABRLadderUnderCopy bool     `json:"abrLadderUnderCopy"` // soft encode rungs under Direct Stream / video-copy (default on; set false to disable)
	VBVReservoir       bool     `json:"vbvReservoir"`       // constant-quality + generous VBV for capped transcodes
	VBVBufferSeconds   int      `json:"vbvBufferSeconds"`   // VBV buffer depth in seconds of maxrate (default 8)
	SpikeGovernor      bool     `json:"spikeGovernor"`      // clamp VBR spikes in copy streams to measured goodput
	SpikeMarginPercent int      `json:"spikeMarginPercent"` // spike threshold over goodput, percent (default 125)
}

type Updates

type Updates struct {
	Channel    string `json:"channel"` // "stable", "beta"
	AutoUpdate bool   `json:"autoUpdate"`
	FeedURL    string `json:"feedUrl"`
	// MaintenanceStart/End bound the local-time window ("HH:MM") in which
	// a downloaded update may be installed automatically. The window may
	// wrap midnight; equal values mean "any time".
	MaintenanceStart string `json:"maintenanceStart"`
	MaintenanceEnd   string `json:"maintenanceEnd"`
	// MaxDrain bounds how long a clustered node about to install an update
	// waits for its own playback sessions to end (it admits no new ones
	// meanwhile) before installing anyway inside the window. 0 means 30
	// minutes. Ignored on a standalone server, which waits for idle.
	MaxDrain Duration `json:"maxDrain,omitempty"`
}

type Usenet

type Usenet struct {
	Repair    Repair           `json:"repair"`
	Providers []UsenetProvider `json:"providers"`
	// MinIdle is how many connections per primary (non-backfill) provider
	// are kept warm (dialled, authenticated, DATE-pinged) at all times so
	// Play never pays a cold handshake. 0 = default (2); negative = off
	// (connections are dialled lazily and expire when idle). A provider's
	// own MinIdle overrides this.
	MinIdle int `json:"minIdle"`
}

type UsenetProvider

type UsenetProvider struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Host        string `json:"host"`
	Port        int    `json:"port"`
	TLS         bool   `json:"tls"`
	Username    string `json:"username"`
	Password    string `json:"password"`
	Connections int    `json:"connections"`
	// Priority: lower is tried first. Block accounts set Backfill.
	Priority int  `json:"priority"`
	Backfill bool `json:"backfill"` // only used for articles missing elsewhere
	Enabled  bool `json:"enabled"`
	// MinIdle overrides Usenet.MinIdle for this provider: 0 = inherit the
	// global setting, positive = that many warm connections, negative =
	// no warm floor for this provider. Backfill providers are never kept
	// warm regardless.
	MinIdle int `json:"minIdle,omitempty"`
}

Jump to

Keyboard shortcuts

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