107 lines
4.8 KiB
Python
Executable File
107 lines
4.8 KiB
Python
Executable File
#!/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)
|