first commit
This commit is contained in:
106
scripts/stack_installers/configure_pytorrent_api.py
Executable file
106
scripts/stack_installers/configure_pytorrent_api.py
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configure pyTorrent through its HTTP API after rTorrent is installed."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _request(base_url: str, method: str, path: str, payload: dict | None = None, token: str | None = None, timeout: int = 10) -> dict:
|
||||
url = base_url.rstrip("/") + path
|
||||
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
headers = {"Accept": "application/json"}
|
||||
if payload is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
req = urllib.request.Request(url, data=data, method=method.upper(), headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
raw = resp.read().decode("utf-8", "replace")
|
||||
return json.loads(raw or "{}")
|
||||
except urllib.error.HTTPError as exc:
|
||||
raw = exc.read().decode("utf-8", "replace")
|
||||
raise RuntimeError(f"API {method} {path} failed with HTTP {exc.code}: {raw}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(f"API {method} {path} failed: {exc.reason}") from exc
|
||||
|
||||
|
||||
def _wait_for_api(base_url: str, token: str | None, seconds: int) -> None:
|
||||
deadline = time.time() + seconds
|
||||
last_error = None
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
_request(base_url, "GET", "/api/profiles", token=token, timeout=5)
|
||||
return
|
||||
except Exception as exc: # noqa: BLE001 - installation helper should keep retrying.
|
||||
last_error = exc
|
||||
time.sleep(2)
|
||||
raise RuntimeError(f"pyTorrent API is not ready after {seconds}s at {base_url}: {last_error}. Check PYTORRENT_PORT in .env and systemctl status pytorrent.")
|
||||
|
||||
|
||||
def _find_profile(profiles: list[dict], name: str, scgi_url: str) -> dict | None:
|
||||
for profile in profiles:
|
||||
if str(profile.get("name") or "") == name:
|
||||
return profile
|
||||
for profile in profiles:
|
||||
if str(profile.get("scgi_url") or "") == scgi_url:
|
||||
return profile
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Create/update and activate a pyTorrent rTorrent profile through the HTTP API.")
|
||||
parser.add_argument("--base-url", default=os.getenv("PYTORRENT_BASE_URL", "http://127.0.0.1:8090"))
|
||||
parser.add_argument("--api-token", default=os.getenv("PYTORRENT_API_TOKEN", ""), help="Bearer token when pyTorrent auth is enabled.")
|
||||
parser.add_argument("--profile-name", default=os.getenv("PYTORRENT_RTORRENT_PROFILE_NAME", "Local rTorrent"))
|
||||
parser.add_argument("--scgi-url", default=os.getenv("PYTORRENT_RTORRENT_SCGI_URL", "scgi://127.0.0.1:5000"))
|
||||
parser.add_argument("--timeout", type=int, default=int(os.getenv("PYTORRENT_RTORRENT_TIMEOUT", "10")))
|
||||
parser.add_argument("--wait", type=int, default=int(os.getenv("PYTORRENT_API_WAIT_SECONDS", "90")))
|
||||
parser.add_argument("--remote", action="store_true", default=os.getenv("PYTORRENT_RTORRENT_REMOTE", "0").lower() in {"1", "true", "yes", "on"})
|
||||
args = parser.parse_args()
|
||||
|
||||
token = args.api_token.strip() or None
|
||||
_wait_for_api(args.base_url, token, args.wait)
|
||||
current = _request(args.base_url, "GET", "/api/profiles", token=token)
|
||||
profiles = current.get("profiles") or []
|
||||
payload = {
|
||||
"name": args.profile_name,
|
||||
"scgi_url": args.scgi_url,
|
||||
"is_default": True,
|
||||
"timeout_seconds": args.timeout,
|
||||
"max_parallel_jobs": 5,
|
||||
"light_parallel_jobs": 4,
|
||||
"light_job_timeout_seconds": 300,
|
||||
"heavy_job_timeout_seconds": 7200,
|
||||
"pending_job_timeout_seconds": 900,
|
||||
"is_remote": bool(args.remote),
|
||||
}
|
||||
existing = _find_profile(profiles, args.profile_name, args.scgi_url)
|
||||
if existing:
|
||||
profile_id = int(existing["id"])
|
||||
result = _request(args.base_url, "PUT", f"/api/profiles/{profile_id}", payload, token=token)
|
||||
action = "updated"
|
||||
else:
|
||||
result = _request(args.base_url, "POST", "/api/profiles", payload, token=token)
|
||||
profile_id = int((result.get("profile") or {}).get("id") or 0)
|
||||
action = "created"
|
||||
if not profile_id:
|
||||
raise RuntimeError(f"Profile {action}, but API response did not include an id: {result}")
|
||||
_request(args.base_url, "POST", f"/api/profiles/{profile_id}/activate", token=token)
|
||||
test = _request(args.base_url, "GET", f"/api/profiles/{profile_id}/diagnostics", token=token)
|
||||
print(json.dumps({"ok": True, "action": action, "profile_id": profile_id, "diagnostics": test.get("diagnostics")}, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc: # noqa: BLE001 - user-facing installer output.
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user