Files
tdarr-cli/tdarrctl.py
claude 7c0c55bcea
All checks were successful
release / release (push) Successful in 1m4s
Initial tdarr-cli: Python CLI for the Tdarr API
status/nodes/worker/cruddb/insert/api commands (stdlib only). Cracked
Tdarr's x-api-key auth (seededApiKey), alter-worker-limit and update-node
formats. Used to set up 2-node transcoding (workstation NVENC + Pi5 CPU
fallback). Playbook standard: README (per-OS prereqs), doc/plan.md,
.gitignore (build/ first), VS Code build task, install.sh + one-line curl,
Gitea Actions rolling-latest release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPrEZVBpPVoCRyJz2exkZ
2026-07-05 20:31:39 +02:00

134 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""tdarrctl — liten temporär CLI mot Tdarrs API (som npmctl fast för Tdarr).
Auth: x-api-key. Nyckeln läses från $TDARR_API_KEY, annars ur en .env-fil
(rad `apiKey=...`), t.ex. ~/tdarr-node/.env. Nyckeln skrivs aldrig ut.
Bas-URL: $TDARR_URL (standard http://192.168.0.19:8266).
"""
import argparse, json, os, sys, urllib.request, urllib.error, glob
URL = os.environ.get("TDARR_URL", "http://192.168.0.19:8266").rstrip("/")
def load_key():
k = os.environ.get("TDARR_API_KEY")
if k:
return k.strip()
for p in [os.path.expanduser("~/tdarr-node/.env"),
os.path.expanduser("~/fileflows-node/.env")]:
if os.path.exists(p):
for line in open(p):
line = line.strip()
if line.startswith("apiKey="):
return line.split("=", 1)[1].strip()
sys.exit("fel: ingen API-nyckel (sätt TDARR_API_KEY eller apiKey=... i ~/tdarr-node/.env)")
KEY = None
def req(method, path, body=None):
url = path if path.startswith("http") else URL + path
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=data, method=method,
headers={"x-api-key": KEY, "Content-Type": "application/json"})
try:
with urllib.request.urlopen(r, timeout=30) as resp:
raw = resp.read().decode()
code = resp.status
except urllib.error.HTTPError as e:
raw = e.read().decode(); code = e.code
try:
return code, json.loads(raw)
except Exception:
return code, raw
def cruddb(collection, mode, obj=None, docID=""):
return req("POST", "/api/v2/cruddb",
{"data": {"collection": collection, "mode": mode, "docID": docID, "obj": obj or {}}})
def pp(x):
print(json.dumps(x, indent=2, ensure_ascii=False) if not isinstance(x, str) else x)
# ---- kommandon ----
def cmd_status(a):
pp(req("GET", "/api/v2/status")[1])
def cmd_nodes(a):
code, d = req("GET", "/api/v2/get-nodes")
if not isinstance(d, dict):
return pp(d)
for k, n in d.items():
wl = n.get("workerLimits") or {}
print(f"{n.get('nodeName'):24} id={k} paused={n.get('nodePaused')} "
f"cpu={wl.get('transcodecpu')} gpu={wl.get('transcodegpu')}")
def cmd_worker(a):
# hitta nod-id via namn-delsträng
_, d = req("GET", "/api/v2/get-nodes")
nid = next((k for k, n in d.items() if a.node.lower() in (n.get("nodeName") or "").lower()), None)
if not nid:
sys.exit(f"ingen nod matchar '{a.node}'")
for _ in range(a.count):
code, r = req("POST", "/api/v2/alter-worker-limit",
{"data": {"nodeID": nid, "workerType": a.type, "process": a.direction}})
print(code, r)
def cmd_cruddb(a):
obj = None
if a.obj:
obj = json.load(open(a.obj)) if a.obj != "-" else json.load(sys.stdin)
pp(cruddb(a.collection, a.mode, obj, a.docID)[1])
def cmd_api(a):
body = None
if a.data:
body = json.load(open(a.data)) if a.data != "-" else json.load(sys.stdin)
code, r = req(a.method.upper(), a.path, body)
print("HTTP", code)
pp(r)
def cmd_libraries(a):
pp(cruddb("LibrarySettingsJSONDB", "getAll")[1])
def cmd_insert(a):
# generisk insert/update i en collection från en JSON-fil
obj = json.load(open(a.file)) if a.file != "-" else json.load(sys.stdin)
pp(cruddb(a.collection, a.mode, obj, obj.get("_id", ""))[1])
def main():
global KEY
KEY = load_key()
p = argparse.ArgumentParser(prog="tdarrctl", description="Temporär Tdarr-API-CLI")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("status").set_defaults(f=cmd_status)
sub.add_parser("nodes").set_defaults(f=cmd_nodes)
w = sub.add_parser("worker", help="ändra worker-gräns")
w.add_argument("node"); w.add_argument("type",
choices=["transcodecpu", "transcodegpu", "healthcheckcpu", "healthcheckgpu"])
w.add_argument("direction", choices=["increase", "decrease"])
w.add_argument("count", nargs="?", type=int, default=1)
w.set_defaults(f=cmd_worker)
c = sub.add_parser("cruddb", help="generisk cruddb")
c.add_argument("collection"); c.add_argument("mode")
c.add_argument("--obj"); c.add_argument("--docID", default="")
c.set_defaults(f=cmd_cruddb)
ap = sub.add_parser("api", help="generisk HTTP")
ap.add_argument("method"); ap.add_argument("path"); ap.add_argument("--data")
ap.set_defaults(f=cmd_api)
sub.add_parser("libraries").set_defaults(f=cmd_libraries)
ins = sub.add_parser("insert", help="insert/update obj (JSON-fil) i collection")
ins.add_argument("collection"); ins.add_argument("file")
ins.add_argument("--mode", default="insert")
ins.set_defaults(f=cmd_insert)
a = p.parse_args()
a.f(a)
if __name__ == "__main__":
main()