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

plextv

package
v0.2.20 Latest Latest
Warning

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

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

Documentation

Overview

Package plextv is Ether's plex.tv client: PIN sign-in, server claim, connection publishing, resources, shared users and their server access tokens, and plex.direct certificates. It also holds the Registrar that runs these periodically.

Index

Constants

View Source
const (
	ClaimExchange   = "claim-exchange"
	ClaimOwnerToken = "owner-token"
)

Claim methods reported in ClaimResult.

View Source
const (
	// SharedCertName is the shared blob holding the certificate.
	SharedCertName = "plextv/plexdirect"
	// CertTopic is published after a node stores a new certificate.
	CertTopic = "certs:changed"
	// DefaultCertSyncInterval is RunSharedSync's default poll interval.
	DefaultCertSyncInterval = time.Minute
)
View Source
const (
	// DefaultBaseURL is plex.tv's API host.
	DefaultBaseURL = "https://plex.tv"
	// DefaultServersURL is the PMS account API host observed in current PMS traffic.
	DefaultServersURL = "https://servers.plex.tv"
	// DefaultAuthAppURL is the hosted sign-in page PIN codes are approved on.
	DefaultAuthAppURL = "https://app.plex.tv/auth"
	// DefaultServerVersion is the PMS version Ether reports towards plex.tv
	// and clients. Official apps gate features on server version, so it
	// tracks a recent PMS release rather than Ether's own version.
	DefaultServerVersion = "1.43.4.10903-e5521bd8c"
	// ServerProduct is the product name of a Plex Media Server device.
	ServerProduct = "Plex Media Server"
	// AppProduct is the product shown for the setup wizard's sign-in.
	AppProduct = "Ether"
)
View Source
const PubsubTLSName = "pubsub.plex.tv"

PubsubTLSName is the name pubsub hosts' certificates are valid for.

Variables

View Source
var ErrCertUnavailable = errors.New("plextv: plex.direct certificate not available for this server")

ErrCertUnavailable means plex.tv will not issue a plex.direct certificate for this device (no subject, or the endpoint is refused).

View Source
var ErrPINExpired = errors.New("plextv: sign-in PIN expired")

ErrPINExpired is returned by WaitPIN when the PIN expired unapproved.

View Source
var ErrRateLimited = errors.New("plex.tv rate limit exceeded")

ErrRateLimited matches plex.tv 429 responses (errors.Is).

View Source
var ErrRenewalDeferred = errors.New("plextv: certificate renewal is handled by the cluster leader")

ErrRenewalDeferred: this node may not request certificates (another cluster node is the leader).

Functions

func Fingerprint

func Fingerprint(c *x509.Certificate) string

Fingerprint is the hex SHA-256 of a certificate's DER.

func LatestPublicVersion

func LatestPublicVersion(ctx context.Context, hc *http.Client, endpoint string) (string, error)

LatestPublicVersion reads the public Plex release manifest. It only reads version metadata; it never downloads or executes a Plex server binary.

func NewerPMSVersion

func NewerPMSVersion(a, b string) bool

func PMSVersion

func PMSVersion(c config.Config) string

PMSVersion is the version Ether reports: the owner override, else the latest public release when the owner opted into tracking, else the version the PMS conformance fixtures were recorded against.

func ValidPMSVersion

func ValidPMSVersion(v string) bool

Types

type APIError

type APIError struct {
	Method     string
	Path       string
	Status     int
	Code       int // plex.tv error code, e.g. 1001
	Message    string
	RetryAfter time.Duration
}

APIError is a non-2xx plex.tv response.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is maps HTTP statuses to sentinel errors: 401/403 -> core.ErrUnauthorized, 404 -> core.ErrNotFound, 429 -> ErrRateLimited.

type Account

type Account struct {
	ID           int64        `json:"id"`
	UUID         string       `json:"uuid"`
	Username     string       `json:"username"`
	Title        string       `json:"title"`
	FriendlyName string       `json:"friendlyName"`
	Email        string       `json:"email"`
	Thumb        string       `json:"thumb"`
	AuthToken    string       `json:"authToken"`
	Home         bool         `json:"home"`
	HomeAdmin    bool         `json:"homeAdmin"`
	Restricted   bool         `json:"restricted"`
	Subscription Subscription `json:"subscription"`
}

Account is a plex.tv user (GET /api/v2/user).

func (*Account) DisplayName

func (a *Account) DisplayName() string

DisplayName prefers the friendly name, then title, then username.

type CertBundle

type CertBundle struct {
	CertPEM []byte // leaf first, then any chain
	KeyPEM  []byte // PKCS#8
	Leaf    *x509.Certificate
	Hash    string // plex.direct hash ("" for self-signed without one)
	Subject string // e.g. "*.<hash>.plex.direct"
}

CertBundle is an issued (or self-signed) certificate and its key.

func GenerateSelfSigned

func GenerateSelfSigned(hosts []string, hash string, validity time.Duration) (*CertBundle, error)

GenerateSelfSigned makes a self-signed certificate for hosts (DNS names or IPs) valid for validity. It is Ether's fallback when no plex.direct certificate can be obtained. Official apps do not trust it: they fall back to plain http on the LAN (when secure connections are "preferred") and refuse it for https connections, so it mostly serves browsers and third-party clients that let users accept it.

type CertInfo

type CertInfo struct {
	Present     bool          `json:"present"`
	SelfSigned  bool          `json:"selfSigned"`
	Custom      bool          `json:"custom"` // owner-supplied files, never replaced
	Hash        string        `json:"hash,omitempty"`
	Subject     string        `json:"subject,omitempty"`
	DNSNames    []string      `json:"dnsNames,omitempty"`
	NotBefore   time.Time     `json:"notBefore,omitempty"`
	NotAfter    time.Time     `json:"notAfter,omitempty"`
	Fingerprint string        `json:"fingerprint,omitempty"`
	CertFile    string        `json:"certFile,omitempty"`
	KeyFile     string        `json:"keyFile,omitempty"`
	LastAttempt time.Time     `json:"lastAttempt,omitempty"` // last plex.tv request
	NextAttempt time.Time     `json:"nextAttempt,omitempty"`
	RetryAfter  time.Duration `json:"retryAfter,omitempty"`
	LastError   string        `json:"lastError,omitempty"`
}

CertInfo describes the certificate Ether serves.

type CertManager

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

CertManager obtains, stores and renews the server's TLS certificate: a plex.direct certificate when plex.tv issues one, otherwise a self-signed fallback. It serves it through GetCertificate so renewals apply without restarting listeners.

func NewCertManager

func NewCertManager(o CertManagerOptions) (*CertManager, error)

NewCertManager returns a manager and loads any certificate on disk.

func (*CertManager) Ensure

func (m *CertManager) Ensure(ctx context.Context, serverToken string, hosts []string, force bool) (CertInfo, error)

Ensure makes sure a usable certificate is installed. With a server token and a missing, self-signed or expiring certificate it requests a plex.direct certificate (at most once per RetryAfter after a failure, unless force). If none can be obtained and there is no valid certificate, it installs a self-signed one for hosts. It returns the resulting info; the error reports the plex.tv failure, if any, even when a fallback is in place.

func (*CertManager) GetCertificate

func (m *CertManager) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate is a tls.Config.GetCertificate callback.

func (*CertManager) Info

func (m *CertManager) Info() CertInfo

Info returns the current certificate state.

func (*CertManager) Install

func (m *CertManager) Install(b *CertBundle) error

Install stores and activates a bundle (used by Ensure; exported so the spike can install a certificate it obtained by hand).

func (*CertManager) NeedsRenewal

func (m *CertManager) NeedsRenewal() bool

NeedsRenewal reports whether the current certificate should be replaced.

func (*CertManager) RunSharedSync

func (m *CertManager) RunSharedSync(ctx context.Context, every time.Duration)

RunSharedSync polls the shared certificate every interval (default DefaultCertSyncInterval) until ctx ends. It returns at once when the manager has no shared store.

func (*CertManager) SetEventBus

func (m *CertManager) SetEventBus(bus EventBus)

SetEventBus subscribes to certificate change announcements from other nodes (and the bus's resync notice) and announces this node's writes. Nil disconnects.

func (*CertManager) SetRenewalAllowed

func (m *CertManager) SetRenewalAllowed(fn func() bool)

SetRenewalAllowed installs the predicate deciding whether this node may request certificates from plex.tv (cluster wiring passes the leader check). Nil, the default, always allows (a standalone server).

func (*CertManager) SyncShared

func (m *CertManager) SyncShared(ctx context.Context) (bool, error)

SyncShared installs the cluster's certificate when another node stored a newer one, and seeds the shared copy from this node's plex.direct certificate when the cluster has none. It reports whether the served certificate changed.

func (*CertManager) TLSConfig

func (m *CertManager) TLSConfig() *tls.Config

TLSConfig returns a server TLS config backed by GetCertificate.

type CertManagerOptions

type CertManagerOptions struct {
	// Dir holds plexdirect.crt, plexdirect.key and cert-state.json.
	Dir string
	// Client must carry the server identity.
	Client *Client
	// CustomCertFile/CustomKeyFile, when set to files outside Dir, are an
	// owner-supplied certificate: loaded and reported, never replaced.
	CustomCertFile string
	CustomKeyFile  string
	// RenewBefore triggers renewal this long before expiry (default: a third
	// of the certificate's lifetime, at least 7 days).
	RenewBefore time.Duration
	// RetryAfter is the minimum gap between plex.tv certificate requests
	// after a failure (default 24h). plex.tv rate-limits CSR uploads per
	// device; PMS servers that retried daily ended up locked out for days.
	RetryAfter time.Duration
	// ClientErrorRetryAfter controls retries after non-429 4xx responses,
	// which usually indicate a malformed request. Default: 5 minutes.
	ClientErrorRetryAfter time.Duration
	// SelfSignedValidity for the fallback certificate (default 1 year).
	SelfSignedValidity time.Duration
	// Request overrides the download timing (tests).
	Request CertRequest
	// Shared, on a cluster node, holds the certificate for every node
	// (see certshared.go). Nil: a standalone server keeps it in Dir only.
	Shared SharedCertStore
	Logger *slog.Logger
	Now    func() time.Time
}

CertManagerOptions configures a CertManager.

type CertRequest

type CertRequest struct {
	// Key is the private key to certify; a fresh RSA-2048 key when nil
	// (RSA for compatibility with older TV clients).
	Key crypto.Signer
	// Reason is "missing" (no certificate yet) or "expiring", as PMS sends.
	Reason string
	// InvalidIn is seconds until the current certificate stops being valid
	// (negative when already expired; 0 when missing), as PMS sends.
	InvalidIn int
	// PollInterval between download attempts (default 3s) and Timeout for
	// the whole download phase (default 90s, PMS's window).
	PollInterval time.Duration
	Timeout      time.Duration
}

CertRequest tunes RequestPlexDirectCertificate.

type ClaimResult

type ClaimResult struct {
	ServerToken string
	Owner       Account
	Method      string
}

ClaimResult is the outcome of claiming the server.

type Client

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

Client talks to plex.tv with one identity. It is safe for concurrent use.

func New

func New(o Options) (*Client, error)

New returns a client.

func (*Client) AccessTokens

func (c *Client) AccessTokens(ctx context.Context, serverToken string) ([]Friend, error)

AccessTokens fetches the access tokens of every account that can use this server: GET servers.plex.tv/api/v2/server/access_tokens, called with the server token and the server identity. This is the call PMS makes ("MyPlex: updating with N access tokens").

func (*Client) AuthURL

func (c *Client) AuthURL(pin *PIN, forwardURL string) string

AuthURL is the app.plex.tv page where the owner approves pin. forwardURL, when set, is where plex.tv sends the browser afterwards (the wizard).

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the plex.tv base URL.

func (*Client) CertificateSubject

func (c *Client) CertificateSubject(ctx context.Context, serverToken string) (cn, hash string, err error)

CertificateSubject asks plex.tv for the certificate subject of this device: GET /api/v2/devices/{machineIdentifier}/certificate/subject. The subject carries the plex.direct hash ("/CN=*.<hash>.plex.direct"); the hash is assigned by plex.tv, not derived locally.

func (*Client) CheckPIN

func (c *Client) CheckPIN(ctx context.Context, id int64, code string) (*PIN, error)

CheckPIN polls a PIN: GET /api/v2/pins/{id}?code=. AuthToken is empty until the user approves it. It must be called with the same client identifier that created the PIN.

func (*Client) Claim

func (c *Client) Claim(ctx context.Context, userToken string) (*ClaimResult, error)

Claim claims this server for the user behind userToken, the way a fresh PMS install is claimed: fetch a claim token for the user, exchange it under the server identity for the server's own token, then confirm the owner. If the exchange is refused for a reason other than a bad user token, it falls back to using the owner's token directly (how PMS worked before claim tokens existed) and reports Method = ClaimOwnerToken.

c must carry the server identity.

func (*Client) ClaimToken

func (c *Client) ClaimToken(ctx context.Context, userToken string) (string, error)

ClaimToken fetches a short-lived claim token ("claim-xxxx") for the signed-in user: GET /api/claim/token.json. This is what plex.tv/claim shows and what Plex Web hands a fresh server during setup.

func (*Client) CreatePIN

func (c *Client) CreatePIN(ctx context.Context) (*PIN, error)

CreatePIN starts a sign-in: POST /api/v2/pins?strong=true. Strong PINs are long codes meant for the hosted auth page (AuthURL) rather than typing.

func (*Client) ExchangeClaim

func (c *Client) ExchangeClaim(ctx context.Context, claimToken string) (string, error)

ExchangeClaim turns a claim token into the server's own plex.tv token: POST /api/claim/exchange?token=claim-xxxx, sent with the server identity (X-Plex-Provides: server, client identifier = machine identifier). This is exactly what PMS (and the official Docker image's first-run script) do; the response is a <user> document with an <authentication-token> element.

func (*Client) FindServer

func (c *Client) FindServer(ctx context.Context, token, machineID string) (*Resource, error)

FindServer returns this server's (machineID's) entry in token's resources, or core.ErrNotFound.

func (*Client) Friends

func (c *Client) Friends(ctx context.Context, serverToken string) ([]Friend, error)

Friends returns everyone with access to this server and their tokens. It uses AccessTokens (what PMS calls) and falls back to SharedServers, then enriches entries with profile data from Users when available.

func (*Client) HomeUserToken

func (c *Client) HomeUserToken(ctx context.Context, ownerToken string, id int64, pin string) (string, error)

HomeUserToken uses the owner's authorized Home switch operation. A PIN, when configured by Plex, is still required; this never resets or bypasses it.

func (*Client) Identity

func (c *Client) Identity() Identity

Identity returns the identity c sends.

func (*Client) PublishConnections

func (c *Client) PublishConnections(ctx context.Context, serverToken string, uris []string) error

PublishConnections publishes the server's connection URIs on its plex.tv device record: PUT /devices/{machineIdentifier}?Connection[][uri]=... (one Connection[][uri] per URI; an empty list publishes a single empty value, which clears them, as PMS does when it has no addresses).

c must carry the server identity: plex.tv associates the PUT with the device whose client identifier matches the path.

func (*Client) PublishConnectionsWith

func (c *Client) PublishConnectionsWith(ctx context.Context, serverToken string, uris []string, flags *PublishFlags) error

PublishConnectionsWith publishes connection URIs plus capability flags; flags=nil sends only the URIs.

func (*Client) PubsubServers

func (c *Client) PubsubServers(ctx context.Context, serverToken string) ([]string, error)

PubsubServers lists pubsub hosts, best region first as plex.tv orders them.

func (*Client) RegisterServer

func (c *Client) RegisterServer(ctx context.Context, serverToken string, reg ServerRegistration) error

RegisterServer sends ServerRegistration as PMS does: POST {servers host}/servers.xml?auth_token=<server token> with Content-Type: application/xml and the MediaContainer/Server XML body of registrationXML. PMS sends its X-Plex-* identity headers but no X-Plex-Token header and no other query parameters. Failures remain non-fatal because clients discover connections through resources.

func (*Client) ReportConnectivity

func (c *Client) ReportConnectivity(ctx context.Context, serverToken string) (string, error)

ReportConnectivity starts Plex's asynchronous reachability check. The PMS sends PUT /api/servers/{machineIdentifier}/connectivity with its server token and a fresh asyncIdentifier immediately after servers.xml registration.

func (*Client) RequestPlexDirectCertificate

func (c *Client) RequestPlexDirectCertificate(ctx context.Context, serverToken string, req CertRequest) (*CertBundle, error)

RequestPlexDirectCertificate runs PMS's certificate flow end to end:

  1. GET /api/v2/devices/{id}/certificate/subject -> "*.<hash>.plex.direct"
  2. PUT /api/v2/devices/{id}/certificate/csr?reason=…&invalidIn=… multipart/form-data, one part "file" (no filename, Content-Type application/x-pem-file) holding the PEM CSR; 2xx
  3. GET /api/v2/devices/{id}/certificate/download (202 while pending, 200 with a PEM chain: leaf first, then the intermediate)

The request formats follow the PMS 1.43.4 executable; downloads are still parsed liberally (PEM, DER or JSON-wrapped PEM). plex.tv rate-limits step 2 (HTTP 429, code 1003) per device, so callers must not retry quickly: see CertManager.

It is deliberately self-contained so the spike can call it directly.

func (*Client) Resources

func (c *Client) Resources(ctx context.Context, token string) ([]Resource, error)

Resources lists the devices visible to token, with plex.direct https connections, relays and IPv6 included, as official apps request it.

func (*Client) SharedServers

func (c *Client) SharedServers(ctx context.Context, ownerToken string) ([]Friend, error)

SharedServers lists the owner's shares of this server with their access tokens: GET /api/servers/{machineIdentifier}/shared_servers (owner token). It is the documented fallback for AccessTokens.

func (*Client) User

func (c *Client) User(ctx context.Context, token string) (*Account, error)

User returns the account a token belongs to. A revoked or unknown token yields an error matching core.ErrUnauthorized.

func (*Client) UserServices

func (c *Client) UserServices(ctx context.Context, serverToken string) ([]Service, error)

UserServices fetches the current server-scoped service snapshot.

func (*Client) UserSubscriptions

func (c *Client) UserSubscriptions(ctx context.Context, serverToken string) ([]SharedUser, error)

UserSubscriptions reads the server-scoped subscription snapshot. Plex returns an XML users collection; the owner's subscription feature names are retained for the admin UI, while Active reflects Plex Pass entitlement.

func (*Client) Users

func (c *Client) Users(ctx context.Context, ownerToken string) ([]SharedUser, error)

Users lists the owner's friends and home users from the observed v2 API.

func (*Client) WaitPIN

func (c *Client) WaitPIN(ctx context.Context, pin *PIN, interval time.Duration) (string, error)

WaitPIN polls until the PIN is approved, expires or ctx ends.

func (*Client) WithIdentity

func (c *Client) WithIdentity(id Identity) *Client

WithIdentity returns a copy of c that sends id.

type ConfigStore

type ConfigStore interface {
	Get() config.Config
	Update(fn func(*config.Config) error) error
}

ConfigStore is the part of *config.Store the registrar uses.

type EventBus

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

EventBus is the cluster event bus as the certificate manager uses it.

type Friend

type Friend struct {
	PlexID     int64    `json:"plexId"`
	UUID       string   `json:"uuid,omitempty"`
	Username   string   `json:"username"`
	Title      string   `json:"title"`
	Email      string   `json:"email,omitempty"`
	Thumb      string   `json:"thumb,omitempty"`
	Token      string   `json:"-"`
	Owned      bool     `json:"owned"` // the owner's own access token
	Home       bool     `json:"home"`
	Restricted bool     `json:"restricted"` // managed user
	Pending    bool     `json:"pending"`    // invite not accepted yet
	AllLibs    bool     `json:"allLibraries"`
	Sections   []string `json:"sections,omitempty"` // shared section keys
}

Friend is an account with access to this server and the server access token plex.tv issued it. Official apps send that token (from their resources list's accessToken) as X-Plex-Token when they talk to the server, so the server can recognise friends without asking plex.tv.

func (Friend) DisplayName

func (f Friend) DisplayName() string

DisplayName prefers title over username.

type Identity

type Identity struct {
	Product          string
	Version          string
	ClientIdentifier string
	DeviceName       string
	Device           string
	Platform         string
	PlatformVersion  string
	// Provides is "server" for the server identity, empty for the wizard.
	Provides string
}

Identity is the set of X-Plex-* headers a client sends to plex.tv.

func AppIdentity

func AppIdentity(cfg config.Config, version string) Identity

AppIdentity is the setup wizard's identity for PIN sign-in. It uses config.Plex.ClientIdentifier so the owner's plex.tv "authorized devices" list shows an "Ether" entry distinct from the server device.

func ServerIdentity

func ServerIdentity(cfg config.Config, version string) Identity

ServerIdentity is how Ether presents itself as a server: the same headers PMS sends, with the machine identifier as client identifier, because plex.tv keys a server's device record (and clients' resources entries) on it.

type LibrarySection added in v0.2.14

type LibrarySection struct {
	Key      int    `xml:"key,attr"`
	Type     string `xml:"type,attr"`
	Title    string `xml:"title,attr"`
	UUID     string `xml:"uuid,attr"`
	Language string `xml:"language,attr,omitempty"`
}

LibrarySection describes a local library, not a sharing grant. Key and UUID must match the section exposed by the PMS HTTP API.

type Options

type Options struct {
	VersionSource func() string
	BaseURL       string // DefaultBaseURL when empty
	AuthAppURL    string // DefaultAuthAppURL when empty
	HTTPClient    *http.Client
	Identity      Identity
	Logger        *slog.Logger
	// RetryBackoff is the initial delay between retries of idempotent
	// requests (default 500ms).
	RetryBackoff time.Duration
}

Options configures a Client.

type PIN

type PIN struct {
	ID               int64  `json:"id"`
	Code             string `json:"code"`
	ClientIdentifier string `json:"clientIdentifier"`
	Trusted          bool   `json:"trusted"`
	ExpiresIn        int    `json:"expiresIn"`
	ExpiresAt        time.Time
	// AuthToken is set once the user approved the PIN.
	AuthToken string `json:"authToken"`
}

PIN is a plex.tv sign-in PIN (POST /api/v2/pins).

type PortMapper

type PortMapper interface {
	Ensure(ctx context.Context, internal, external int) (remote.MappingStatus, error)
	Status() remote.MappingStatus
	Close(ctx context.Context)
}

PortMapper keeps the remote-access port forwarded (remote.PortMapper).

type Presence

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

Presence keeps the pubsub stream open, reconnecting with backoff and rotating hosts after repeated failures.

func NewPresence

func NewPresence(o PresenceOptions) *Presence

func (*Presence) Run

func (p *Presence) Run(ctx context.Context)

Run holds the stream open until ctx ends.

func (*Presence) Status

func (p *Presence) Status() PresenceStatus

type PresenceOptions

type PresenceOptions struct {
	Client *Client
	// Credentials returns the owner's plex.tv id, this server's machine
	// identifier and server token; ok=false while unclaimed.
	Credentials func() (ownerID int64, machineID, token string, ok bool)
	OnMessage   func(PubsubMessage)
	// StreamClient performs the SSE request; defaults to one verifying TLS
	// against PubsubTLSName.
	StreamClient *http.Client
	// StreamURL builds the stream URL for a host (tests use http).
	StreamURL              func(host string, ownerID int64, machineID, token string) string
	MinBackoff, MaxBackoff time.Duration
	// IdleTimeout reconnects when the stream delivers no bytes for this long
	// (default 5m). Pubsub sends ": -1" keep-alive comments, so silence
	// means a half-open connection (NAT timeout, sleep/wake) that would
	// otherwise block the reader forever and leave the server offline.
	IdleTimeout time.Duration
}

PresenceOptions configure a Presence.

type PresenceStatus

type PresenceStatus struct {
	Connected   bool      `json:"connected"`
	Host        string    `json:"host,omitempty"`
	Since       time.Time `json:"since,omitempty"`
	LastError   string    `json:"lastError,omitempty"`
	LastMessage string    `json:"lastMessage,omitempty"`
}

PresenceStatus is reported in the registrar status.

type PublishFlags

type PublishFlags struct {
	HTTPSEnabled           bool
	HTTPSRequired          bool
	DNSRebindingProtection bool
	NATLoopbackSupported   bool
}

PublishFlags are the capability flags PMS appends to its device publish (after the Connection[][uri] list, in this order: httpsEnabled, httpsRequired, dnsRebindingProtection, natLoopbackSupported). plex.tv uses httpsEnabled to offer clients the https plex.direct form of each published http URI.

type PubsubMessage

type PubsubMessage struct {
	Command         string `xml:"command,attr"`
	AsyncIdentifier string `xml:"asyncIdentifier,attr"`
	Connectivity    string `xml:"connectivity,attr"`
	Address         string `xml:"address,attr"`
	Port            string `xml:"port,attr"`
	Raw             string `xml:"-"`
}

PubsubMessage is one event pushed to the server.

func (PubsubMessage) Reachable

func (m PubsubMessage) Reachable() bool

Reachable reports a notifyConnectivity result.

type Registrar

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

Registrar runs Ether's plex.tv presence: claim, connection publishing, friend sync, certificate renewal, port mapping and reachability.

func NewRegistrar

func NewRegistrar(o RegistrarOptions) (*Registrar, error)

NewRegistrar validates options and returns a registrar; call Start.

func (*Registrar) CheckReachability

func (r *Registrar) CheckReachability(ctx context.Context) remote.Reachability

CheckReachability probes the WAN connections and records the result.

func (*Registrar) Claim

func (r *Registrar) Claim(ctx context.Context, userToken string) (*ClaimResult, error)

Claim claims the server for the account behind userToken (from PIN sign-in), persists the server token and owner, informs UserSync and triggers publishing.

func (*Registrar) Client

func (r *Registrar) Client() *Client

Client returns the server-identity plex.tv client.

func (*Registrar) NodeURIs

func (r *Registrar) NodeURIs(ctx context.Context) []string

NodeURIs returns the URIs this node would publish for itself, refreshing its LAN/WAN addresses first when they are older than AddressInterval (the loop keeps them fresh while it runs). A clustered node heartbeats these so the leader can publish the union.

func (*Registrar) PublishNow

func (r *Registrar) PublishNow(ctx context.Context) error

PublishNow publishes the current connections to plex.tv and verifies the server appears in the resources list.

func (*Registrar) RefreshOwner

func (r *Registrar) RefreshOwner(ctx context.Context) error

RefreshOwner re-reads the owner's account (Plex Pass state) and passes it to UserSync; call at startup.

func (*Registrar) RenewCertificate

func (r *Registrar) RenewCertificate(ctx context.Context, force bool) error

RenewCertificate runs the certificate manager; force bypasses the failure back-off (admin "retry now").

func (*Registrar) RequestPublish

func (r *Registrar) RequestPublish()

RequestPublish asks the running loop to publish connections now without re-checking addresses (e.g. after cluster membership changed).

func (*Registrar) RunPortMapping

func (r *Registrar) RunPortMapping(ctx context.Context)

RunPortMapping keeps this node's port mapping (UPnP IGD or NAT-PMP) alive until ctx ends, then removes it. Used with NodeMapping, on every cluster node whether or not it leads.

func (*Registrar) SetLibrarySections added in v0.2.14

func (r *Registrar) SetLibrarySections(sections []LibrarySection)

SetLibrarySections connects account publishing to the actual PMS library identities. Call during front-end construction and whenever sections change.

func (*Registrar) SignInClient

func (r *Registrar) SignInClient() *Client

SignInClient returns a client with the setup wizard's identity for PIN sign-in (CreatePIN, AuthURL, WaitPIN).

func (*Registrar) Start

func (r *Registrar) Start(ctx context.Context) error

Start runs the background loop until ctx ends or Stop is called.

func (*Registrar) Status

func (r *Registrar) Status() Status

Status returns a snapshot for the admin dashboard.

func (*Registrar) Stop

func (r *Registrar) Stop()

Stop ends the loop and removes any port mapping.

func (*Registrar) SyncFriendsNow

func (r *Registrar) SyncFriendsNow(ctx context.Context) error

SyncFriendsNow refreshes friends and their access tokens.

func (*Registrar) Trigger

func (r *Registrar) Trigger()

Trigger asks the loop to re-check addresses and re-publish now (e.g. after a config change).

func (*Registrar) Unclaim

func (r *Registrar) Unclaim(ctx context.Context) error

Unclaim forgets the server token (sign out). The plex.tv device record stays until the owner removes it from their account.

type RegistrarOptions

type RegistrarOptions struct {
	Config ConfigStore
	// Client carries the server identity; built from Config when nil.
	Client  *Client
	BaseURL string // for the default client (tests)
	Version string // PMS version reported to plex.tv
	Users   UserSync
	WAN     WANDetector
	LAN     func() ([]net.IP, error)
	Mapper  PortMapper // nil creates a remote.PortMapper
	Reach   *remote.ReachabilityChecker
	Certs   *CertManager // nil disables certificate management

	// Intervals (defaults in parentheses).
	Tick                 time.Duration // loop granularity (15s)
	AddressInterval      time.Duration // LAN/WAN re-check (30s)
	PublishInterval      time.Duration // unconditional re-publish (1h)
	FriendSyncInterval   time.Duration // access token sync (1m, observed)
	MappingInterval      time.Duration // UPnP/NAT-PMP renew check (5m)
	CertInterval         time.Duration // certificate expiry check (1w, observed when not near expiry)
	ReachabilityInterval time.Duration // remote self-test (1h, observed)

	// PeerURIs, when set (clustered servers), returns the other live
	// nodes' connection URIs in publication order. PublishNow appends them,
	// de-duplicated, after this node's own URIs so plex.tv lists the union.
	// It is called on every publish and must not block on the network.
	PeerURIs func() []string

	// NodeMapping moves port mapping out of the publishing loop: the owner
	// runs RunPortMapping on every node (each cluster node is reached
	// through its own port forward, plex.publicPort being node-local), and
	// Stop leaves the mapping in place. Off, the loop maps the port and
	// Stop removes the mapping (a standalone server).
	NodeMapping bool

	// PublishPlexDirect also publishes the https plex.direct URIs. Off by
	// default: PMS publishes plain http URIs and plex.tv synthesises the
	// plex.direct https variants for clients asking with includeHttps=1.
	PublishPlexDirect bool

	Logger *slog.Logger
	Now    func() time.Time
}

RegistrarOptions configures a Registrar. Only Config is required; nil collaborators get production defaults or are disabled.

type Resource

type Resource struct {
	Name                   string               `json:"name"`
	Product                string               `json:"product"`
	ProductVersion         string               `json:"productVersion"`
	Platform               string               `json:"platform"`
	PlatformVersion        string               `json:"platformVersion"`
	Device                 string               `json:"device"`
	ClientIdentifier       string               `json:"clientIdentifier"`
	CreatedAt              flexTime             `json:"createdAt"`
	LastSeenAt             flexTime             `json:"lastSeenAt"`
	Provides               string               `json:"provides"`
	OwnerID                *int64               `json:"ownerId"`
	SourceTitle            string               `json:"sourceTitle"`
	PublicAddress          string               `json:"publicAddress"`
	AccessToken            string               `json:"accessToken"`
	Owned                  bool                 `json:"owned"`
	Home                   bool                 `json:"home"`
	Synced                 bool                 `json:"synced"`
	Relay                  bool                 `json:"relay"`
	Presence               bool                 `json:"presence"`
	HTTPSRequired          bool                 `json:"httpsRequired"`
	PublicAddressMatches   bool                 `json:"publicAddressMatches"`
	DNSRebindingProtection bool                 `json:"dnsRebindingProtection"`
	NatLoopbackSupported   bool                 `json:"natLoopbackSupported"`
	Connections            []ResourceConnection `json:"connections"`
}

Resource is a device in the account's resources list (GET /api/v2/resources), which is how official apps find servers.

func (Resource) LastSeen

func (r Resource) LastSeen() time.Time

LastSeen is when plex.tv last saw the resource; zero when unknown.

func (Resource) ProvidesServer

func (r Resource) ProvidesServer() bool

ProvidesServer reports whether the resource is a media server.

func (*Resource) UnmarshalJSON

func (r *Resource) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts Plex's boolean flags serialized as either JSON booleans or numeric 0/1 values.

type ResourceConnection

type ResourceConnection struct {
	Protocol string `json:"protocol"`
	Address  string `json:"address"`
	Port     int    `json:"port"`
	URI      string `json:"uri"`
	Local    bool   `json:"local"`
	Relay    bool   `json:"relay"`
	IPv6     bool   `json:"IPv6"`
}

ResourceConnection is one published way to reach a resource.

func (*ResourceConnection) UnmarshalJSON

func (c *ResourceConnection) UnmarshalJSON(b []byte) error

UnmarshalJSON handles the integer IPv6 flag emitted by current resources responses as well as boolean variants used by other Plex API versions.

type ServerRegistration

type ServerRegistration struct {
	Name           string
	Address        string // public IP; empty lets plex.tv use the request's source address
	Port           int    // public (mapped) port; 0 when not published
	PrivateAddress string // primary LAN address (not part of the PMS body)
	PrivatePort    int    // listener port (not part of the PMS body)
	Version        string
	LocalAddresses []string // LAN IPs
	// MappingState mirrors PMS's states: "mapped", "unmapped", "unknown".
	// PMS does not send it in servers.xml; it is kept for callers.
	MappingState string
	// Now stamps the "date" attribute; zero means time.Now().
	Now time.Time
	// Libraries are Directory siblings of Server in the registration container.
	// plex.tv uses these to populate Manage Library Access; publishing only
	// connection addresses makes a server discoverable but not shareable.
	Libraries []LibrarySection
}

ServerRegistration is the server info PMS sends to POST /servers.xml ("MyPlex: Sending Server Info to myPlex (user=…, ip=…, port=…)"), which is where plex.tv learns the public address/port.

type Service

type Service struct {
	PlexID     int64
	Identifier string
	Endpoint   string
	Status     string
}

Service describes a Plex service entitlement attached to an account. Credentials are intentionally omitted from the public type.

type SharedCertStore

type SharedCertStore interface {
	Get(ctx context.Context, name string) ([]byte, int64, error)
	Put(ctx context.Context, name string, data []byte) (int64, error)
	Version(ctx context.Context, name string) (int64, error)
}

SharedCertStore holds named, sealed blobs shared by the cluster (implemented by clusterconfig.Secrets). Version 0 means absent.

type SharedUser

type SharedUser struct {
	PlexID        int64
	UUID          string
	Username      string
	Title         string
	Email         string
	Thumb         string
	Home          bool
	Restricted    bool
	PlexPass      bool
	PlexPassKnown bool
	// Servers are the machine identifiers shared with the user.
	Servers []string
}

SharedUser is an entry of the owner's friends/home list (v2 server users).

type Status

type Status struct {
	Claimed           bool   `json:"claimed"`
	OwnerUsername     string `json:"ownerUsername,omitempty"`
	MachineIdentifier string `json:"machineIdentifier"`

	LANAddresses []string            `json:"lanAddresses"`
	PublicIP     string              `json:"publicIp,omitempty"`
	PublicIPErr  string              `json:"publicIpError,omitempty"`
	Connections  []remote.Connection `json:"connections"`

	PublishedURIs    []string  `json:"publishedUris"`
	LastPublish      time.Time `json:"lastPublish,omitempty"`
	LastPublishError string    `json:"lastPublishError,omitempty"`
	RegisterError    string    `json:"registerError,omitempty"` // servers.xml (non-fatal)

	// Listed reports whether plex.tv's resources list contains this server;
	// ResourceConnections is what clients will be offered.
	Listed              bool                 `json:"listed"`
	ResourceConnections []ResourceConnection `json:"resourceConnections,omitempty"`
	PublicAddress       string               `json:"publicAddress,omitempty"` // as plex.tv sees us
	PlexPass            bool                 `json:"plexPass"`                // owner subscription active
	OwnerFeatures       []string             `json:"ownerFeatures"`           // granted by plex.tv

	RemoteAccess bool                 `json:"remoteAccess"`
	Reachability *remote.Reachability `json:"reachability,omitempty"`
	UPnP         remote.MappingStatus `json:"upnp"`

	Cert      CertInfo `json:"cert"`
	CertError string   `json:"certError,omitempty"`

	// Presence is the plex.tv pubsub stream that marks the server online.
	Presence PresenceStatus `json:"presence"`
	// PlexReachable is plex.tv's last notifyConnectivity verdict for our
	// published address (nil until one arrives).
	PlexReachable   *bool     `json:"plexReachable,omitempty"`
	Friends         int       `json:"friends"`
	LastFriendSync  time.Time `json:"lastFriendSync,omitempty"`
	FriendSyncError string    `json:"friendSyncError,omitempty"`
}

Status is the plex.tv / remote-access state for the admin dashboard.

type Subscription

type Subscription struct {
	Active   bool     `json:"active"`
	Status   string   `json:"status"`
	Plan     string   `json:"plan"`
	Features []string `json:"features"`
}

Subscription is the account's Plex Pass state.

type UserSync

type UserSync interface {
	SetOwner(ctx context.Context, owner Account, serverToken string) error
	SyncFriends(ctx context.Context, friends []Friend) error
}

UserSync receives the owner and the friend list (implemented by internal/auth).

type WANDetector

type WANDetector interface {
	Detect(ctx context.Context) (net.IP, error)
}

WANDetector finds the public IP (remote.WANDetector).

Jump to

Keyboard shortcuts

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