Documentation
¶
Overview ¶
Package admin is Ether's owner-facing HTTP surface: the JSON API under /ether/api/ and the embedded React SPA (setup wizard, dashboard, settings, logs, resolver traces) served under /ether/.
The package deliberately depends only on internal/config, internal/core (for types) and the small interfaces/funcs declared in Deps; internal/app wires the real implementations. The SPA is embedded in the same binary, so every endpoint it calls is always present: the component deps in Deps are required, and a handler only reaches the deps its routes use.
Mounting:
mux.Handle("/ether/", admin.New(admin.Deps{Config: store, ...}))
The handler accepts both "/ether/..." paths and paths already stripped of the prefix (http.StripPrefix style).
Logging: pass a shared ring buffer so the Logs page can tail the server:
buf := admin.NewLogBuffer(2000)
logger := slog.New(admin.TeeHandler(slog.NewTextHandler(os.Stderr, nil), buf))
h := admin.New(admin.Deps{Config: store, Logs: buf, Logger: logger, ...})
Index ¶
- Constants
- Variables
- func CacheVolumeMounted() bool
- func DiskFree(path string) (int64, error)
- func DiskUsage(path string) (total, free int64, err error)
- func ImageURL(path string, w int) string
- func New(deps Deps) http.Handler
- func RedactConfig(cfg config.Config) config.Config
- func SetupOpen(setupComplete bool, r *http.Request, trusted ...netip.Prefix) bool
- func Shell() http.Handler
- func ShouldAdoptCacheVolume(setupComplete bool, storage, dir, dataDir string, mounted bool) bool
- func TeeHandler(next slog.Handler, buf *LogBuffer) slog.Handler
- func UnredactIndexer(ix config.Indexer, prev config.Config) config.Indexer
- type Access
- type CatalogBrowser
- type CatalogHelper
- type CollectionStudio
- type Deps
- type FavoritesMirror
- type FieldError
- type FrontendAdmin
- type FrontendInfo
- type Handler
- type ImageProxy
- type LLMModel
- type LogBuffer
- type LogEntry
- type NotInterestedEntry
- type NotInterestedPortal
- type Personalization
- type PlexAuth
- type PlexWatch
- type PlexWatchStatus
- type Sessions
- type SetupStep
- type SignIn
- type StatusFunc
- type StatusFuncE
- type TitleCard
- type UserStore
- type ViewerAlertPreferences
- type ViewerRecord
Constants ¶
const RedactedSecret = "••••"
RedactedSecret is the placeholder shown in place of a stored secret. A PUT that sends it back unchanged preserves the stored value.
Variables ¶
var ( ErrPlexAccountMismatch = errors.New("plex account does not belong to this viewer") ErrPlexNotEligible = errors.New("account has not allowed watch-state sync") ErrPlexWatchRevoked = errors.New("plex rejected this viewer's token") )
Sentinels returned by the app adapter so this package does not import the watch client.
Functions ¶
func CacheVolumeMounted ¶ added in v0.3.0
func CacheVolumeMounted() bool
CacheVolumeMounted reports whether /cache is a mount point. Docker installs bind the segment-cache volume there; a plain directory on the root disk is not the volume.
func DiskUsage ¶ added in v0.2.48
DiskUsage reports the size and free bytes of the volume holding path.
func RedactConfig ¶
RedactConfig returns a copy of cfg with every secret replaced by RedactedSecret ("••••"). Empty secrets stay empty so the UI can tell "unset" from "set". The input is not modified.
func SetupOpen ¶ added in v0.3.0
SetupOpen is setupOpen for handlers outside the admin API (a failed start's recovery page): whether r gets tokenless local-network access to a server whose setup is not complete.
func Shell ¶ added in v0.3.0
Shell serves the console's static files without its API, for a server whose settings and database are not open yet (internal/startup's status page). The owner's frame allowlist is not loaded then, so framing is refused outright.
func ShouldAdoptCacheVolume ¶ added in v0.3.0
ShouldAdoptCacheVolume is true on a fresh install whose cache folder is still the default inside the config directory and a /cache volume is mounted.
func TeeHandler ¶
TeeHandler returns an slog.Handler that writes to next and records into buf (e.g. console + in-memory ring for the Logs page).
func UnredactIndexer ¶
UnredactIndexer restores an indexer's API key and secret settings that the UI sent back as placeholders, from the stored config (for tests of unsaved edits run outside this package). A placeholder that is not bound to the stored entry (see UnredactConfig) comes back empty.
Types ¶
type Access ¶ added in v0.2.4
Access is how requireAuth admitted a request: with a validated owner token, or (Owner false) through tokenless local-network setup access.
func RequestAccess ¶ added in v0.2.4
RequestAccess reports how the admin API admitted the request whose context is ctx. Only requireAuth sets it, from its own checks, so extension handlers can demand more than admission (an owner token, a finished setup); ok is false for a request that did not pass through requireAuth.
type CatalogBrowser ¶ added in v0.2.9
type CatalogBrowser interface {
BrowseTitles(context.Context, core.BrowseQuery) (core.Page, error)
}
CatalogBrowser exposes paginated local browsing without a remote TMDB search.
type CatalogHelper ¶
type CatalogHelper interface {
SearchTitles(ctx context.Context, q string, t core.MediaType, limit int) ([]core.Title, error)
// Facets: genre, decade, contentRating, studio, country, language.
Facets(ctx context.Context, t core.MediaType, facet string) ([]core.FacetValue, error)
// Lookup kinds: person, keyword, company, network, collection, watchProvider.
Lookup(ctx context.Context, kind, q, region string) ([]core.LookupResult, error)
TitleDetail(ctx context.Context, key core.RatingKey) (*core.Title, error)
}
CatalogHelper backs /ether/api/catalog/*. Implemented by internal/collections.
type CollectionStudio ¶
type CollectionStudio interface {
ListSpecs(ctx context.Context) ([]core.CollectionSpec, error)
GetSpec(ctx context.Context, id string) (*core.CollectionSpec, error)
CreateSpec(ctx context.Context, spec core.CollectionSpec) (*core.CollectionSpec, error)
UpdateSpec(ctx context.Context, id string, spec core.CollectionSpec) (*core.CollectionSpec, error)
DeleteSpec(ctx context.Context, id string) error
Preview(ctx context.Context, spec core.CollectionSpec, n int) (*core.CollectionPreview, error)
Rebuild(ctx context.Context, id string) (*core.CollectionSpec, error)
Templates() []core.CollectionTemplate
Generate(ctx context.Context, prompt, mediaType string) (*core.CollectionSpec, string, error)
// Artwork returns the collection's poster at width w.
Artwork(ctx context.Context, id string, w int) ([]byte, string, time.Time, error)
}
CollectionStudio backs /ether/api/collections*. Implemented by internal/collections.
Errors are mapped by behaviour, so implementations need not import this package: core.ErrNotFound → 404; an error with InvalidFields() map[string]string → 400 with per-field messages; NotConfigured() bool (the owner has not enabled the LLM that Generate drafts with) → 501; Conflict() bool → 409; Upstream() bool → 502.
type Deps ¶
type Deps struct {
Frontends FrontendAdmin
FavoritesMirror FavoritesMirror
NotInterested NotInterestedPortal
ViewerAlerts ViewerAlertPreferences
ActiveViewer func(context.Context, int64) (*core.User, error)
ViewerPortalURL func(context.Context, *http.Request) (string, error)
StremioPortalURL func(context.Context, *http.Request) (string, error)
CreateDLNAGuest func(context.Context) (*core.Viewer, error)
LocalIdentity *identity.Local
IdentityGrants *identity.Grants
IdentityLimiter *identity.LoginLimiter
// OwnerCredentialChanged runs after an owner local sign-in, setup and
// any local password change (the app refreshes the owner's recovery
// sign-in from it). Nil does nothing.
OwnerCredentialChanged func(context.Context)
PollLimiter *identity.LoginLimiter
QuickConnect *identity.QuickConnect
Invites *identity.Invite
RefreshViewers func(context.Context) error
OwnerConfigured func(context.Context) bool
LocalOwnerConfigured func(context.Context) bool
SetViewerActive func(context.Context, int64, bool) error
AuthenticatePlex func(context.Context, string) (*core.User, error)
LinkPlex func(context.Context, int64, string) error
// Extensions are owner-authenticated API subtrees keyed by relative path.
Extensions map[string]http.Handler
ValidateExtra func(config.Config) []FieldError
// Config is the owner settings store. Required.
Config *config.Store
// Logger defaults to slog.Default().
Logger *slog.Logger
// Logs is the in-memory log ring served at /ether/api/logs. A fresh
// 2000-entry buffer is created when nil (retrieve it via
// Handler.Logs()); pass a shared one to tail server logs.
Logs *LogBuffer
// LogDir holds the on-disk logs offered by GET /api/logs/download;
// empty offers the in-memory buffer instead.
LogDir string
// Support is the in-memory problem-capture recorder. Nil answers the
// support routes with the same not-found result as an unknown path.
Support *diagnostics.Recorder
// Version is reported by /ether/api/bootstrap ("dev" when empty).
Version string
// PlexAuth drives wizard step 1 (plex.tv PIN sign-in and claim).
PlexAuth PlexAuth
// SignIn powers "Sign in with Plex" on the admin login screen.
SignIn SignIn
// IsOwnerToken reports whether a bearer token (an owner credential or
// a console session) belongs to the owner.
IsOwnerToken func(ctx context.Context, token string) bool
// Sessions backs console sign-in and sign-out.
Sessions Sessions
// Provider/integration testers used by the wizard and settings pages.
TestUsenet func(ctx context.Context, p config.UsenetProvider) error
TestIndexer func(ctx context.Context, ix config.Indexer) error
TestDebrid func(ctx context.Context, p config.DebridProvider) error
TestTMDB func(ctx context.Context, key string) error
TestOpenSubtitles func(ctx context.Context, cfg config.OpenSubtitles) error
TestLLM func(ctx context.Context, cfg config.LLM) error
// LLMModels lists the models the provider in cfg offers.
LLMModels func(ctx context.Context, cfg config.LLM) ([]LLMModel, error)
TestTrakt func(ctx context.Context, cfg config.Trakt) error
// TestIntroDB probes TheIntroDB with the posted key, or the stored key
// when the post is empty or still redacted.
TestIntroDB func(ctx context.Context, apiKey string) error
// Dashboard status providers. Each non-nil provider appears on the
// dashboard and at /ether/api/status/{name}.
SessionsStatus StatusFunc // "sessions" — now playing
CacheStatus StatusFunc // "cache" — usage; {used,budget} renders as a bar
ProvidersStatus StatusFunc // "providers" — Usenet provider health
IndexersStatus StatusFunc // "indexers" — budgets remaining
CatalogStatus StatusFunc // "catalog" — catalogue sync/stats
RemoteStatus StatusFunc // "remote" — remote access
HWAccelStatus StatusFunc // "hwaccel" — transcoding hardware
// CacheRestartRequired reports whether the saved folder/storage differs
// from the running cache, including changes made during initial setup.
CacheRestartRequired func() bool
// SystemLoad is this machine's recent CPU, memory, network and GPU
// load, served on its own route because only the Overview polls it.
SystemLoad StatusFunc
// ExtraStatus adds/overrides named status providers generically. The
// "widget" entry is required: it backs GET /ether/api/status.
ExtraStatus map[string]StatusFunc
// ResolverTraces backs GET /ether/api/traces.
ResolverTraces StatusFuncE
// Users backs the user-administration and viewer endpoints.
Users UserStore
// ViewerDirectory lists stored identities for owner administration, including
// imported or inactive viewers. It must never be used for authentication.
// Nil preserves the active-user fallback for smaller integrations.
ViewerDirectory func(context.Context) ([]ViewerRecord, error)
// TestTimeout bounds each wizard test call (default 20s).
TestTimeout time.Duration
// StatusTimeout bounds each status provider call (default 5s).
StatusTimeout time.Duration
// DiskUsage overrides free-space reporting (tests). Nil → OS statfs.
DiskUsage func(path string) (total, free int64, err error)
// Personalization backs /api/viewers/*, /api/layout and
// /api/insights/* (internal/personalize.Engine). Viewers are listed
// from Users.
Personalization Personalization
// Collections backs /api/collections* (Collection Studio).
Collections CollectionStudio
// CatalogHelper backs /api/catalog/*.
CatalogHelper CatalogHelper
CatalogVisibility func(context.Context, []core.LibraryExclusion, string) (any, error)
// WaitingReleaseKeys are in-window movies still hidden from viewers.
// Nil leaves the waiting catalog filter empty.
WaitingReleaseKeys func(context.Context) []core.RatingKey
// Images backs the TMDB image proxy at /api/image.
Images ImageProxy
// PlexWatch is the per-viewer Plex account watch-state integration.
// Nil hides the routes.
PlexWatch PlexWatch
}
Deps are the dependencies of the admin handler. The component dependencies (Plex sign-in, testers, traces, users, personalization, collections, catalogue helper, image proxy) are required. Only the tuning knobs, Logger/Logs, the dashboard status providers, Extensions, ValidateExtra and DiskUsage have defaults or may be left empty.
type FavoritesMirror ¶ added in v0.3.0
type FavoritesMirror interface {
MirrorEnabled(context.Context, *core.User) (bool, error)
SetMirror(context.Context, *core.User, bool) error
}
FavoritesMirror is viewer presentation state, not the favorites themselves.
type FieldError ¶
FieldError is one validation problem, addressed by dotted field path.
func UnredactConfig ¶
func UnredactConfig(next, prev config.Config) (config.Config, []FieldError)
UnredactConfig restores secrets in next that the client sent back as the redaction placeholder, using prev (the currently stored config). List entries are matched by stable ID only, so reordering a list does not swap secrets. A placeholder whose entry is new, has no ID, or now points somewhere else is not restored: it is reported as a FieldError and left empty.
func ValidateConfig ¶
func ValidateConfig(cfg config.Config) []FieldError
ValidateConfig checks a candidate config for the problems the owner can fix in the UI. It is intentionally permissive about empty optional sections (an unconfigured install must remain saveable mid-wizard) and strict about structurally broken values.
func (FieldError) Error ¶
func (e FieldError) Error() string
type FrontendAdmin ¶ added in v0.3.0
type FrontendAdmin interface {
List(context.Context) ([]FrontendInfo, error)
Settings(context.Context, frontend.Protocol) (frontend.Settings, error)
PutSettings(context.Context, frontend.Protocol, frontend.Settings) error
Toggle(context.Context, frontend.Protocol, bool) error
}
FrontendAdmin is supplied by the app's protocol lifecycle manager.
type FrontendInfo ¶ added in v0.3.0
type FrontendInfo struct {
Descriptor frontend.Descriptor `json:"descriptor"`
Enabled bool `json:"enabled"`
Status frontend.Status `json:"status"`
}
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is the admin HTTP handler. Use NewHandler when you need access to the log buffer the handler created.
func NewHandler ¶
NewHandler builds the admin handler. It panics when Deps.Config is nil.
type ImageProxy ¶
ImageProxy serves TMDB artwork by path at the TMDB size covering w. Implemented by internal/artwork. It must reject anything that is not a TMDB image path.
type LogBuffer ¶
type LogBuffer struct {
// contains filtered or unexported fields
}
LogBuffer is a concurrency-safe ring of recent log records. It backs the Logs page; install its Handler (usually teed with the console handler via TeeHandler) as the server logger.
func NewLogBuffer ¶
NewLogBuffer creates a ring holding at most capacity entries (default 2000 when capacity <= 0).
func (*LogBuffer) Entries ¶
Entries returns up to limit entries, oldest first. level filters to that level or more severe ("info" includes warn+error); q is a case-insensitive substring over message and attrs. Empty filters match all.
type LogEntry ¶
type LogEntry struct {
Time time.Time `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
Attrs map[string]string `json:"attrs,omitempty"`
}
LogEntry is one captured log record.
type NotInterestedEntry ¶ added in v0.3.0
type NotInterestedPortal ¶ added in v0.3.0
type Personalization ¶
type Personalization interface {
ViewerStats(ctx context.Context, userIDs []int64) (map[int64]core.ViewerStats, error)
RecentActivity(ctx context.Context, userID int64, limit int) ([]core.ViewerActivity, error)
Taste(ctx context.Context, user *core.User) (*core.TasteProfile, error)
SetTasteOverrides(ctx context.Context, user *core.User, overrides []core.TasteOverride) (*core.TasteProfile, error)
// PreviewHome is exactly what /hubs (or /hubs/sections/{id} when
// section is "movies", "shows" or a section id) serves the viewer in
// the context.
PreviewHome(ctx context.Context, user *core.User, hc core.HubContext, section string, limit int) (*core.HomePreview, error)
Refresh(ctx context.Context, user *core.User) error
// HomeLayout is the saved Home layout document; SetHomeLayout
// validates and replaces it, applying from the next /hubs request.
HomeLayout(ctx context.Context) (core.HomeLayout, error)
SetHomeLayout(ctx context.Context, d core.HomeLayout) (core.HomeLayout, error)
// PreviewLayout assembles what the viewer would see under a draft
// document without saving it.
PreviewLayout(ctx context.Context, user *core.User, hc core.HubContext, section string, limit int, d core.HomeLayout) (*core.HomePreview, error)
// LayoutDefaults is the default row count per section (home, movies,
// shows).
LayoutDefaults() map[string]int
AvailableRows(ctx context.Context) ([]core.RowDescriptor, error)
RowInsights(ctx context.Context, days int) ([]core.RowInsight, error)
// RankerInsight describes the learned ranking model.
RankerInsight(ctx context.Context) (core.RankerInsight, error)
Activity(ctx context.Context, days int) ([]core.ActivityDay, error)
}
Personalization is the engine behind the viewers, Home Designer and insights endpoints. Implemented by internal/personalize.Engine.
type PlexAuth ¶
type PlexAuth interface {
// StartPIN begins the PIN flow: id is the poll handle, code is the
// 4-character code shown to the owner, authURL is the plex.tv page to open.
StartPIN(ctx context.Context) (id, code, authURL string, err error)
// PollPIN reports whether the owner has approved the PIN yet. When done
// is true the implementation has recorded the owner account for Claim.
PollPIN(ctx context.Context, id string) (done bool, err error)
// Claim claims this server to the signed-in account.
Claim(ctx context.Context) error
// Status returns a JSON-able snapshot (claimed, owner, connections...).
Status() any
}
PlexAuth is the plex.tv sign-in/claim surface the wizard needs. Implemented by internal/plextv.
type PlexWatch ¶ added in v0.2.57
type PlexWatch interface {
Status(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
BeginPIN(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
FinishPIN(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
SetEnabled(ctx context.Context, userID, plexID int64, on bool) (PlexWatchStatus, error)
Disconnect(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
Retry(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
Resolve(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
ExportCurrent(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
ImportCurrent(ctx context.Context, userID, plexID int64) (PlexWatchStatus, error)
}
PlexWatch is the per-viewer account watch-state integration. Handlers always pass the stored viewer's Plex account id, never the owner's.
type PlexWatchStatus ¶ added in v0.2.57
type PlexWatchStatus struct {
Connected bool `json:"connected"`
PlexID int64 `json:"plexId,omitempty"`
Username string `json:"username,omitempty"`
Enabled bool `json:"enabled"`
Eligibility string `json:"eligibility"`
Revoked bool `json:"revoked"`
Pending int `json:"pending"`
Held int `json:"held"`
HeldLegacy int `json:"heldLegacy"`
Dead int `json:"dead"`
InFlight bool `json:"inFlight"`
Skipped int `json:"skipped"`
Restricted bool `json:"restricted"`
PINCode string `json:"pinCode,omitempty"`
PINURL string `json:"pinUrl,omitempty"`
PINExpires string `json:"pinExpires,omitempty"`
AccountMatch bool `json:"accountMatch"`
}
PlexWatchStatus is the viewer-facing connection. It never includes a token.
type Sessions ¶ added in v0.2.48
type Sessions interface {
// IssueOwnerSession exchanges an owner credential for a session. It
// fails with core.ErrUnauthorized unless credential is the owner's.
IssueOwnerSession(ctx context.Context, credential string) (token string, expires time.Time, err error)
// RevokeOwnerSession ends a session; other tokens are ignored.
RevokeOwnerSession(ctx context.Context, token string) error
}
Sessions issues and revokes owner console sessions (satisfied by *auth.Service). Sign-in hands the browser a session that only opens this API, never the owner's plex.tv account token.
type SetupStep ¶
type SetupStep struct {
ID string `json:"id"`
Title string `json:"title"`
Complete bool `json:"complete"`
Required bool `json:"required"`
Detail string `json:"detail,omitempty"`
}
SetupStep is one wizard step's completion state.
type SignIn ¶
type SignIn interface {
// StartPIN creates a PIN; forwardURL is where plex.tv sends the browser
// after approval.
StartPIN(ctx context.Context, forwardURL string) (id, code, authURL string, err error)
// PINToken returns the account token once approved, "" while pending.
PINToken(ctx context.Context, id string) (string, error)
}
SignIn is a plex.tv PIN sign-in that, unlike PlexAuth, changes nothing on the server: it only yields the signed-in account's token so the admin UI can authenticate as that account.
type StatusFunc ¶
StatusFunc returns a JSON-able status blob for a dashboard card. It may return (any, error) shaped values via StatusFuncE; StatusFunc never fails.
type StatusFuncE ¶
StatusFuncE is a StatusFunc that can fail.
type TitleCard ¶
type TitleCard struct {
Key string `json:"key"`
Type string `json:"type"`
Title string `json:"title"`
Year int `json:"year,omitempty"`
Summary string `json:"summary,omitempty"`
Poster string `json:"poster,omitempty"`
Backdrop string `json:"backdrop,omitempty"`
Rating float64 `json:"rating,omitempty"`
ContentRating string `json:"contentRating,omitempty"`
// ContentRatingSource is where ContentRating came from ("tmdb:US:6",
// "omdb"); see core.Title.
ContentRatingSource string `json:"contentRatingSource,omitempty"`
RuntimeMinutes int `json:"runtimeMinutes,omitempty"`
Genres []string `json:"genres,omitempty"`
ShowTitle string `json:"showTitle,omitempty"`
Season int `json:"season,omitempty"`
Episode int `json:"episode,omitempty"`
}
TitleCard is the shared title shape of the owner API.
type UserStore ¶
type UserStore interface {
Users(ctx context.Context) ([]core.User, error)
UpdateUser(ctx context.Context, u core.User) error
}
UserStore is the user-administration surface (satisfied by core.Auth implementations; only the two admin methods are required here).
Source Files
¶
- admin.go
- auth.go
- cache_volume.go
- cache_volume_unix.go
- catalog_api.go
- collections_api.go
- config.go
- connect_apps.go
- connect_urls.go
- disk_unix.go
- dlna_guest.go
- favorite_episode_alert.go
- favorites_mirror.go
- frames.go
- frontend_qr.go
- frontends_api.go
- handlers.go
- http.go
- identity_api.go
- insights_api.go
- layout_api.go
- logbuffer.go
- logs_download.go
- notinterested.go
- outbound.go
- plexwatch_api.go
- ratelimit.go
- redact.go
- signin.go
- spa.go
- support.go
- validate.go
- viewers_api.go
- widget.go