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
This commit is contained in:
139
scripts/jellyseerr_init.py
Normal file
139
scripts/jellyseerr_init.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user