liquiflux-xray-api

liquiflux-xray-api

Docker app from Foehammer's Repository

Overview

Liquiflux X-Ray API: shares X-Ray timelines ("who's on screen") with the Liquiflux Plex client. Sign-in is with Plex: anyone your Plex server is shared with gets in, checked live with plex.tv at every sign-in. Two ports: LAN 8089 (also the owner's status page and pack upload) and public 8090 (sign-in and timelines only; point Cloudflare Tunnel here, never at 8089). Why a separate container: this is the only internet-facing part, so it holds no Plex token, no GPU and reads timelines read-only; your Plex token stays in the scanner, which has no ports. Install liquiflux-xray-scanner too (see the README).

Liquiflux X-Ray server

Shares X-Ray timelines ("who's on screen right now") with the Liquiflux Plex client, at home and through your Cloudflare Tunnel, to anyone who signs in with a Plex account that can use your Plex server. It also scans movies Plex adds, on your NVIDIA GPU, and imports packs made with X-Ray Desktop. Built for Unraid; runs on any Docker host.

Got a big library? Scan it on your PC first. The server's scanner is made to keep up with the movies you add. A first scan of a whole library on a typical server GPU (or on the CPU) can take days to weeks; a recent gaming PC is many times faster. The recommended way to bulk process a library, unless your server has a powerful GPU, is X-Ray Desktop: a Windows app that scans your Plex movies on any NVIDIA, AMD or Intel graphics card and sends the results here as a pack. This server waits for you on first start (step 5 below), so import the pack before it starts scanning; from then on it only scans what's new.

 Liquiflux (TV, phone, PC)                         Unraid / Docker host
 ─────────────────────────   https   ┌───────────────────────────────────────────┐
  away from home ──► Cloudflare ──► cloudflared ──► :8090  xray-api (public port)  │
  at home ─────────────────────── http ──────────► :8089  xray-api (LAN port)     │
                                                     │ reads timelines (read-only)│
                                                     ▼                            │
                                    appdata/liquiflux-xray/timelines              │
                                                     ▲ writes                     │
                                     xray-scanner (no ports, GPU, Plex token) ────┘
  • Sign-in is Plex. Liquiflux already holds your Plex sign-in, so it just works: it swaps it for an X-Ray session (2 hours, renewed by itself) and never sends it anywhere else. The web page at the server's address has a "Sign in with Plex" button (the same flow Seerr uses), mainly so the owner can see the scanner's status and upload packs.
  • Who gets in: you, and everyone your Plex server is shared with right now, checked live with plex.tv at every sign-in. Nothing to maintain. (You can narrow it with XRAY_ALLOWED_USERS or XRAY_OWNER_ONLY=1, but the default needs neither.)
  • Why two containers? On purpose, for security. The API is the only part the internet can reach, and it holds nothing worth stealing: no Plex token, no GPU, timelines read-only, a tiny image with no shell. The scanner holds your Plex owner token (full control of your Plex account) and a large Python/CUDA stack, but has no ports at all. If they were one container, a bug in the web-facing code would be one step from your Plex token; split, someone who breaks in through the tunnel gets read-only cast timelines.
  • Two ports. 8090 is the public port: sign-in and timelines only. 8089 is the LAN port: it also has the owner's status page and pack upload, only answers private addresses, and refuses anything that came through Cloudflare. Point the tunnel at 8090.

What the two containers can and can't do is in SECURITY.md.

Images

Built from this repository by GitHub Actions (images.yml):

  • ghcr.io/foehammer1987/liquiflux-xray-api (static Go binary on distroless, ~10 MB)
  • ghcr.io/foehammer1987/liquiflux-xray-scanner (Python + CUDA 12 + face models, ~3 GB)

Tags: latest, the release in VERSION (e.g. 0.9.40), and sha-<commit>.

Needs

  • Unraid 6.12 or 7.x (or any Linux Docker host).
  • For GPU scanning: the Nvidia Driver plugin (driver 525 or newer). GTX 10-series (Pascal) and newer work. Without a GPU the scanner still runs, slowly, on the CPU.
  • Optional: Cloudflare Tunnel already running (for example the official cloudflare/cloudflared container). Without it, X-Ray works at home only.

Install on Unraid (templates)

  1. Nothing to prepare: Unraid creates the appdata folders, and the scanner signs in to Plex itself (step 3).

  2. Add the templates. Download them to Unraid's template folder (not templates-user, where Unraid keeps your saved settings: a copy there makes Edit open blank fields):

    mkdir -p /boot/config/plugins/dockerMan/templates/liquiflux-xray
    cd /boot/config/plugins/dockerMan/templates/liquiflux-xray
    for t in api scanner; do
      curl -fsSLo liquiflux-xray-$t.xml https://raw.githubusercontent.com/Foehammer1987/liquiflux-xray-server/main/unraid/liquiflux-xray-$t.xml
    done
    

    Then Docker tab > Add Container > Template: pick each one (under the default templates). When you Apply, Unraid saves your values in templates-user/my-<name>.xml, and Edit opens those. Downloading the templates again later only adds new fields; it never touches your settings.

  3. liquiflux-xray-scanner: set Plex URL (e.g. 192.168.1.10:32400; http:// is optional), Movies (your movies share; Movies 2 and Movies 3 for more folders) and Path map (<the path Plex uses>=/media/movies; check a movie in Plex: Get Info > View XML > Part file="..."). Apply. The first pull is about 3 GB. Link it to Plex: open the scanner's log (Docker tab > its icon > Logs). It shows go to https://plex.tv/link ... and enter ABCD. Open plex.tv/link signed in as the owner of the Plex server and enter the code. The scanner checks the account owns this server, then saves its token in appdata/liquiflux-xray/config/plex_token, readable only by the scanner's user. The token never goes into the template, the container's settings, or the log. (To link again later, delete that file and restart the scanner.) Its log then says GPU: CUDA (<your GPU>). If it says CPU, see Troubleshooting. No NVIDIA GPU: remove --runtime=nvidia from Extra Parameters.

  4. liquiflux-xray-api: set Plex URL. Leave Allowed users empty. Apply.

  5. Open http://<unraid-ip>:8089 (also the scanner's WebUI link) and sign in with Plex as the owner. The scanner is waiting for you: it scans nothing until you either upload a pack (Import a pack; the films the pack doesn't cover are then scanned) or press Start scanning. For a big library, make that pack with X-Ray Desktop on your PC first (see the note at the top); it can also Send it straight to this address. Scan settings there set films at once, decode threads, films per round, how often to look for new films, and the hours scans may run; Pause scanning stops after the films in progress.

  6. In Liquiflux: Settings > Discovery > X-Ray Server = http://<unraid-ip>:8089 and Save. It should say "Saved, signed in to X-Ray".

Both templates already run the containers as 99:100 with a read-only root filesystem, all capabilities dropped, no-new-privileges, and pid/memory/CPU limits (Extra Parameters). Keep those. The scanner's memory and CPU limits (--memory=8g --cpus=8) can be changed there.

Separate networks (recommended). Compose puts each container on its own network. With the templates, do the same once in the terminal, then pick the network in each template's Network Type (Unraid lists custom networks there):

docker network create liquiflux-xray-api
docker network create liquiflux-xray-scanner

Optional: a secret file instead of plex.tv/link. For a token kept in a folder only root can open, with tighter folder permissions than Unraid's default, run the setup script before adding the scanner, then set the scanner's Plex token file (advanced view) to /mnt/user/appdata/liquiflux-xray/secrets/plex_token:

curl -fsSLo /tmp/xray-setup.sh https://raw.githubusercontent.com/Foehammer1987/liquiflux-xray-server/main/unraid-setup.sh
less /tmp/xray-setup.sh                      # read it first
bash /tmp/xray-setup.sh /mnt/user/appdata/liquiflux-xray

Updating or reinstalling. Unraid only pulls an image it doesn't have yet, so removing and re-adding a container keeps the old image. To get a new release: Docker tab > click the container's icon > Force update (or docker pull ghcr.io/foehammer1987/liquiflux-xray-scanner:latest and the same for -api). The scanner's first log lines name its version.

Install with Docker Compose

git clone https://github.com/Foehammer1987/liquiflux-xray-server
cd liquiflux-xray-server
cp .env.example .env
nano .env                      # PLEX_URL, XRAY_APPDATA, MOVIES_PATH and XRAY_MAP at least
bash unraid-setup.sh           # makes the folders, asks for the Plex token (hidden)
docker compose up -d           # pulls the images; add --build to build them here instead
docker compose logs -f xray-scanner

Unraid's Compose Manager plugin works too: point a stack at the cloned folder.

Timelines you already have

Packs from X-Ray Desktop do this through the upload on the web page. To copy files by hand: put timeline *.json files in appdata/liquiflux-xray/timelines, an overrides.txt in appdata/liquiflux-xray/config/, and a scanner cache/people folder in appdata/liquiflux-xray/cache/people (saves re-reading actors' photos). Then: chown -R 99:100 /mnt/user/appdata/liquiflux-xray/{timelines,config,cache}.

Cloudflare Tunnel

In the Cloudflare dashboard: Zero Trust > Networks > Tunnels > your tunnel > Public hostnames > Add:

  • Subdomain: xray (any), your domain
  • Service: HTTP, URL: <unraid-ip>:8090

If your cloudflared container is on its own Docker network, connect it to the API's network and use liquiflux-xray-api:8090, e.g. docker network connect liquiflux-xray-api cloudflared (Compose names that network liquiflux-xray_xray-api).

Then on devices away from home, X-Ray Server = https://xray.yourdomain. Liquiflux only sends a Plex sign-in over https (or plain http on your own network), so an http:// address outside your network gets nothing.

Recommended on the Cloudflare side (all free):

  • SSL/TLS > Edge Certificates: Always Use HTTPS on, Minimum TLS 1.2.
  • Security > WAF > Rate limiting rules: one rule, URI path starts with /api/v1/auth/, 20 requests per 10 seconds per IP, block for 10 seconds. (The server limits sign-ins itself too; this stops floods before they reach your house.)
  • Don't put Cloudflare Access in front of this hostname: Liquiflux can't do its login page. Plex sign-in is the gate.
  • Set XRAY_TRUSTED_PROXIES on the API to your cloudflared container's address.

Settings

Template fields and .env (Compose) use the same names.

Setting Default What
PLEX_URL (required) Your Plex server on the LAN, e.g. http://192.168.1.10:32400
XRAY_APPDATA (Compose, required) Folder for timelines, cache, status, packs and the token secret
MOVIES_PATH, XRAY_MAP /mnt/user/movies, /movies=/media/movies Read films from disk. XRAY_MAP is <Plex's path>=<container path>, one per movies folder, joined with ; (e.g. /movies=/media/movies;/movies4k=/media/movies2). Films outside every mapped folder are read through Plex (slower, shows as a Plex stream)
XRAY_ALLOWED_USERS everyone with access Leave empty. Or a comma list of Plex usernames or account ids
XRAY_OWNER_ONLY 0 1 = only the Plex server's owner
XRAY_SESSION_HOURS 2 How long a sign-in lasts (1-168). Removing someone's Plex access cuts them off within this time
XRAY_LAN_PORT, XRAY_PUBLIC_PORT 8089, 8090 Host ports
XRAY_TRUSTED_PROXIES private ranges Who may say which address a visitor came from (Cf-Connecting-Ip). Set it to cloudflared's address to be strict
XRAY_LAN_CIDRS private ranges + 100.64/10 Who the LAN port answers
XRAY_SECTIONS auto Movie libraries to scan: auto = all, or Plex section ids
XRAY_INTERVAL_MIN, XRAY_BATCH 30, 20 How often to look for new movies, and how many per round (starting values; the status page's Scan settings override them, as for the next three rows)
XRAY_HOURS empty Only scan between these hours, e.g. 01-07; empty = any time
XRAY_WORKERS, XRAY_READERS 1, 3 Scanner parallelism (1-2 workers suit a GPU shared with Plex)
XRAY_CPUS, XRAY_MEMORY 8, 8g Container limits for the scanner (templates: Extra Parameters)
XRAY_REQUIRE_GPU 0 1 = stop with an error instead of falling back to the CPU
NVIDIA_VISIBLE_DEVICES all A GPU UUID (nvidia-smi -L) to pin one GPU
XRAY_VERSION latest (Compose) image tag, e.g. 0.9.40

How new movies get scanned

Once scanning is on (see step 5), every XRAY_INTERVAL_MIN minutes the scanner lists Plex's movie libraries (one request each), picks up to XRAY_BATCH movies with no timeline that were added over 15 minutes ago (so Plex has matched them), and scans them at low priority, matching faces only against each film's own Plex cast list. Animated films are detected from their frames and get the cast list only. A film that fails is tried again after 1 h, 2 h, 4 h... up to daily. Once a day, films skipped as "nomatch" or "nophotos" are looked at again (rescanned only if Plex's cast for them changed, e.g. after you fixed the match). Packs dropped into appdata/liquiflux-xray/import/ are imported within a minute.

config/overrides.txt fixes titles the animation check gets wrong: <ratingKey> animated or <ratingKey> live, one per line.

Packs from X-Ray Desktop

X-Ray Desktop is the fast way to scan a whole library: it runs the same scanner on your PC's graphics card (NVIDIA, AMD or Intel, through DirectML) and its Send button uploads the pack to http://<server-ip>:8089 for you. Otherwise: upload on the web page (LAN, signed in as the owner), or drop the .xrpack in appdata/liquiflux-xray/import/. Results: import/done/<name>.result.json or import/failed/.... A pack must be made for this Plex server, by the same scanner version; a timeline replaces the server's only when it's newer. Name a pack *.replace.xrpack to replace regardless.

API (for apps)

POST /api/v1/auth/plex {"plexToken":"..."} -> {"token","expiresAt","user":{"id","username","owner"}}. 401 bad token, 403 no access, 429 too many tries
GET /api/v1/timeline/<ratingKey> + Authorization: Bearer <token> the timeline (no title, guid or scan stats), 404 if not scanned
GET /api/v1/me who the session is
POST /api/v1/auth/logout ends the session
LAN port, owner only: GET /api/v1/status, POST /api/v1/admin/import (raw zip body), GET /api/v1/admin/import/<id>

Troubleshooting

  • GPU: CPU (CUDA not working ...): check nvidia-smi works in the Unraid terminal, that the Nvidia Driver plugin is installed and Docker restarted after it, and that the scanner runs with --runtime=nvidia. GTX 10-series cards need this image's CUDA 12 build; never "upgrade" onnxruntime-gpu past 1.26 or cuDNN past 9.10 (they dropped Pascal).
  • Scanner keeps showing a plex.tv/link code: enter it at plex.tv/link while signed in as the Plex server's owner. Another account is refused (the log says so) and a new code appears. Codes are replaced every 14 minutes.
  • Liquiflux says "Saved, but X-Ray: ...": the message is the server's reason. "no access to this server" means that Plex account isn't shared on this server.
  • The api log warns "Plex accepts requests from this container without a valid token": Plex > Settings > Network > "List of IP addresses and networks that are allowed without auth" covers the Docker network. Then only plex.tv sign-ins work (owner and Plex-account tokens), not shared users' server tokens. Remove the Docker range from that list.
  • A shared user can't get in: Liquiflux 0.9.39 and later sign in with the plex.tv account token, which the server checks with plex.tv. An older Liquiflux (or a pasted token) only has a server token; that's checked with your Plex server directly, which needs PLEX_URL reachable from the API container. Signing in to Plex again in Liquiflux's Settings fixes it.

License

MIT, see LICENSE.

Requirements

Install liquiflux-xray-scanner as well.

Related apps

Details

Repository
ghcr.io/foehammer1987/liquiflux-xray-api:latest
Last Updated2026-11-02
First Seen2026-11-02

Runtime arguments

Web UI
http://[IP]:[PORT:8089]/
Network
bridge
Shell
sh
Privileged
false
Extra Params
--restart=unless-stopped --user 99:100 --read-only --cap-drop=ALL --security-opt=no-new-privileges:true --pids-limit=128 --memory=256m --cpus=1

Template configuration

LAN portPorttcp

Sign-in, timelines, and the owner's status page and pack upload. Answers private addresses only and refuses Cloudflare traffic. Never point a tunnel at it.

Target
8089
Default
8089
Value
8089
Public portPorttcp

Sign-in and timelines only. Point Cloudflare Tunnel (HTTP) at [unraid-ip]:this port.

Target
8090
Default
8090
Value
8090
Plex URLVariable

Your Plex server on the LAN, e.g. 192.168.1.10:32400 (http:// is added if you leave it out; use https:// if your Plex needs it).

Target
PLEX_URL
TimelinesPathro

Timelines the scanner writes (read-only here).

Target
/data/timelines
Default
/mnt/user/appdata/liquiflux-xray/timelines
Value
/mnt/user/appdata/liquiflux-xray/timelines
StatusPathro

Scanner status (read-only here).

Target
/data/status
Default
/mnt/user/appdata/liquiflux-xray/status
Value
/mnt/user/appdata/liquiflux-xray/status
ImportPathrw

Uploaded X-Ray packs wait here for the scanner.

Target
/data/import
Default
/mnt/user/appdata/liquiflux-xray/import
Value
/mnt/user/appdata/liquiflux-xray/import
Session hoursVariable

How long a sign-in lasts (1-168). Removing someone's Plex access cuts them off within this time.

Target
XRAY_SESSION_HOURS
Default
2
Value
2
Allowed usersVariable

Leave empty: everyone your Plex server is shared with. Or a comma list of Plex usernames / account ids.

Target
XRAY_ALLOWED_USERS
Owner onlyVariable

1 = only the Plex server's owner may sign in.

Target
XRAY_OWNER_ONLY
Default
0
Value
0
Plex server idVariable

Optional: your Plex machineIdentifier. Empty = asked from Plex at start.

Target
PLEX_SERVER_ID
Trusted proxiesVariable

Addresses allowed to send Cf-Connecting-Ip. Empty = private ranges. Set it to your cloudflared container's IP to be strict.

Target
XRAY_TRUSTED_PROXIES
LAN rangesVariable

Who the LAN port answers. Empty = private ranges + 100.64.0.0/10.

Target
XRAY_LAN_CIDRS