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
- Variables
- func DefaultDataDir() string
- func InstallCredentialKey(dir string, key []byte) error
- func KeyFingerprint(key []byte) string
- func LocalJSON(c Config) []byte
- func ReadCredentialKey(dir string) ([]byte, error)
- func RedactDatabaseURL(raw string) string
- func SharedJSON(c Config) []byte
- type Cache
- type Catalog
- type Config
- type Database
- type Debrid
- type DebridProvider
- type Downloads
- type Duration
- type Extras
- type Indexer
- type IndexerSites
- type LLM
- type LLMProvider
- type Markers
- type Notifier
- type OpenSubtitles
- type Personalize
- type Plex
- type Prowlarr
- type Quality
- type QualityRule
- type Resolver
- type Scope
- type Server
- type SharedBackend
- type SharedDoc
- type SharedOptions
- type Spike
- type Store
- func (s *Store) AttachShared(ctx context.Context, b SharedBackend, o SharedOptions) error
- func (s *Store) Close()
- func (s *Store) Clustered() bool
- func (s *Store) CredentialKey() []byte
- func (s *Store) Get() Config
- func (s *Store) KeyFingerprint() string
- func (s *Store) Path() string
- func (s *Store) RefreshShared(ctx context.Context) error
- func (s *Store) Seal(name string, plain []byte) (string, error)
- func (s *Store) SetNotifier(n Notifier)
- func (s *Store) SharedVersion() int64
- func (s *Store) Subscribe(fn func(Config))
- func (s *Store) Unseal(name, sealed string) ([]byte, error)
- func (s *Store) Update(fn func(*Config) error) error
- type Trakt
- type Transcode
- type Updates
- type Usenet
- type UsenetProvider
Constants ¶
const ( IndexerKindNewznab = "newznab" IndexerKindCardigann = "cardigann" )
Indexer kinds (Indexer.Kind).
const ( DatabaseSQLite = "sqlite" DatabasePostgres = "postgres" )
const ( DebridRealDebrid = "realdebrid" DebridTorBox = "torbox" DebridAllDebrid = "alldebrid" DebridPremiumize = "premiumize" )
Debrid kinds (DebridProvider.Kind).
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.
const ( // 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 = 30 * time.Second )
const CredentialKeyFile = ".credentials-key"
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.
const LLMProviderCustom = "custom"
LLMProviderCustom is any OpenAI-compatible endpoint the owner supplies.
Variables ¶
var ( // 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 = errors.New("config: the cluster database holds no shared settings") )
var DebridKinds = []string{DebridRealDebrid, DebridTorBox, DebridAllDebrid, DebridPremiumize}
DebridKinds lists the supported debrid services.
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 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 InstallCredentialKey ¶
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 KeyFingerprint ¶
KeyFingerprint identifies a credential key without revealing it. Cluster nodes compare fingerprints to prove they hold the same key.
func ReadCredentialKey ¶
ReadCredentialKey returns dir's credential key, or os.ErrNotExist.
func RedactDatabaseURL ¶
RedactDatabaseURL masks the password in a connection URL.
func SharedJSON ¶
SharedJSON is the cluster-shared part of c as JSON (unsealed).
Types ¶
type Cache ¶
type Cache struct {
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 {
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 {
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"`
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 MergeShared ¶
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.
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=require
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 ¶
Effective applies the ETHER_DATABASE_URL override (read through getenv) and normalises the driver name.
func (Database) IsPostgres ¶
IsPostgres reports whether these (effective) settings select PostgreSQL.
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 ¶
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
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 ¶
Duration marshals as a Go duration string ("6h").
func (Duration) MarshalJSON ¶
func (*Duration) UnmarshalJSON ¶
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 ¶
PrewarmBytesOr returns the trailer pre-buffer size, clamped to 32–64 MiB.
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. For a built-in site URL
// is the site address and Protocol follows the definition.
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"`
}
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 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 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"`
DisableVersionTracking bool `json:"disableVersionTracking"`
// 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 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 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"`
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.
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"`
// 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"`
}
type SharedBackend ¶
type SharedBackend interface {
// has been stored yet.
LoadShared(ctx context.Context) (doc SharedDoc, found bool, err error)
// nothing stored yet) and returns the new version, or
// ErrSharedConflict.
SaveShared(ctx context.Context, doc SharedDoc, expectVersion int64) (int64, error)
SharedVersion(ctx context.Context) (int64, error)
}
SharedBackend stores the shared settings document (implemented over the database by internal/clusterconfig).
type SharedOptions ¶
type SharedOptions struct {
// SetNotifier. Nil: polling only.
Notifier Notifier
// DefaultSharedPoll; negative disables polling and the watcher).
PollInterval time.Duration
Timeout time.Duration
// NoSeed makes AttachShared fail with ErrSharedMissing instead of
// seeding an empty database from this node's settings.
}
SharedOptions configure AttachShared.
type Spike ¶
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 (*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) CredentialKey ¶
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) KeyFingerprint ¶
KeyFingerprint identifies this node's credential key.
func (*Store) RefreshShared ¶
RefreshShared applies the stored shared settings if another node changed them, notifying subscribers. It is a no-op on a standalone server.
func (*Store) SetNotifier ¶
SetNotifier connects (or, with nil, disconnects) the cluster event bus. It may be called before or after AttachShared.
func (*Store) SharedVersion ¶
SharedVersion is the version of the shared settings this node holds (0 on a standalone server).
func (*Store) Update ¶
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.
type Transcode ¶
type Transcode struct {
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
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 {
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"`
}