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
140 lines
4.6 KiB
Python
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()
|