191 lines
7.1 KiB
Python
Executable File
191 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Configure safe baseline ARR app settings from local environment values."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
ENV_FILE = Path(os.environ.get("ARR_ENV_FILE", SCRIPT_DIR / ".env"))
|
|
|
|
|
|
def load_env(path: Path) -> None:
|
|
if not path.exists():
|
|
return
|
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
|
|
|
|
|
def request_json(method: str, url: str, api_key: str, payload: dict | None = None) -> object:
|
|
data = None
|
|
headers = {"X-Api-Key": api_key, "Accept": "application/json"}
|
|
if payload is not None:
|
|
data = json.dumps(payload).encode("utf-8")
|
|
headers["Content-Type"] = "application/json"
|
|
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
body = response.read()
|
|
if not body:
|
|
return {}
|
|
return json.loads(body.decode("utf-8"))
|
|
|
|
|
|
def wait_for_app(name: str, base_url: str, api_key: str) -> None:
|
|
for attempt in range(1, 31):
|
|
try:
|
|
request_json("GET", f"{base_url}/api/v3/system/status", api_key)
|
|
return
|
|
except Exception as exc: # noqa: BLE001
|
|
if attempt == 30:
|
|
raise RuntimeError(f"{name} did not become reachable at {base_url}") from exc
|
|
time.sleep(2)
|
|
|
|
|
|
def ensure_root_folder(name: str, base_url: str, api_key: str, path: str) -> None:
|
|
existing = request_json("GET", f"{base_url}/api/v3/rootfolder", api_key)
|
|
if isinstance(existing, list) and any(item.get("path") == path for item in existing if isinstance(item, dict)):
|
|
print(f"{name}: root folder already exists: {path}")
|
|
return
|
|
request_json("POST", f"{base_url}/api/v3/rootfolder", api_key, {"path": path})
|
|
print(f"{name}: added root folder: {path}")
|
|
|
|
|
|
def schema_fields(base_url: str, api_key: str, endpoint: str, implementation: str) -> list[dict]:
|
|
schemas = request_json("GET", f"{base_url}/api/v3/{endpoint}/schema", api_key)
|
|
if not isinstance(schemas, list):
|
|
return []
|
|
for schema in schemas:
|
|
if isinstance(schema, dict) and schema.get("implementation") == implementation:
|
|
fields = schema.get("fields")
|
|
return fields if isinstance(fields, list) else []
|
|
return []
|
|
|
|
|
|
def fill_fields(fields: list[dict], values: dict[str, object]) -> list[dict]:
|
|
hydrated = []
|
|
for field in fields:
|
|
if not isinstance(field, dict):
|
|
continue
|
|
item = dict(field)
|
|
name = item.get("name")
|
|
if name in values:
|
|
item["value"] = values[name]
|
|
hydrated.append(item)
|
|
return hydrated
|
|
|
|
|
|
def ensure_qbittorrent(name: str, base_url: str, api_key: str, category: str) -> None:
|
|
clients = request_json("GET", f"{base_url}/api/v3/downloadclient", api_key)
|
|
if isinstance(clients, list) and any(
|
|
isinstance(item, dict) and item.get("implementation") == "QBittorrent" for item in clients
|
|
):
|
|
print(f"{name}: qBittorrent download client already exists")
|
|
return
|
|
|
|
fields = schema_fields(base_url, api_key, "downloadclient", "QBittorrent")
|
|
if not fields:
|
|
raise RuntimeError(f"{name}: could not find qBittorrent download client schema")
|
|
|
|
port = int(os.environ.get("QBITTORRENT_PORT", "8080"))
|
|
values = {
|
|
"host": "qbittorrent",
|
|
"port": port,
|
|
"urlBase": "",
|
|
"username": os.environ.get("QBITTORRENT_USERNAME", "admin"),
|
|
"password": os.environ.get("QBITTORRENT_PASSWORD", ""),
|
|
"category": category,
|
|
"movieCategory": category,
|
|
"tvCategory": category,
|
|
"recentMoviePriority": 0,
|
|
"olderMoviePriority": 0,
|
|
"recentTvPriority": 0,
|
|
"olderTvPriority": 0,
|
|
"addPaused": False,
|
|
"useSsl": False,
|
|
}
|
|
payload = {
|
|
"enable": True,
|
|
"protocol": "torrent",
|
|
"priority": 1,
|
|
"removeCompletedDownloads": True,
|
|
"removeFailedDownloads": True,
|
|
"name": "qBittorrent",
|
|
"implementation": "QBittorrent",
|
|
"configContract": "QBittorrentSettings",
|
|
"fields": fill_fields(fields, values),
|
|
"tags": [],
|
|
}
|
|
request_json("POST", f"{base_url}/api/v3/downloadclient", api_key, payload)
|
|
print(f"{name}: added qBittorrent download client")
|
|
|
|
|
|
def apply_import_lists(name: str, base_url: str, api_key: str, path_env: str) -> None:
|
|
list_path = os.environ.get(path_env, "")
|
|
if not list_path:
|
|
return
|
|
path = Path(list_path)
|
|
if not path.exists():
|
|
raise RuntimeError(f"{name}: {path_env} points to missing file {path}")
|
|
|
|
desired = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(desired, list):
|
|
raise RuntimeError(f"{name}: {path} must contain a JSON array")
|
|
|
|
existing = request_json("GET", f"{base_url}/api/v3/importlist", api_key)
|
|
existing_names = set()
|
|
if isinstance(existing, list):
|
|
existing_names = {item.get("name") for item in existing if isinstance(item, dict)}
|
|
for item in desired:
|
|
if not isinstance(item, dict) or not item.get("name"):
|
|
raise RuntimeError(f"{name}: every import-list entry must be an object with a name")
|
|
if item["name"] in existing_names:
|
|
print(f"{name}: import list already exists: {item['name']}")
|
|
continue
|
|
request_json("POST", f"{base_url}/api/v3/importlist", api_key, item)
|
|
print(f"{name}: added import list: {item['name']}")
|
|
|
|
|
|
def required_env(name: str) -> str:
|
|
value = os.environ.get(name, "")
|
|
if not value:
|
|
raise RuntimeError(f"Set {name} in {ENV_FILE} before running this script")
|
|
return value
|
|
|
|
|
|
def main() -> int:
|
|
load_env(ENV_FILE)
|
|
radarr_key = required_env("RADARR_API_KEY")
|
|
sonarr_key = required_env("SONARR_API_KEY")
|
|
|
|
radarr_url = os.environ.get("RADARR_URL", "http://127.0.0.1:7878")
|
|
sonarr_url = os.environ.get("SONARR_URL", "http://127.0.0.1:8989")
|
|
|
|
wait_for_app("Radarr", radarr_url, radarr_key)
|
|
wait_for_app("Sonarr", sonarr_url, sonarr_key)
|
|
ensure_root_folder("Radarr", radarr_url, radarr_key, os.environ.get("RADARR_ROOT_FOLDER", "/data/media/movies"))
|
|
ensure_root_folder("Sonarr", sonarr_url, sonarr_key, os.environ.get("SONARR_ROOT_FOLDER", "/data/media/tv"))
|
|
ensure_qbittorrent("Radarr", radarr_url, radarr_key, "radarr")
|
|
ensure_qbittorrent("Sonarr", sonarr_url, sonarr_key, "sonarr")
|
|
apply_import_lists("Radarr", radarr_url, radarr_key, "RADARR_IMPORT_LISTS_JSON")
|
|
apply_import_lists("Sonarr", sonarr_url, sonarr_key, "SONARR_IMPORT_LISTS_JSON")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except (RuntimeError, urllib.error.URLError, urllib.error.HTTPError) as error:
|
|
print(f"error: {error}", file=sys.stderr)
|
|
raise SystemExit(1)
|