users

package
v1.5.8 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: GPL-2.0, GPL-3.0 Imports: 7 Imported by: 0

Documentation

Overview

Package users owns the per-user state, token storage, and per-user Plex client cache. It is the home of the userManager subsystem that previously lived in main.go and the cross-module typed user-id.

Inviolate contracts preserved (see refactor-agent-guide.md):

  • The on-disk cache.json schema is untouched. The Manager reads and writes tokens through the api.Cache interface (backed by internal/cache), never by mutating cache.Data directly.
  • WARN/ERROR slog keys for token refresh ("failed to refresh shared user tokens", "shared user tokens refreshed") are byte-for-byte identical to the pre-extraction log lines.
  • Initial-refresh retry semantics (5 attempts, 5s base, 2× backoff, 60s cap, short-circuit on cached users, context-cancel aware) are preserved; the tunables live on a RefreshConfig value so tests can shrink them without reaching into package-level globals.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func PeriodicRefreshInterval

func PeriodicRefreshInterval() time.Duration

PeriodicRefreshInterval returns the background refresh cadence. The composition root uses this to log the interval at startup.

Types

type ID

type ID string

ID is the typed user identifier (runtime-types-p2). Plex user IDs are numeric strings, but they are routinely treated as opaque keys — the typed wrapper keeps them from being conflated with other string keys (ratingKey, tokens, session keys) inside this package while still round-tripping through APIs that expect strings.

The Manager's public methods accept plain strings (rather than ID) so *Manager naturally satisfies api.UserLookup without a wrapper; the typed ID remains available for internal map keys and for callers that want stricter typing at their own boundaries.

func (ID) String

func (i ID) String() string

String returns the ID as a plain string for APIs that accept strings (HTTP query params, slog values, cache keys).

type Info

type Info struct {
	ID    ID
	Name  string
	Token string
}

Info is the per-user record: the typed ID, display name, and Plex access token. Tokens are secret; callers must not log Token values.

type Manager

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

Manager owns the shared-user map, the per-user HTTP client cache, and the admin user identity. All fields are guarded by mu; the manager is safe for concurrent use.

func NewManager

func NewManager(c api.Cache) *Manager

NewManager returns a Manager with empty shared-user and client maps. The Init method (called by the composition root after the admin user is resolved) seeds admin identity and base URL.

func (*Manager) Admin

func (m *Manager) Admin() api.UserInfo

Admin returns the admin UserInfo. Primarily for tests that need to assert the manager was initialized with the expected admin identity.

func (*Manager) All

func (m *Manager) All() []api.UserInfo

All returns the admin plus all shared users as api.UserInfo values. Every returned entry (admin and shared alike) has an empty Token field; All() never threads tokens through this slice. Callers that need an HTTP client for any user must use ClientForUser (which falls back to the admin client for the admin ID and looks up a shared user's token internally) rather than reading UserInfo.Token. Keeping tokens out narrows the in-memory surface that holds them.

The return type is api.UserInfo (not internal Info) so *Manager satisfies api.UserLookup and consumers (sync, scheduler) can depend on the api interface without pulling in internal/users.

func (*Manager) ClientForUser

func (m *Manager) ClientForUser(userID string, adminClient *plex.Client) *plex.Client

ClientForUser returns a *plex.Client using the given user's token. Caches clients to avoid creating new HTTP connection pools on every call. Returns the admin client only when the userID matches admin. Returns nil (fail CLOSED) whenever no per-user identity is available: either the user is unknown/departed (absent from the shared-user map, or holding an empty token) or a per-user client cannot be constructed (the CA-cert file changed/corrupted mid-run). In every nil case the caller must skip the operation rather than write under the admin token — a per-user stream PUT is per-user-scoped on the server, so executing it under the admin token corrupts the ADMIN's own stream selection and still does not apply the intended user's preference.

userID is accepted as a plain string so *Manager satisfies api.UserLookup; convert to ID internally for map keys.

func (*Manager) Init

func (m *Manager) Init(admin *plex.User, baseURL *url.URL, caCertPath string)

Init seeds the manager with the admin user and base URL. Safe to call multiple times; existing shared-user state is preserved so a re-init (e.g., after a token refresh during startup) does not clobber in-flight data.

func (*Manager) InitialRefreshWithRetry

func (m *Manager) InitialRefreshWithRetry(ctx context.Context, adminClient *plex.Client, machineID string, cfg RefreshConfig)

InitialRefreshWithRetry runs the initial plex.tv shared-user-token refresh with bounded exponential backoff. It exits early on any of:

  • plex.tv answered successfully, even with zero shared users: a server with no shared users legitimately returns an empty list, and retrying cannot conjure users that do not exist,
  • at least one shared user is already known (e.g. cached tokens from a prior run, loaded via LoadFromCache),
  • the context is cancelled (e.g., shutdown during startup),
  • the attempt budget is exhausted.

Failures are rare but happen in practice: plex.tv auth has had multi-minute outages, and local Plex can be up while plex.tv is unreachable. Without retry, a fresh install during such an outage would leave the shared-users map empty for up to PeriodicRefreshInterval() (12h). The retry bounds the degraded window to tens of seconds in the common case.

Cached tokens from a previous run short-circuit this entirely: if LoadFromCache already populated the shared map, the first attempt sees SharedCount > 0 and returns immediately even if plex.tv itself failed.

func (*Manager) LoadFromCache

func (m *Manager) LoadFromCache()

LoadFromCache seeds the shared-user map from cached tokens. The cached entries use synthetic display names ("user-{id}") until a successful plex.tv refresh supplies the real username. Called at startup so the app can operate on per-user tokens when plex.tv is unreachable.

func (*Manager) Name

func (m *Manager) Name(userID string) string

Name returns the display name for a userID. Unknown users get an "unknown-{id}" placeholder so log lines remain parseable.

func (*Manager) RefreshLoop

func (m *Manager) RefreshLoop(ctx context.Context, adminClient *plex.Client, machineID string)

RefreshLoop periodically refreshes shared user tokens from plex.tv on the PeriodicRefreshInterval cadence. Exits on context cancellation. The initial synchronous refresh is not the responsibility of this loop — run InitialRefreshWithRetry before starting the loop.

func (*Manager) RefreshTokens

func (m *Manager) RefreshTokens(ctx context.Context, adminClient *plex.Client, machineID string) error

RefreshTokens fetches shared user tokens from plex.tv and updates the cache. The plex.tv response is the source of truth: shared users absent from the response are pruned from the manager's shared map, the per-user client cache, and the cache's user-tokens map so revoked tokens stop being used for subsequent operations. A transient plex.tv failure short-circuits above the state rebuild; existing state is left untouched. LanguageProfiles are kept untouched — a re-shared user recovers their learned audio→subtitle mappings on return.

func (*Manager) SharedCount

func (m *Manager) SharedCount() int

SharedCount returns the number of shared (non-admin) users currently known. Used by InitialRefreshWithRetry to detect whether a refresh attempt populated any users, independent of whether the plex.tv API call itself succeeded or silently returned an empty shared-servers list.

type RefreshConfig

type RefreshConfig struct {
	MaxAttempts int
	BaseDelay   time.Duration
	MaxDelay    time.Duration
}

RefreshConfig bundles retry tunables for the initial token-refresh loop. The zero value is not useful; production code uses DefaultRefreshConfig. Tests construct a Config with shrunk durations to exercise the retry path without sleeping in real time.

func DefaultRefreshConfig

func DefaultRefreshConfig() RefreshConfig

DefaultRefreshConfig returns the production retry parameters: 5 attempts, 5s base, 2× backoff, 60s cap. Worst-case additional startup delay ≈ 75s before proceeding with an empty shared-users map; zero-cost in the good case because the first attempt exits early.

Jump to

Keyboard shortcuts

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