All checks were successful
release / release (push) Successful in 1m4s
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
134 lines
4.8 KiB
Python
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()
|