diff --git a/modules/nzbget/client.ts b/modules/nzbget/client.ts index f4d82f4..0708793 100644 --- a/modules/nzbget/client.ts +++ b/modules/nzbget/client.ts @@ -48,6 +48,20 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** One key of nzbget's own nzbget.conf, from the config directory the mesh mounts read-only + * (MESH_NZBGET_CONFIG_DIR). The software's file is the truth about who may log in, so the tools + * ask it rather than a setting that could disagree. Absent, unreadable or unset yields undefined. */ +function confValue(dir: string | undefined, key: string): string | undefined { + if (!dir) return undefined; + try { + const line = readFileSync(`${dir.replace(/\/$/, "")}/nzbget.conf`, "utf8") + .split("\n") + .find((l) => l.startsWith(`${key}=`)); + const value = line?.slice(key.length + 1).trim(); + return value ? value : undefined; + } catch { return undefined; } +} + /** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`); * absent or unreadable yields undefined so callers fall back rather than crash. */ function readSecret(file?: string): string | undefined { @@ -69,7 +83,9 @@ export class NzbgetClient { * Build from the module's resolved environment. URL and password are read from MESH_NZBGET_URL * and MESH_NZBGET_PASSWORD; both must be present — an unconfigured NZBGet throws rather than * pretend to be reachable, so the tools/events simply do not load (the harness treats the throw - * as "exposes nothing"). The control username defaults to "nzbget", NZBGet's own default. + * as "exposes nothing"). The password file is the same own-secret the server container is started + * with (FILE__NZBGET_PASS), so the two cannot disagree. The control username is read from + * nzbget.conf, falling back to "nzbget", NZBGet's own default. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): NzbgetClient { const cfg = meshConfig(env.MESH_NZBGET_CONFIG_FILE); @@ -78,7 +94,9 @@ export class NzbgetClient { if (!url || !password) { throw new Error("NZBGet not configured — set MESH_NZBGET_URL and MESH_NZBGET_PASSWORD"); } - const user = cfg.user ?? env.MESH_NZBGET_USER ?? "nzbget"; + // The control username: a setting or the environment if one says so, else whatever + // nzbget.conf holds (an adopted machine keeps its own, e.g. not "nzbget"), else NZBGet's default. + const user = cfg.user ?? env.MESH_NZBGET_USER ?? confValue(env.MESH_NZBGET_CONFIG_DIR, "ControlUsername") ?? "nzbget"; return new NzbgetClient(url, user, password); } diff --git a/modules/nzbget/module.json b/modules/nzbget/module.json index ce3c33d..3fd5d0c 100644 --- a/modules/nzbget/module.json +++ b/modules/nzbget/module.json @@ -19,7 +19,7 @@ "port": 6789, "protocol": "tcp", "from": "mesh", - "why": "the download client's pages" + "why": "the download client's pages, and the JSON-RPC API its consumers and its own tools call" } ], "accesses": [ @@ -35,10 +35,15 @@ "path": "/var/lib/mesh/nzbget", "mode": "0700" }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": "." + }, { "id": "config", "type": "directory", - "path": "/services/nzbget/config", "mode": "0700", "owner": "1000:1000" }, @@ -46,24 +51,29 @@ "id": "server", "type": "container", "name": "nzbget", - "image": "lscr.io/linuxserver/nzbget@sha256:5f3d3fa71029004156eff2cbf4ef4455ce4ce59517cf13fa7d1d7c8a4cd2c8a4", + "image": "lscr.io/linuxserver/nzbget@sha256:ec3ef0ae7dc410084086a7fdd447deda4747531154aa1cc5c7c48ac60794e277", "env": { "PUID": "1000", "PGID": "1000", - "TZ": "Etc/UTC" + "TZ": "Etc/UTC", + "FILE__NZBGET_PASS": "/run/secrets/password" }, "ports": [ "6789" ], "volumes": [ - "/services/nzbget/config:/config", - "/services/media/downloads:/downloads" + "${dir:config}:/config", + "/services/media/downloads:/downloads", + "/var/lib/mesh/nzbget/password:/run/secrets/password:ro" + ], + "restart-on": [ + "needs-password" ] }, { "id": "runtime-config", "type": "file", - "path": "/var/lib/mesh/nzbget/config.json", + "path": "${dir:state}/config.json", "mode": "0600", "content": "{}\n", "merge": "json" @@ -76,22 +86,46 @@ "volumes": [ "/var/lib/mesh/nzbget/broker:/run/secrets/broker:ro", "/var/lib/mesh/nzbget/password:/run/secrets/password:ro", - "/var/lib/mesh/nzbget/config.json:/run/config/config.json:ro", - "/services/nzbget/config:/var/lib/nzbget/config:ro" + "${dir:state}/config.json:/run/config/config.json:ro", + "${dir:config}:/var/lib/nzbget/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_NZBGET_URL": "http://127.0.0.1:6789", + "MESH_NZBGET_URL": "http://127.0.0.1:${port:6789}", "MESH_NZBGET_PASSWORD_FILE": "/run/secrets/password", "MESH_NZBGET_CONFIG_FILE": "/run/config/config.json", "MESH_NZBGET_CONFIG_DIR": "/var/lib/nzbget/config" }, "restart-on": [ - "runtime-config" + "runtime-config", + "needs-password" ], "artifact": "runtime" } ], + "provides": [ + "nzbget-api" + ], + "serves": { + "nzbget-api": { + "scheme": "http", + "port": 6789, + "url-base": "", + "username": "nzbget" + } + }, + "requires": [ + "route" + ], + "contributes": { + "route": { + "label": "nzbget", + "endpoint": "web" + } + }, + "binds": { + "route": "${dir:state}/route.json" + }, "build": { "on": [ {