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
- func DiskFree(path string) (int64, error)
- func ImageURL(path string, w int) string
- func New(deps Deps) http.Handler
- func RedactConfig(cfg config.Config) config.Config
- 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 FieldError
- type Handler
- type ImageProxy
- type LLMModel
- type LogBuffer
- type LogEntry
- type Personalization
- type PlexAuth
- type SetupStep
- type SignIn
- type StatusFunc
- type StatusFuncE
- type TitleCard
- type UserStore
- 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 ¶
This section is empty.
Functions ¶
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 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 {
// 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
// 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 belongs to the owner.
IsOwnerToken func(ctx context.Context, token string) bool
// 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
// 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) (any, error)
// Images backs the TMDB image proxy at /api/image.
Images ImageProxy
}
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 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 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 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 stored the owner token in config.
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 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"`
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.