From 8dcdd456606d296fb0d645adcf1fb3ab666b8e51 Mon Sep 17 00:00:00 2001 From: jochens Date: Wed, 30 Sep 2026 11:57:15 +0200 Subject: [PATCH] plex: its state is placed, not written over ace's disk The manifest named /services/plex/{config,transcode} with owner and mode. On ace /services/plex is a link to /mnt/plex, plex's 133 GB of state, so a take would have chmod'ed 0700 the top of the one directory the operator ruled must never be re-moded or re-owned. The directories are now pathless (config, data, transcode), placed by the mesh; on an adopted machine they must be placed where the data is (hq 153) before plex is ever taken. - The container sees exactly the paths ace's plex sees today: /config, /data (HAL mounts it; the catalogue did not), /transcode and all eight libraries, including sport-games, live-shows and formula-1. A library whose mount disappears is emptied by Plex's automatic trash emptying, taking its watch state with it. - Libraries are mounted read-only, as the accesses already said. - Owner 1000:1000 and mode 0755: plex runs as uid 1000 (the image reads PLEX_UID, not the PUID HAL passes), pms-docker leaves its dirs 0755, and ace's /mnt/plex is 1000:1000 0755 - so placing adopted data is a no-op. - Image pinned to what ace runs, 1.43.4.10903; the old pin was 1.43.3. - ADVERTISE_IP comes from the route's own public name through an env-file (${bound:route:name}); it depends on mesh-controller #149, and without it the declaration is refused, not applied. - The server is routed (label plex) and declares its four GDM discovery ports, which LAN players use. - No token secret: a minted one is not a Plex token and the sidecar preferred it. The sidecar reads PlexOnlineToken from Preferences.xml through its read-only config mount, and dials ${port:32400}. Verified: catalogue tests with MESH_CATALOGUE (parse, mounts); a scratch resolution with ace's assignment on the #149 controller renders ADVERTISE_IP=https://plex.zurag.be/, both route names and 32400/tcp + GDM/udp open to anywhere; on main it is refused naming "name". A throwaway pms-docker at the pinned digest on empty dirs answered /identity, wrote customConnections from the env-file and ran as 1000; client.ts typechecks strict and found the token in a Preferences.xml. --- modules/plex/client.ts | 6 ++- modules/plex/module.json | 111 ++++++++++++++++++++++++++++++++------- 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/modules/plex/client.ts b/modules/plex/client.ts index 5aa7e6f..992c14f 100644 --- a/modules/plex/client.ts +++ b/modules/plex/client.ts @@ -58,8 +58,10 @@ export class PlexClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): PlexClient { const url = env.MESH_PLEX_URL ?? `http://127.0.0.1:${env.PLEX_PORT ?? "32400"}`; const dataDir = env.MESH_PLEX_DATA_DIR ?? "/var/lib/plex"; - // The operator-provided token is an own-secret the mesh mounts at MESH_PLEX_TOKEN_FILE (delivered - // by `secret accept`); prefer it, fall back to a bare env var, then to discovery from the data dir. + // The manifest sets neither MESH_PLEX_TOKEN_FILE nor MESH_PLEX_TOKEN: the server already keeps its + // token in Preferences.xml, read here through the manifest's read-only mount of the config dir. + // A token the mesh minted would be one plex.tv never issued, and preferring it would break every + // call, so the module declares no token secret. The file and env overrides stay for hand runs. const token = readSecret(env.MESH_PLEX_TOKEN_FILE) ?? env.MESH_PLEX_TOKEN ?? PlexClient.detectToken(dataDir); if (!token) throw new Error("no Plex token — set MESH_PLEX_TOKEN or make the data dir readable"); return new PlexClient(url, token); diff --git a/modules/plex/module.json b/modules/plex/module.json index 426a70f..71e54a5 100644 --- a/modules/plex/module.json +++ b/modules/plex/module.json @@ -13,8 +13,7 @@ "*.download.completed" ], "own-secrets": { - "broker": "/var/lib/mesh/plex/broker", - "token": "/var/lib/mesh/plex/token" + "broker": "/var/lib/mesh/plex/broker" }, "listens": [ { @@ -22,7 +21,35 @@ "port": 32400, "protocol": "tcp", "from": "mesh", - "why": "streaming and the app; reaching it from outside is a route grant later" + "why": "the server itself: the apps, streaming and the web player, direct and through its route" + }, + { + "name": "gdm-1", + "port": 32410, + "protocol": "udp", + "from": "mesh", + "why": "G'Day Mate discovery: players on the same network find the server without signing in" + }, + { + "name": "gdm-2", + "port": 32412, + "protocol": "udp", + "from": "mesh", + "why": "G'Day Mate discovery" + }, + { + "name": "gdm-3", + "port": 32413, + "protocol": "udp", + "from": "mesh", + "why": "G'Day Mate discovery" + }, + { + "name": "gdm-4", + "port": 32414, + "protocol": "udp", + "from": "mesh", + "why": "G'Day Mate discovery" } ], "accesses": [ @@ -45,6 +72,18 @@ { "path": "/services/media/audiobooks", "mode": "read" + }, + { + "path": "/services/media/sport-games", + "mode": "read" + }, + { + "path": "/services/media/live-shows", + "mode": "read" + }, + { + "path": "/services/media/formula-1", + "mode": "read" } ], "resources": [ @@ -54,39 +93,63 @@ "path": "/var/lib/mesh/plex", "mode": "0700" }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": "." + }, { "id": "config", "type": "directory", - "path": "/services/plex/config", - "mode": "0700", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "data", + "type": "directory", + "mode": "0755", "owner": "1000:1000" }, { "id": "transcode", "type": "directory", - "path": "/services/plex/transcode", - "mode": "0700", + "mode": "0755", "owner": "1000:1000" }, + { + "id": "server-env", + "type": "file", + "path": "${dir:state}/server.env", + "mode": "0600", + "content": "ADVERTISE_IP=https://${bound:route:name}/\n" + }, { "id": "server", "type": "container", "name": "plex", - "image": "plexinc/pms-docker@sha256:83a425ae9e133b1cb2cc3b809556e01c61cd8ff65c582e41b4374bc2210bac9e", + "image": "plexinc/pms-docker@sha256:e0ab27395614a8e1a4fdf84c6bc60ac664915cfdde70c52d030c7728a1c48e14", "network": "host", "env": { "PLEX_UID": "1000", "PLEX_GID": "1000", "TZ": "Etc/UTC" }, + "env-file": [ + "${dir:state}/server.env" + ], "volumes": [ - "/services/plex/config:/config", - "/services/plex/transcode:/transcode", - "/services/media/movies:/movies", - "/services/media/series:/series", - "/services/media/anime:/anime", - "/services/media/music:/music", - "/services/media/audiobooks:/audiobooks" + "${dir:config}:/config", + "${dir:data}:/data", + "${dir:transcode}:/transcode", + "/services/media/movies:/movies:ro", + "/services/media/series:/series:ro", + "/services/media/anime:/anime:ro", + "/services/media/music:/music:ro", + "/services/media/audiobooks:/audiobooks:ro", + "/services/media/sport-games:/sport-games:ro", + "/services/media/live-shows:/live-shows:ro", + "/services/media/formula-1:/formula-1:ro" ] }, { @@ -96,18 +159,28 @@ "network": "host", "volumes": [ "/var/lib/mesh/plex/broker:/run/secrets/broker:ro", - "/var/lib/mesh/plex/token:/run/secrets/token:ro", - "/services/plex/config:/var/lib/plex/config:ro" + "${dir:config}:/var/lib/plex/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_PLEX_URL": "http://127.0.0.1:32400", - "MESH_PLEX_TOKEN_FILE": "/run/secrets/token", + "MESH_PLEX_URL": "http://127.0.0.1:${port:32400}", "MESH_PLEX_DATA_DIR": "/var/lib/plex" }, "artifact": "runtime" } ], + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "plex", + "endpoint": "stream" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, "build": { "on": [ {