Skip to content

feat(plex): add Plex Media Server integration - #61

Open
Nort1346 wants to merge 15 commits into
mainfrom
feat/plex-support
Open

Nort1346 wants to merge 15 commits into
mainfrom
feat/plex-support

Conversation

@Nort1346

@Nort1346 Nort1346 commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds Plex Media Server as a first-class sync provider alongside Jellyfin. Lode provisions users on Plex itself, so a Plex-only install can run without Jellyfin, and a Jellyfin-only install behaves exactly as before.

User model (read this first)

  • Lode creates a Plex Home managed user on the owner's Plex account (POST /api/v2/home/users), then creates or updates a server share granting the selected library sections.
  • No email invites. Nothing is sent to any address - the managed user simply appears in the owner's Plex Home and is picked via "Switch User". This is intentional: Lode never collects a Plex email, so it cannot and does not email an invitation.
  • No per-user Plex password. Plex owns authentication for managed users; Lode stores and syncs nothing password-related for Plex (Jellyfin keeps its current behaviour, including the temporary password on force-sync).
  • Requires an account-wide NUXT_PLEX_TOKEN belonging to the server owner, and a claimed server (myPlexSigninState).

Changes

  • Sync: new PlexSyncProvider registered in the orchestrator; syncNewUser / syncUserUpdate accept per-provider settings overrides, so Plex section keys never collide with Jellyfin library IDs. Providers may declare requiresPassword: false (Plex does), which lets provisioning run without a password. Per-user surface is library access, Live TV (allowTuners) and Sync downloads (allowSync) - Plex has no per-user transcoding/remuxing/session/avatar API, so those optional hooks are not implemented and are skipped by the orchestrator.
  • Client (server/utils/clients/plex.ts): PlexPmsClient (sections, paginated library walk, GUID → TMDB/IMDB/TVDB resolution, post-download section refresh with a 60 s per-section debounce) and PlexTvClient (home users + v2 shares) kept separate, since two different hosts are involved. 10 s request timeouts, a 200-page pagination guard, and asList normalisation because Plex collapses single-element JSON lists into bare objects.
  • Admin API: GET/PUT /api/admin/plex/presets, GET /api/admin/plex/status (version + myPlexSigninState), Plex fields on user create/edit/list, and Plex in /api/admin/sync/libraries + /api/admin/sync/providers. Request bodies are validated with zod (server/utils/sync/provider-settings.ts), shared by create and edit so the same malformed value cannot pass one route and fail the other.
  • UI: Plex block in Settings > Sync Providers, per-user PlexUserFields, and the shared MediaServerStatusBanner (renamed from JellyfinStatusBanner, now used by both). The force-sync button is enabled when any media server is configured, with provider-aware wording.
  • Browse: "in library" now reflects every connected media server (OR semantics; a provider that throws counts as false for that item instead of failing the request).
  • Torrents: post-download Plex section refresh, fire-and-forget with a 60 s per-section debounce.
  • DB: additive enable_sync column on per-user sync settings - 0009_nosy_tarantula (SQLite) / 0008_cold_dark_phoenix (PostgreSQL), single ALTER each.
  • i18n: 17 new admin.plex* keys, hand-written in all six locales (no machine translation); the key-parity test covers them.
  • Ops: docker-compose.plex.yml overlay (plexinc/pms-docker, port 32400), scripts/setup-plex.mjs creating the Movies + TV Shows sections on container start (idempotent, best-effort), entrypoint wiring.
  • Setup CLI: Plex as a media-server choice (local container or external) with a token step next to Jellyfin; pipeline 16 → 17 steps.
  • Docs: README (media-server matrix + roadmap), CHANGELOG 1.1.0, docs/configuration.md (env vars + settings keys), docs/deployment.md (overlay, volumes, entrypoint, Plex setup section), docs/api/admin.md, docs/features/admin-features.md, .env.example, AGENTS.md.
  • Dev-mode noise cleanup: deduped auto-imported types (dead server/types/status.ts deleted, duplicate RankedTorrent/ParsedTitle removed, DiscordLocale consolidated, sync DB rows renamed to *Record), removed the no-op definePageMeta from the default layout (NUXT_E1007 ×12), and hardened the system-status Redis probe so a down Redis no longer emits unhandled ioredis errors.

Review fixes included

008060b - harden share sync and validate admin settings

  • Share sync now prefers updateShare (via new findShareId / getSharedServers) and only falls back to remove+create when no share exists; a failed create is logged and rethrown instead of silently leaving a user without access.
  • zod validation for all Plex payloads (presets, user create/edit), shared schemas in provider-settings.ts.
  • Request timeouts and the pagination guard in the Plex client.
  • Removed the dead inviteExistingUser path rather than wiring it up - auto-inviting arbitrary addresses would email people Lode knows nothing about.
  • Per-user settings merge in [id].put.ts no longer clobbers untouched Plex fields.
  • New admin.plexLibrariesError key in all six locales.

58b801c - align docs and harden single-item responses

  • asList normalisation in the Plex client and scripts/setup-plex.mjs (sections, Location, metadata, home users, shared servers).
  • Force sync treats a mapping on any provider as "account exists", so a Plex-only user's Lode password is not silently reset.
  • requiresPassword added to the SyncProvider contract (Plex: false).
  • users.get.ts and plex/presets.get.ts use the lenient parseStoredLibraryAccess, so one corrupt row cannot break the admin UI.
  • Removed the unused PlexPmsClient.createSection (the setup script owns provisioning).
  • Removed every remaining "invite" claim from CHANGELOG, docs/api/admin.md and the force-sync strings in all six locales; README matrix/roadmap, docs/configuration.md, docs/deployment.md and .env.example now describe the real behaviour.

Testing

  • pnpm typecheck - 0 errors, 0 warnings
  • pnpm lint - clean
  • pnpm test - 164 files / 1785 tests passing (baseline on main before this work: 150 / 1504). New/updated coverage: Plex client (GUID false-positives, section mapping, single-object collapse, pagination guard, share lookup), sync multi-provider matrix (Jellyfin-only, Plex-only, both, both with one failing, neither, password-less provisioning), admin Plex routes, admin user list leniency, force-sync password behaviour, usePlexStatus/usePlexConfigured, i18n key parity.
  • CLI (cli/): verified in CI by the check job (install + typecheck + lint + test). Not run locally - Bun is not installed in this environment, so the CI job is the source of truth for cli/**.
  • Manual verification - needs a live Plex instance (not available here):
    • managed user creation + removal on a claimed server
    • share create / update / remove round-trip, including a single-section server
    • real cost of the post-download section refresh
    • browser rendering of the new admin UI (Plex block, per-user fields, status banner)

Notes for reviewers

  • Migration deviation: the brief said "no DB migration", but the per-user Sync toggle needs persistence and Jellyfin-shaped payloads must not clobber it, hence the additive enable_sync column.
  • Plex Home limits: managed users live in the owner's Plex Home, which is capped (15 members including the owner) and whose creation rights depend on the account. A rejected plex.tv call is surfaced in that user's sync status rather than swallowed.
  • machineIdentifier: shares are keyed by the PMS machineIdentifier read from GET / and cached. Re-claiming/reinstalling the server changes it, and Lode will then create a new share - the stale one must be removed in Plex.
  • Single-item responses: Plex returns a bare object instead of a 1-element array for sections, locations, metadata, home users and shares. asList handles that at every boundary; there are regression tests for each.
  • plex.tv v2 share endpoints are community-documented, not official - deliberately isolated in PlexTvClient so a shape change is a one-file fix.
  • enableUser re-derives share settings from the server-wide Plex presets; per-user settings are intentionally ignored on re-enable (documented behaviour).
  • NUXT_PLEX_TOKEN is account-wide (warned about in .env.example), never logged, and test fixtures use obvious fake values.
  • The removed definePageMeta({ middleware: 'auth' }) in app/layouts/default.vue was a no-op (the macro is only compiled in pages), and every protected page declares its own middleware - no behaviour change.
  • The remaining dev-console lines are environmental, not code issues: the Node version warning is local, and [nostics] import.meta.hot.send() comes from nostics@1.3.0 (a dev-only transitive dependency of @nuxt/kit).

# Conflicts:
#	server/database/migrations/meta/0008_snapshot.json
#	server/database/migrations/meta/_journal.json
#	server/database/migrations/postgres/meta/0007_snapshot.json
#	server/database/migrations/postgres/meta/_journal.json
#	server/types/entities.ts
#	server/types/status.ts
#	server/utils/i18n-server.ts
#	server/utils/torrents/torrent-sync.ts
#	test/utils/torrents/torrent-sync.test.ts
- update existing plex.tv shares in place instead of remove-then-create, so a failed create can no longer leave a user without library access

- validate libraryAccess/toggles in the plex presets and admin user routes; share one Plex defaults builder between the orchestrator and the provider (also fixes per-user settings being reset to defaults on unrelated user edits)

- bound every PMS/plex.tv request with a 10s timeout and truncate error bodies before they reach the logs

- stop section pagination when the server repeats a page, ignore empty section paths, and drop the unused inviteExistingUser method

- surface Plex library load failures in the admin UI (new plexLibrariesError key in all six locales)
- tolerate Plex collapsing single-element JSON lists into bare objects (sections, locations, metadata, home users, shares) in both the client and the setup script

- keep a Plex-only user's Lode password when force-syncing (a mapping on any provider now means the external account exists) and let password-less providers provision users

- parse stored library access leniently in the user list and Plex presets so one corrupt row cannot break the admin UI

- drop the unused PMS createSection method and rewrite invite-based wording/theming in all six locales, CHANGELOG, README and docs; document the Plex env vars, settings keys and Docker setup
@Nort1346 Nort1346 changed the title feat: add Plex Media Server integration feat(plex): add Plex Media Server integration Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant