- Python 99.1%
- Shell 0.6%
- Dockerfile 0.2%
|
All checks were successful
Build Docker Images / build (push) Successful in 1m37s
|
||
|---|---|---|
| .forgejo/workflows | ||
| deploy | ||
| migrations | ||
| src/plex_middleware | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| alembic.ini | ||
| ARCHITECTURE.md | ||
| CHANGELOG.md | ||
| config.example.yml | ||
| docker-compose.ula.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| ROADMAP.md | ||
| uv.lock | ||
plex-middleware
plex-middleware is an intelligent playback middleware for Plex. Development is intentionally
incremental. The implementation covers Phase 1A through Phase 1K, shared homepage rows
through Phase 2B, and the first admin-scoped Phase 2C discovery surface: observation,
capability
evidence, media analysis, explainable dry-run decisions, guarded enforcement, version/audio/subtitle
selection, network policy, failure learning, and the minimal operational API. All playback-changing
features remain opt-in.
Current capabilities
- Reads active playback from Plex
/status/sessions. - Normalizes the user, client, item, media, selected streams, and playback decisions.
- Uses Plex notifications as an experimental refresh signal with polling as the reliable fallback.
- Emits structured JSON session lifecycle logs without Plex tokens.
- Persists clients, exact playback observations, and provenance-aware capability evidence in SQLite.
- Keeps reported identity, observed evidence, and manual overrides distinct; manual overrides have highest priority.
- Normalizes every playable media-version/part pair and its video, audio, and subtitle streams into Plex-independent models.
- Preserves media and part indexes while deriving conservative codec, HDR, Dolby Vision, spatial audio, language, forced-subtitle, and SDH fields.
- Generates every part-safe video/audio/subtitle candidate, evaluates compatibility before scoring, and emits structured reason codes for every score.
- Persists dry-run
Plex chose X / middleware would choose Ycomparisons with confidence and a safepreserve_plexresult for unknown clients. - Transparently forwards the exact Universal Transcoder decision endpoint, preserving unknown request data and changing only an explicit high-confidence media/part/audio/subtitle selection.
- Scores subtitle-off, forced, preferred-language, and SDH choices while strongly penalizing any subtitle that would require video transcoding.
- Resolves LAN, WAN, and mobile policies with combined bitrate ceilings and configurable highest-quality or low-bandwidth preferences.
- Learns exact client-specific candidate failures from repeated stalls, expires their penalties, and bounds retry eligibility without forcibly restarting clients.
- Exposes
GET /health,GET /api/sessions,GET /api/clients, andGET /api/clients/{identifier}for debugging.GET /api/media/{rating_key}returns normalized media candidates without file-system paths.GET /api/decisionsandGET /api/decisions/{decision_id}expose explainable decisions.GET /api/failuresandGET /api/configexpose failure evidence and sanitized configuration. - Compares and optionally reconciles documented native Plex Home/Recommended rows from validated
YAML, with dry-run drift available at
GET /api/recommendations. - Records meaningful completed playback sessions and builds explainable shared rows for Top 10, Trending, Popular, Critically Acclaimed, Highly Rated & Unwatched, Hidden Gems, duration, decade, and Binge-Worthy policies. Generated membership is previewed before enforcement.
- Provides a read-only
/debuginspector and Prometheus-compatible/metricscounters. - Provides an admin-token-protected
/debug/suggestionspage with per-Plex-user missing-title recommendations and confirmed, idempotent Radarr/Sonarr additions. - Atomically reloads validated playback policy, scoring, client overrides, logging, network, and enforcement settings while pinning active sessions to their original configuration generation.
Observations are evidence, not universal device-model assumptions.
The decision engine does not infer unsupported capabilities from absence in observed evidence. Successful observed values are positive/open evidence and may drive a decision when every required codec and container for that exact candidate has been proven. Unobserved values remain unknown; only manual overrides and configured device-rule assumptions are treated as closed capability sets. This lets a learned client select a proven alternate stream without making a device-model-wide assumption, while a new or incompletely observed client safely preserves Plex's selection.
Experimental audio control requires both playback.enforcement_enabled: true and
audio.client_control_enabled: true. The service also verifies that Plex currently exposes the
target client as present and remotely controllable. It never changes the media item's persistent
audio-stream selection; a missing or ambiguous client simply preserves Plex behavior.
The decision proxy has two independent switches. proxy.decision_interception_enabled observes and
persists request-time decisions without changing the request. proxy.request_enforcement_enabled
permits explicit parameter changes only when global enforcement is also enabled. Keep both false
until dry-run results for your clients are trustworthy.
The operational API and /debug expose playback metadata and should be bound to a trusted network
or protected by the edge proxy. The Suggestions API additionally requires a separate admin bearer
token. No endpoint returns Plex, TMDB, Radarr, Sonarr, or admin credentials.
Local development
Python 3.13 and uv are recommended.
cp config.example.yml config.yml
export PLEX_TOKEN='your-token'
uv sync --extra dev
uv run plex-middleware
Run the quality gates:
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy
Schema migrations run automatically at startup. They can also be run explicitly:
uv run alembic upgrade head
Manual overrides are keyed by the Plex Client Identifier. Each capability may be a scalar or a list:
clients:
overrides:
C672FAC8-CC00-4EA6-9862-6D993048946A:
model: Living Room Audio Path
capabilities:
audio.codec: [eac3, aac]
audio.max_channels: 6
video.dolby_vision: false
Overrides affect the registry's effective view but never delete the original reported or observed evidence.
Capabilities used for confident dry-run decisions can be configured without device-specific code:
clients:
overrides:
your-plex-client-identifier:
capabilities:
video.codec: [hevc, h264]
video.profile: [main, main 10]
video.hdr_format: [sdr, hdr10, dolby_vision]
video.max_width: 3840
video.max_height: 2160
audio.codec: [aac, eac3, dts_hd_ma]
audio.max_channels: 8
media.container: [mkv, mp4]
Plex tokens are secrets. Use the environment or an uncommitted .env; never place a real token in
tracked configuration. The observer authenticates ordinary API requests via headers. The isolated
experimental WebSocket adapter uses the token query mechanism established by Python-PlexAPI, but
never logs its URL or exception details.
Configuration reload
The service watches config.yml by default. A complete, valid edit is loaded atomically and applies
to new sessions; active sessions retain the settings generation they started with, so current
Direct Play streams and transcodes are not reselected. Invalid or partial writes retain the
last-known-good settings and appear in structured logs plus the runtime object returned by
GET /api/config.
Recommendation changes are independent of playback session policy. A valid reload schedules their fail-isolated reconciliation immediately after the new configuration generation is published.
Plex connection settings (including TLS verification), the API listener, storage URL, upstream
timeout, and nginx proxy.force_https require a container restart. These changes are rejected
during hot reload with only the affected field names logged. Configure the watcher with:
config_reload:
enabled: true
interval_seconds: 1
Native Plex homepage rows
Phase 2A manages existing Plex recommendation hubs and can optionally compose Plex Web's separate
pinned-source Home responses. Set recommendations.enabled: true to inspect configured libraries in
dry-run mode through /api/recommendations. Only
recommendations.enforcement_enabled: true writes visibility and ordering changes to Plex.
Configured rows are ordered first in YAML order. Rows not listed remain unchanged and keep their relative order. The disabled-by-default example places Plex's built-in Recently Added hub first in each movie/TV library. Plex Web fetches pinned libraries separately, so native PMS moves alone cannot produce an absolute cross-library order. The library must be favorited/pinned in a client before its rows can appear there, and Plex clients may need a refresh after a change.
For Plex Web, home_order_sections and home_order opt into exact cross-library composition:
recommendations:
enabled: true
home_order_sections: [1, 2]
home_order:
- movie.recentlyadded
- tv.recentlyadded
- movie.topunwatched
- custom.collection.1.47917
- tv.startwatching
The first section becomes the managed response. Matching hubs from every listed section are moved
there in configured order and removed from later source responses. Prefix matching supports PMS's
suffixed hub identifiers. Before changing anything, the adapter fetches every listed source and
requires every selector to match exactly once; otherwise it returns Plex's original response. It
activates only for Home requests whose pinnedContentDirectoryID contains every configured
section, so library Recommended pages retain normal Plex behavior. These settings are hot-reloaded.
An optional non-negative per-library order gives managed rows an explicit position independent of YAML list
order. Rows with order are placed first from lowest to highest; rows without it follow in their
existing YAML order.
Generated shared homepage rows
Phase 2B creates regular, explicitly ordered Plex collections for rows that PMS does not provide
natively. Enable recommendations.generated_rows_enabled first while leaving recommendation
enforcement disabled. /api/recommendations and /debug then show every candidate title, score,
and structured reason without writing to Plex.
Writing requires all four switches:
recommendations:
enabled: true
generated_rows_enabled: true
enforcement_enabled: true
experimental_collection_creation_enabled: true
Collection creation is marked experimental because PMS documents collection membership and custom
hub endpoints, but does not publish the creation request used by Plex Web and Python-PlexAPI. The
adapter verifies creation immediately, records the resulting collection as middleware-owned, and
will never adopt or overwrite an existing title collision. Existing collection membership is
snapshotted and restored after a partial update failure. Rows with fewer than minimum_items keep
their last good collection unchanged.
Periodic reconciliation is idempotent: one recommendation-hub snapshot is reused for every generated row in a library, and Plex receives no visibility or membership write when the row is already synchronized. This avoids unnecessarily invalidating PMS's homepage hub cache. The nginx edge supports HTTP/2 for parallel homepage metadata and artwork requests and records token-safe upstream timing fields for diagnosing slow hub generation. Authenticated homepage responses are never proxy-cached because Continue Watching and other hubs are user-specific.
smart_studio rows are the exception to materialized membership. They create a native Plex smart
collection containing an OR-filter over exact Plex studio names and sort=random. PMS evaluates
that filter whenever the homepage hub is requested, so the order changes on every reload and newly
added matching titles become eligible without waiting for the middleware reconciler. The hourly
reconciler only verifies the filter definition, title, and homepage visibility. For example:
- key: disney-pixar-classics
title: Disney & Pixar Klassiker
algorithm: smart_studio
source: plex
media_type: movie
studios:
- Pixar
- Walt Disney Animation Studios
- Walt Disney Feature Animation
- Walt Disney Pictures
- Walt Disney Productions
limit: 20
minimum_items: 5
Smart-collection creation and filter updates use Plex's unpublished filter-URI contract, remain
behind experimental_collection_creation_enabled, and are verified after every write. Existing
unmanaged collections and regular generated rows are never converted.
catalog_filter rows combine normalized Plex metadata without title matching. Supported criteria
include any/excluded genres, exact studios, content ratings, countries, duration, maximum episode
or season counts, minimum rating, and owner-unwatched state. active_months hides only the hub
outside its season and keeps the last good collection intact. This supports rows such as
Kinderfilme, Kinderserien, Deutsches Kino, short episodes, one-season shows, Halloween, Christmas,
and summer holidays.
Activity-based rows begin empty and become useful after the observer has recorded real playback. Top 10 counts unique users and completions to prevent one person's repeated starts dominating the ranking. Trending measures recent activity against the preceding window; Popular uses longer-term activity. Unwatched metadata currently reflects the server-owner token's Plex view state, so those rows must not be described as personalized for shared users.
Continue Watching, Next Episode, and Recently Added remain Plex-native. Picked for You, Because You Watched, Watch Again, favorite genres, and other genuinely personalized membership are deferred to Phase 2C because a published Plex collection has one server-wide membership list.
TMDB-backed shared rows
TMDB can optionally provide the ordering for Trending, Popular, and Top Rated rows while Plex
continues to provide the playable library items and their displayed metadata. Put the read-access
token only in the uncommitted deployment .env:
TMDB_READ_ACCESS_TOKEN=
Then enable the provider and choose it on individual generated rows:
recommendations:
tmdb:
enabled: true
preferred_languages: [de-DE, en-US]
region: DE
cache_ttl_seconds: 28800
stale_cache_seconds: 604800
max_pages: 3
libraries:
- section_id: 1
generated_rows:
- key: top-10-movies-today
title: Top 10 Filme heute
algorithm: top_10
source: tmdb_trending
tmdb_time_window: day
media_type: movie
limit: 10
minimum_items: 3
Available external sources are tmdb_trending, tmdb_popular, and tmdb_top_rated. TMDB is
queried in the configured language order, so this example prefers German and falls back to
English. Native Plex collections have one server-wide title and membership list: their row title
comes from YAML, and each poster's displayed title remains the localized title in the Plex
library. Per-client row-language switching would require rewriting client hub responses and is not
part of this safe native-collection implementation.
tmdb_watch_provider resolves provider names from TMDB's current catalog for the configured
region and filters by flatrate, free, ads, rent, or buy; numeric provider IDs are never
hardcoded. tmdb_discover accepts reviewed TMDB keyword IDs for award and seasonal rows. Both use
the same persistent eight-hour cache, stale fallback, localized-title fallback, and exact TMDB-ID
matching as other external rows. Watch-provider data is supplied by JustWatch through TMDB.
- key: netflix-de
title: Jetzt bei Netflix
algorithm: popular
source: tmdb_watch_provider
tmdb_watch_provider: Netflix
tmdb_monetization_types: [flatrate]
media_type: movie
limit: 20
minimum_items: 3
Matching is fail-closed. The middleware requests Plex GUIDs and accepts only an exact TMDB ID match; equal title/year text is never enough for an enforced row. Unavailable or unmatched TMDB results are skipped. Every TMDB GET response is cached in SQLite for eight hours and survives container restarts. A bounded stale response may be reused during an outage, and a provider failure never empties the last good Plex collection. This product uses the TMDB API but is not endorsed or certified by TMDB.
Personalized missing-title suggestions
Phase 2C begins with a separate admin surface rather than rewriting Plex client hub responses.
Native Plex collections have global membership, so /debug/suggestions selects a Plex account and
uses that account's imported PMS history plus middleware-observed playback as recommendation seeds.
Titles already present in Plex are excluded. Profiles with no usable seeds fall back to localized
TMDB trending results, and every result carries structured ranking reasons.
Enable it only after setting a strong admin token. Arr credentials remain environment-only:
PLEX_MIDDLEWARE_ADMIN_TOKEN=
RADARR_API_KEY=
SONARR_API_KEY=
suggestions:
enabled: true
movie_section_id: 1
show_section_id: 2
localization:
default_locale: de-DE
fallback_locale: en-US
supported_locales: [de-DE, en-US]
radarr:
enabled: true
base_url: http://172.31.0.234:7878
quality_profile_id: 1
root_folder_path: /movies
sonarr:
enabled: true
base_url: http://172.31.0.233:8989
quality_profile_id: 1
root_folder_path: /tv
The browser retains the admin token only in session storage and sends it as a bearer token. Adding a title always requires confirmation, validates the configured root/profile, checks for an existing entry, adds it as monitored, and asks Radarr or Sonarr to search immediately. A failed discovery or Arr request cannot affect Plex playback or shared homepage reconciliation.
Streaming-provider discovery
/debug/streaming is a separate authenticated admin page for titles offered by configured German
streaming providers but missing from Plex. Netflix, Disney+, Prime Video, Apple TV+, Joyn, and WOW
are included by default. The page switches between films and series, localizes German with the
configured English fallback, excludes exact TMDB IDs already in Plex, displays Radarr/Sonarr state,
and requires explicit confirmation before every add-and-search action.
It reuses PLEX_MIDDLEWARE_ADMIN_TOKEN; TMDB and Arr credentials are never returned. Provider
display names, exact TMDB names, and monetization types are configured under
suggestions.streaming_providers. Joyn defaults to free plus ads; subscription providers use
flatrate.
Client playback rules
Ordered client rules apply policy by Plex client identifier, device, model, platform, or product. Matchers are case-insensitive globs, all populated match fields must match, and the first enabled rule wins. For example, the shipped configuration prefers an existing compatible 1080p version for Plex Web:
clients:
observed_failure_threshold: 3
rules:
- name: plex-web-1080p
match:
product: [Plex Web]
preferred_video_height: 1080
override_explicit_media_selection: true
capabilities:
video.codec: [hevc, h264]
video.profile: [main, high]
video.hdr_format: [sdr]
video.max_width: 1920
video.max_height: 1080
audio.codec: [aac]
audio.max_channels: 2
media.container: [mp4]
subtitle.codec: [srt]
The rule is a configurable score contribution with reason code
CLIENT_RULE_PREFERRED_RESOLUTION; Direct Play compatibility still dominates an incompatible
version. Rule capability assumptions close otherwise unknown device-class capability sets so a
decision can be confident; identifier-specific manual overrides retain higher priority. A container
not listed by the rule can still Direct Stream when its video/audio codecs are supported.
A non-negative mediaIndex in a Plex decision request represents a concrete client-selected media
version; a missing value or -1 delegates selection to the server. Explicit selections are
authoritative by default, while audio and subtitle policy may still choose compatible streams
inside that version. A client rule can set override_explicit_media_selection: true when forced
automatic version policy is intentionally more important than the client's concrete choice. The
shipped Plex Web rule opts in because its purpose is to enforce 1080p; compatibility rules for
clients such as Samsung may opt in for the same reason. Do not enable this flag for Apple TV,
VIDAA, or other clients where Play Version must remain authoritative.
Rules apply to new sessions after a configuration reload. Request-time enforcement also requires
the client to have a known registry identity, the normal playback/proxy enforcement safety
switches, and an intercepted request. Each stored decision reports source: dry_run or
source: interceptor; only the latter can modify a request. Unknown clients preserve Plex behavior.
Successful observations remain open positive evidence. After observed_failure_threshold
failure-only observations, only that exact value becomes unsupported; a failed 7.1/8-channel path
does not imply that 5.1 or the codec generally cannot work. Manual overrides always win.
Docker
cp config.example.yml config.yml
cp .env.example .env
# Edit Plex paths, token, LAN address, and hostname in .env.
docker compose -f docker-compose.yml -f docker-compose.ula.yml up --build -d
The production topology uses three containers but only two project images: nginx, the Python
application, and the upstream LinuxServer Plex image. Plex and the application join nginx's network
namespace. nginx alone owns the LAN macvlan/ipvlan address (for example 172.31.0.5), making it the
single ingress path while Plex remains reachable internally at 127.0.0.1:32400.
Inbound port 32400 accepts both Plex HTTP and HTTPS. A narrowly scoped NAT rule redirects only
packets arriving on the configured LAN interface and address to nginx. nginx detects TLS without
decrypting at the stream layer, preserves the socket source through PROXY protocol, then terminates
TLS using the machine-specific certificate extracted from Plex's cert-v2.p12. This allows Plex's
normal https://<address>.<machine-id>.plex.direct:32400 LAN discovery URL to pass through the
middleware without split DNS. Plain HTTP follows the same path. Plex cannot be reached directly on
the LAN address because the ingress redirect runs before its listening socket.
nginx routes the Universal Transcoder decision endpoint, the control portion of matching start.*
requests, and session-correlated direct-file requests through Python. A compatibility-enabled client
that keeps its stale pre-decision URI can be handed to the selected part using an nginx internal
redirect. Ordinary requests and all media bytes still go directly from Plex through nginx; Python
never streams the file. Middleware 5xx responses fail open to Plex, and a Plex 4xx after a
rewritten decision is retried once with the original request.
A mismatching direct-file request is treated as authoritative client intent by default. Serving
byte ranges from a different media file can invalidate the player's offsets and cause permanent
buffering. The old stale-part replacement is therefore available only through a matching client
rule with direct_part_rewrite: true, for clients proven to ignore an enforced Media Decision.
This byte-path compatibility option is separate from
override_explicit_media_selection; the former repairs a stale direct-file URI after a selected
decision, while the latter authorizes overriding a concrete client media index.
The nginx container also prepares the shared ${PLEX_TRANSCODE_PATH} with the configured Plex
PUID/PGID before Plex starts. This prevents remux and transcode failures caused by an unwritable
/transcode/temp directory without adding another container.
nginx sends the standard forwarding headers:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Plex does not consistently use those headers for Dashboard addresses. In the shared namespace,
nginx therefore also transparently binds its loopback PMS connection to $remote_addr; a dedicated
source-port policy route returns Plex's response through that socket. Direct LAN and port-forwarded
sessions consequently reach PMS with the actual TCP client address. The middleware's decision
requests use a loopback-only nginx gateway carrying a sanitized original-address header, so
rewritten requests behave identically. This requires only NET_ADMIN/NET_RAW on nginx and does
not add a relay or route-agent container.
If a known NetBird or other reverse proxy is in front, list only its source CIDR in
PLEX_TRUSTED_PROXY_CIDRS; nginx will then recover the last untrusted forwarded address. Never use
an unrestricted trusted range.
Audio selection on Universal Transcoder requests remains an isolated experimental adapter behavior:
PMS documents mediaIndex and partIndex there, but documents audio selection only on the
user-scoped part-selection endpoint. The middleware supplies the selected audioStreamID together
with the documented DirectPlayStreamSelection profile setting on both decision and start requests.
PMS may still ignore this for a client/version; playback then safely continues with Plex's stream.
The middleware compares the decision response with its requested audio ID and emits
PLEX_AUDIO_SELECTION_IGNORED when PMS declined it. It intentionally does not fall back to Plex's
user-scoped part-selection mutation by default. The explicit
proxy.temporary_audio_selection_enabled mode serializes changes per playback user and part,
selects the stream only while PMS handles decision/start, and restores the previous selection in a
finally block. Failures preserve Plex's normal behavior. This mode is experimental because a
process or host crash during the short mutation window can prevent restoration. Structured logs
expose only the selected indexes and stream IDs for verification.
Samsung and VIDAA clients may omit audioStreamID after a manual audio change. For these requests,
the middleware resolves the selected stream with the playback user's token before scoring. The
exact stream is retained on the same version; a forced version change retains the language and
selects the best matching track in the destination version. Tokens are never logged, and lookup
failure leaves normal Plex scoring and fail-open behavior intact.
Set PLEX_PROXY_LAN_IP to the former Plex LAN address and let Plex rediscover the address from the
shared namespace. The normal machine-specific plex.direct URL then remains valid. Add the public
or explicit hostname on port 8443 under Plex Settings > Server > Network > Custom server access
URLs when that path is also required. Verify nginx logs contain a
decision request with route=middleware_decision, followed by a start.* request with
route=plex_start_stream. Keep proxy.force_https: false while testing.
TLS certificate selection for the explicit hostname is automatic and persistent inside nginx:
certificates/manual/fullchain.pempluscertificates/manual/privkey.pem, when both are valid forPLEX_PROXY_HOSTNAME.- A Certbot DNS-01 lineage obtained through environment-configured hooks or a custom authenticator plugin when ACME is enabled and succeeds.
- A generated self-signed certificate for initial/test deployments.
The self-signed fallback keeps the explicit hostname available but is not automatically trusted by clients. The separate Plex-managed certificate is extracted locally from the mounted Plex configuration and is never logged. See deploy/README.md for migration, certificate sources, DNS-01 plugins, checks, and rollback.
Connection security status
The debug page includes traffic-light security cards for playback connections observed during the last 24 hours. A green client card means nginx negotiated HTTPS and includes the observed TLS version and cipher. A red card means that client reached nginx over HTTP. The middleware trusts these fields only when nginx delivered them from a configured trusted ingress address; clients cannot assert their own secure status.
The two internal hops are reported separately. The compatible defaults use
http://127.0.0.1:39004 from the application to nginx and HTTP from nginx to Plex; both are
confined to the shared network namespace, but neither is encrypted. After Plex has created its
machine certificate, enable verified TLS on both hops in .env:
PLEX_MIDDLEWARE_PLEX_BASE_URL=https://127-0-0-1.<CertificateUUID>.plex.direct:39005
PLEX_SECURE_PMS_UPSTREAM=true
Replace <CertificateUUID> with Plex's value. This Plex-managed hostname resolves naturally to
127.0.0.1, so it reaches the loopback-only nginx TLS gateway without split DNS or a Docker host
mapping. The gateway presents Plex's machine certificate. nginx derives the same hostname from Plex
metadata and verifies Plex's certificate on the second hop. Do not set plex.verify_tls: false;
that encrypts traffic without authenticating either endpoint. Enabling secure PMS upstream is
fail-closed if the Plex certificate identity is unavailable.
Open the TLS-protected status page at
https://<PLEX_PROXY_HOSTNAME>:18082/debug in the Unraid/macvlan deployment, or at
https://<host>:<MIDDLEWARE_HTTPS_PORT>/debug with ordinary Compose publishing. Restrict both
debug ports to a trusted LAN/VPN or firewall; the status API is read-only but exposes operational
metadata. Plex recommends leaving Secure connections at Preferred for compatibility,
because Required can exclude unsupported apps. See Plex's
Network settings and
secure connections guide.