Files
arr-init/scripts/jellyseerr_init.py
Simon Gardling b464a8cea2 refactor: extract Python scripts into standalone files
Move embedded Python scripts out of Nix string interpolation into
standalone files under scripts/.  Each script reads its configuration
from a JSON file passed as the first CLI argument.

Shared utilities (API key reading, API polling, health check loop)
are consolidated into common.py, eliminating three copies of
read_api_key and wait_for_api.

Implementation improvements included in the extraction:
- Remove pyarr dependency; all HTTP calls use raw requests
- Add update semantics: download clients and synced apps are now
  compared against desired state and updated on drift via PUT
- Bazarr configure_provider compares API keys and updates stale ones
- Narrow health_check_loop exception clause from bare Exception to
  (RequestException, ValueError, KeyError)
- Fix double resp.json() call in resolve_profile_id (jellyseerr)
- Replace os.system with subprocess.run for Jellyseerr restart
- Handle missing 'value' key in Servarr field API responses
2026-04-16 16:33:18 -04:00

140 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Declarative quality profile initialization for Jellyseerr.
Resolves profile names to IDs by querying Radarr/Sonarr APIs, then patches
Jellyseerr's settings.json so new requests default to the correct quality
profiles.
"""
import json
import os
import subprocess
import sys
import requests as http
from common import load_config, read_api_key_xml, wait_for_api
def resolve_profile_id(base_url, api_key, profile_name, app_name):
"""Query a Servarr app for quality profiles and resolve a name to an ID."""
resp = http.get(
f"{base_url}/qualityprofile",
headers={"X-Api-Key": api_key},
timeout=30,
)
resp.raise_for_status()
profiles = resp.json()
for profile in profiles:
if profile["name"] == profile_name:
print(f"Resolved {app_name} profile '{profile_name}' -> ID {profile['id']}")
return profile["id"]
available = [p["name"] for p in profiles]
print(
f"Profile '{profile_name}' not found in {app_name}. "
f"Available: {available}",
file=sys.stderr,
)
sys.exit(1)
def main():
cfg = load_config()
settings_path = os.path.join(cfg["configDir"], "settings.json")
if not os.path.isfile(settings_path):
print(f"{settings_path} not found, skipping (Jellyseerr not yet initialized)")
return
timeout = cfg["apiTimeout"]
# Resolve Radarr profile
radarr_cfg = cfg["radarr"]
radarr_key = read_api_key_xml(f"{radarr_cfg['dataDir']}/config.xml")
radarr_bind = radarr_cfg.get("bindAddress", "127.0.0.1")
radarr_base = f"http://{radarr_bind}:{radarr_cfg['port']}/api/v3"
wait_for_api(radarr_base, radarr_key, timeout, "Radarr")
radarr_profile_id = resolve_profile_id(
radarr_base, radarr_key, radarr_cfg["profileName"], "Radarr",
)
# Resolve Sonarr profiles
sonarr_cfg = cfg["sonarr"]
sonarr_key = read_api_key_xml(f"{sonarr_cfg['dataDir']}/config.xml")
sonarr_bind = sonarr_cfg.get("bindAddress", "127.0.0.1")
sonarr_base = f"http://{sonarr_bind}:{sonarr_cfg['port']}/api/v3"
wait_for_api(sonarr_base, sonarr_key, timeout, "Sonarr")
sonarr_profile_id = resolve_profile_id(
sonarr_base, sonarr_key, sonarr_cfg["profileName"], "Sonarr",
)
sonarr_anime_profile_id = resolve_profile_id(
sonarr_base, sonarr_key, sonarr_cfg["animeProfileName"], "Sonarr (anime)",
)
# Patch settings.json
with open(settings_path) as f:
settings = json.load(f)
changed = False
for entry in settings.get("radarr", []):
if (
entry.get("activeProfileId") != radarr_profile_id
or entry.get("activeProfileName") != radarr_cfg["profileName"]
):
entry["activeProfileId"] = radarr_profile_id
entry["activeProfileName"] = radarr_cfg["profileName"]
changed = True
print(
f"Radarr '{entry.get('name', '?')}': "
f"set profile to {radarr_cfg['profileName']} (ID {radarr_profile_id})"
)
for entry in settings.get("sonarr", []):
updates = {}
if (
entry.get("activeProfileId") != sonarr_profile_id
or entry.get("activeProfileName") != sonarr_cfg["profileName"]
):
updates["activeProfileId"] = sonarr_profile_id
updates["activeProfileName"] = sonarr_cfg["profileName"]
if (
entry.get("activeAnimeProfileId") != sonarr_anime_profile_id
or entry.get("activeAnimeProfileName") != sonarr_cfg["animeProfileName"]
):
updates["activeAnimeProfileId"] = sonarr_anime_profile_id
updates["activeAnimeProfileName"] = sonarr_cfg["animeProfileName"]
if updates:
entry.update(updates)
changed = True
print(
f"Sonarr '{entry.get('name', '?')}': "
f"set profile to {sonarr_cfg['profileName']} (ID {sonarr_profile_id})"
)
if not changed:
print("Jellyseerr profiles already correct, no changes needed")
return
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
print("Updated settings.json, restarting Jellyseerr...")
result = subprocess.run(
["systemctl", "restart", "jellyseerr.service"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(
f"Failed to restart Jellyseerr: {result.stderr.strip()}",
file=sys.stderr,
)
sys.exit(1)
print("Jellyseerr init complete")
if __name__ == "__main__":
main()