Initial tdarr-cli: Python CLI for the Tdarr API
All checks were successful
release / release (push) Successful in 1m4s
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
This commit is contained in:
65
.gitea/workflows/release.yaml
Normal file
65
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
name: release
|
||||
|
||||
# Publicerar tdarrctl (arkitektur-oberoende Python-stdlib-skript) + install.sh
|
||||
# till en rullande 'latest'-release; en vX.Y.Z-tag ger en versionerad release.
|
||||
# Runnern använder sin egen GITHUB_TOKEN mot Gitea-API:t.
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release-tag (tom = 'latest')"
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Bestäm tag
|
||||
id: t
|
||||
run: |
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/*) T="$GITHUB_REF_NAME" ;;
|
||||
*) T="${{ inputs.tag }}"; [ -z "$T" ] && T="latest" ;;
|
||||
esac
|
||||
echo "tag=$T" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Paketera + syntaxkoll
|
||||
run: |
|
||||
set -eux
|
||||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO=sudo
|
||||
$SUDO apt-get update && $SUDO apt-get install -y --no-install-recommends jq curl ca-certificates python3 || true
|
||||
python3 -m py_compile tdarrctl.py
|
||||
mkdir -p dist
|
||||
install -m 0755 tdarrctl.py dist/tdarrctl
|
||||
cp install.sh dist/install.sh
|
||||
cd dist && sha256sum tdarrctl install.sh > checksums.txt && ls -la
|
||||
|
||||
- name: Skapa release + ladda upp assets
|
||||
env:
|
||||
API: http://gitea-d:3000/api/v1
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ steps.t.outputs.tag }}
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
BODY="tdarr-cli $TAG — CLI för Tdarrs API (Python, stdlib).
|
||||
|
||||
Installera (en rad):
|
||||
curl -fsSL https://gitea.brasse-pc.eu/brasse/tdarr-cli/raw/branch/master/install.sh | sh
|
||||
|
||||
Kräver python3. Se wiki för användning."
|
||||
payload=$(jq -n --arg tag "$TAG" --arg body "$BODY" '{tag_name:$tag,name:$tag,body:$body}')
|
||||
rid=$(curl -s -X POST "$API/repos/$REPO/releases" -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' -d "$payload" | jq -r '.id // empty')
|
||||
[ -z "$rid" ] && rid=$(curl -s "$API/repos/$REPO/releases/tags/$TAG" -H "Authorization: token $TOKEN" | jq -r '.id')
|
||||
curl -s -X PATCH "$API/repos/$REPO/releases/$rid" -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg tag "$TAG" --arg body "$BODY" '{name:$tag,body:$body}')" -o /dev/null
|
||||
for f in tdarrctl install.sh checksums.txt; do
|
||||
aid=$(curl -s "$API/repos/$REPO/releases/$rid/assets" -H "Authorization: token $TOKEN" | jq -r ".[] | select(.name==\"$f\") | .id")
|
||||
[ -n "$aid" ] && [ "$aid" != "null" ] && curl -s -X DELETE "$API/repos/$REPO/releases/$rid/assets/$aid" -H "Authorization: token $TOKEN" -o /dev/null
|
||||
curl -s -X POST "$API/repos/$REPO/releases/$rid/assets?name=$f" -H "Authorization: token $TOKEN" -F "attachment=@dist/$f" -o /dev/null -w " $f -> HTTP %{http_code}\n"
|
||||
done
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
build/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"detail": "Bygger en fristående tdarrctl till ./build/",
|
||||
"type": "shell",
|
||||
"command": "mkdir -p build && install -m 0755 tdarrctl.py build/tdarrctl && echo '-> build/tdarrctl'",
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "nodes",
|
||||
"detail": "Kör: tdarrctl nodes",
|
||||
"type": "shell",
|
||||
"command": "python3 tdarrctl.py nodes",
|
||||
"group": "test",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "syntaxkoll",
|
||||
"type": "shell",
|
||||
"command": "python3 -m py_compile tdarrctl.py && echo OK",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# tdarr-cli (`tdarrctl`)
|
||||
|
||||
En liten CLI mot **Tdarrs API** — som `npmctl` fast för Tdarr. Byggd för att
|
||||
konfigurera/styra Tdarr-servern (bibliotek, flows, noder, worker-gränser) utan
|
||||
att klicka i webb-UI:t. Ren Python (bara stdlib).
|
||||
|
||||
## Installera
|
||||
|
||||
En rad (laddar ner senaste `tdarrctl` och lägger den i PATH):
|
||||
|
||||
```sh
|
||||
curl -fsSL https://gitea.brasse-pc.eu/brasse/tdarr-cli/raw/branch/master/install.sh | sh
|
||||
```
|
||||
|
||||
Miljövariabler: `TDARRCTL_INSTALL_DIR` (installkatalog), `TDARRCTL_VERSION` (release-tag).
|
||||
|
||||
## Prerequisites (per OS)
|
||||
|
||||
Endast **Python ≥ 3.8** krävs (inga externa paket — bara standardbiblioteket).
|
||||
|
||||
| OS | Kommando |
|
||||
|----|----------|
|
||||
| Garuda/Arch (dev-boxen) | `sudo pacman -S --needed python` |
|
||||
| Debian/Ubuntu | `sudo apt install python3` |
|
||||
|
||||
## Konfiguration
|
||||
|
||||
- Bas-URL: `TDARR_URL` (standard `http://192.168.0.19:8266`).
|
||||
- API-nyckel: `TDARR_API_KEY`, annars läses `apiKey=...` ur `~/tdarr-node/.env`.
|
||||
Skickas som `x-api-key`; skrivs aldrig ut.
|
||||
|
||||
## Kör
|
||||
|
||||
```sh
|
||||
python3 tdarrctl.py status # server-status/version
|
||||
python3 tdarrctl.py nodes # noder + worker-gränser
|
||||
python3 tdarrctl.py worker <namn> transcodegpu increase [n] # ändra workers
|
||||
python3 tdarrctl.py libraries # lista bibliotek
|
||||
python3 tdarrctl.py cruddb <COLLECTION> <mode> [--obj f.json] [--docID id]
|
||||
python3 tdarrctl.py insert <COLLECTION> <f.json> [--mode insert|update|removeOne]
|
||||
python3 tdarrctl.py api <METHOD> <path> [--data f.json|-] # generiskt
|
||||
```
|
||||
|
||||
## Bygg (till ./build)
|
||||
|
||||
VS Code: **Run Build Task** (`Ctrl+Shift+B`) → `build`-tasken lägger en
|
||||
körbar `tdarrctl` i `build/` (gitignoread). Manuellt:
|
||||
|
||||
```sh
|
||||
mkdir -p build && install -m 0755 tdarrctl.py build/tdarrctl
|
||||
./build/tdarrctl status
|
||||
```
|
||||
|
||||
## Test / rök-koll
|
||||
|
||||
```sh
|
||||
python3 -m py_compile tdarrctl.py # syntax
|
||||
python3 tdarrctl.py status # når servern?
|
||||
```
|
||||
|
||||
## Mer
|
||||
|
||||
- Plan & designbeslut: [`doc/plan.md`](doc/plan.md).
|
||||
- Fördjupning (API-format, collections, exempel): repots **wiki**.
|
||||
40
doc/plan.md
Normal file
40
doc/plan.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# tdarr-cli — plan
|
||||
|
||||
## Goal
|
||||
En liten, skriptbar CLI mot Tdarrs (odokumenterade) HTTP-API så att Tdarr-servern
|
||||
på Pi5 kan konfigureras/styras utan webb-UI:t — noder, worker-gränser, bibliotek
|
||||
och generiska anrop. Ursprungligen byggd för att sätta upp 2-nod-transkodning
|
||||
(workstation NVENC + Pi5 CPU-fallback).
|
||||
|
||||
## Stack
|
||||
- Python 3, endast standardbibliotek (`urllib`, `json`, `argparse`). Ingen build.
|
||||
- Arkitektur-oberoende (körs på både x64 och arm64 med python3).
|
||||
|
||||
## Deploy target
|
||||
Pattern A — installerbar CLI. Release-CI publicerar `tdarrctl` (+ `install.sh`,
|
||||
`checksums.txt`) till en rullande `latest`-release; en-rads `curl … | sh`-install.
|
||||
|
||||
## Auth
|
||||
`x-api-key` (Tdarr `seededApiKey`). Nyckeln läses ur env eller `~/tdarr-node/.env`,
|
||||
skrivs aldrig ut.
|
||||
|
||||
## Kommandon (nuläge)
|
||||
`status`, `nodes`, `worker`, `libraries`, `cruddb` (generisk collection-CRUD),
|
||||
`insert` (insert/update/removeOne), `api` (generiskt HTTP).
|
||||
|
||||
Kartlagda API-detaljer:
|
||||
- `alter-worker-limit`: `{data:{nodeID, workerType:<transcodegpu|…>, process:<increase|decrease>}}`.
|
||||
- `update-node`: `{data:{nodeID, nodeUpdates:{…}}}` (t.ex. `nodePaused`).
|
||||
- cruddb-collections: `LibrarySettingsJSONDB`, `FlowsJSONDB`, `NodeJSONDB`, `PluginJSONDB`, m.fl.
|
||||
modes: `getAll`, `insert`, `update`, `removeOne`.
|
||||
|
||||
## Roadmap / expansions
|
||||
- Bekvämlighetskommandon: `pause <nod>` / `resume <nod>` (wrappar `update-node`).
|
||||
- `create-library` / `set-flow` som bygger korrekta objekt (Tdarr-objekten är
|
||||
stora/ömtåliga via rå cruddb — bibliotek/flow görs idag bäst i UI:t).
|
||||
- Konfigfil (`~/.config/tdarrctl/config.json`) för URL + nyckel-referens.
|
||||
- `--json`-utdata för skriptning.
|
||||
|
||||
## Open questions
|
||||
- Vilka fler high-level-kommandon är värda att curera (vs generisk `cruddb`/`api`)?
|
||||
- Ska den slås ihop med/ersättas av tdarr-tray för icke-CLI-styrning?
|
||||
18
install.sh
Executable file
18
install.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# En-rads-install:
|
||||
# curl -fsSL https://gitea.brasse-pc.eu/brasse/tdarr-cli/raw/branch/master/install.sh | sh
|
||||
set -eu
|
||||
REPO="https://gitea.brasse-pc.eu/brasse/tdarr-cli"
|
||||
VER="${TDARRCTL_VERSION:-latest}"
|
||||
command -v python3 >/dev/null 2>&1 || { echo "fel: python3 krävs"; exit 1; }
|
||||
if command -v curl >/dev/null 2>&1; then DL="curl -fsSL -o"; else DL="wget -qO"; fi
|
||||
DEST="${TDARRCTL_INSTALL_DIR:-}"
|
||||
[ -z "$DEST" ] && { if [ "$(id -u)" -eq 0 ] || [ -w /usr/local/bin ]; then DEST=/usr/local/bin; else DEST="$HOME/.local/bin"; fi; }
|
||||
mkdir -p "$DEST"
|
||||
tmp="$(mktemp)"; trap 'rm -f "$tmp"' EXIT
|
||||
$DL "$tmp" "$REPO/releases/download/$VER/tdarrctl"
|
||||
chmod +x "$tmp"
|
||||
mv "$tmp" "$DEST/tdarrctl" 2>/dev/null || { command -v sudo >/dev/null && sudo mv "$tmp" "$DEST/tdarrctl"; }
|
||||
trap - EXIT
|
||||
echo "Installerat: $DEST/tdarrctl"
|
||||
case ":$PATH:" in *":$DEST:"*) ;; *) echo "OBS: lägg $DEST i PATH";; esac
|
||||
133
tdarrctl.py
Normal file
133
tdarrctl.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user