--- - name: Deploy 4K arr instances (radarr-4k, sonarr-4k) on media-server hosts: media-server become: true vars: app_dir: /home/tommy/arrs-4k tasks: - name: Create arrs-4k directories ansible.builtin.file: path: "{{ item }}" state: directory owner: tommy group: tommy mode: "0755" loop: - "{{ app_dir }}" - "{{ app_dir }}/radarr-4k" - "{{ app_dir }}/sonarr-4k" - name: Write docker-compose.yml ansible.builtin.copy: dest: "{{ app_dir }}/docker-compose.yml" owner: tommy group: tommy mode: "0644" content: | # 4K Quality-First arr instances — SEPARATE from the untouched 1080p stack. # Host: media-server (.183). Shares /mnt/media/data -> /data (single hardlink tree). # Roots: radarr-4k -> /data/media/movies-4k ; sonarr-4k -> /data/media/tv-4k # Routing: Traefik FILE provider on node01 dynamic_conf.yml (NO docker labels): # radarr-4k.goattw.net -> 192.168.99.183:7879 ; sonarr-4k.goattw.net -> 192.168.99.183:8990 # middlewares: secure-headers@file, authelia-auth@file, crowdsec-bouncer@file # Tdarr does NOT watch movies-4k/tv-4k (quality-first keeps the grabbed file). services: radarr-4k: image: lscr.io/linuxserver/radarr:latest container_name: radarr-4k mem_limit: 1g cpus: 1.0 environment: - PUID=1000 - PGID=1000 - TZ=America/Chicago networks: - traefik_public volumes: - ./radarr-4k:/config - /mnt/media/data:/data ports: - "7879:7878/tcp" restart: unless-stopped sonarr-4k: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr-4k mem_limit: 1g cpus: 1.0 environment: - PUID=1000 - PGID=1000 - TZ=America/Chicago networks: - traefik_public volumes: - ./sonarr-4k:/config - /mnt/media/data:/data ports: - "8990:8989/tcp" restart: unless-stopped networks: traefik_public: external: true - name: Deploy 4K arrs with docker compose community.docker.docker_compose_v2: project_src: "{{ app_dir }}" state: present pull: missing register: r - name: Result ansible.builtin.debug: msg: "arrs-4k {{ 'running (changed)' if r.changed else 'already up (no-op)' }}" - name: Wait for 4K arr APIs (ports) ansible.builtin.wait_for: host: 192.168.99.183 port: "{{ item }}" timeout: 90 loop: [7879, 8990] - name: Install 4K download-client config script ansible.builtin.copy: dest: "{{ app_dir }}/ensure-arrs-4k-dl.py" owner: tommy group: tommy mode: "0755" content: | #!/usr/bin/env python3 # Idempotent: ensure the 4K arr download clients + sabnzbd categories exist. # Reads RADARR4K_KEY / SONARR4K_KEY / SAB_KEY from env. Prints CHANGED or NOCHANGE. import os, json, urllib.request H = "http://192.168.99.183" SAB = os.environ["SAB_KEY"] changed = False def sab_cats(): return json.load(urllib.request.urlopen(f"{H}:8081/api?mode=get_cats&output=json&apikey={SAB}"))["categories"] def sab_add(cat): urllib.request.urlopen(f"{H}:8081/api?mode=set_config§ion=categories&keyword={cat}&dir={cat}&apikey={SAB}").read() def dc_get(port, key): return json.load(urllib.request.urlopen(urllib.request.Request(f"{H}:{port}/api/v3/downloadclient", headers={"X-Api-Key": key}))) def dc_post(port, key, body): urllib.request.urlopen(urllib.request.Request(f"{H}:{port}/api/v3/downloadclient", data=json.dumps(body).encode(), method="POST", headers={"X-Api-Key": key, "Content-Type": "application/json"})) def sab_body(catf, catv, recent, older): return {"enable": True, "protocol": "usenet", "priority": 1, "name": "SABnzbd", "implementation": "Sabnzbd", "configContract": "SabnzbdSettings", "fields": [{"name": "host", "value": "gluetun"}, {"name": "port", "value": 8080}, {"name": "apiKey", "value": SAB}, {"name": catf, "value": catv}, {"name": recent, "value": -100}, {"name": older, "value": -100}]} def tr_body(catf, catv): return {"enable": True, "protocol": "torrent", "priority": 1, "name": "Transmission", "implementation": "Transmission", "configContract": "TransmissionSettings", "fields": [{"name": "host", "value": "gluetun"}, {"name": "port", "value": 9091}, {"name": "urlBase", "value": "/transmission/"}, {"name": catf, "value": catv}]} cats = sab_cats() for c in ("movies-4k", "tv-4k"): if c not in cats: sab_add(c); changed = True plan = [ (7879, os.environ["RADARR4K_KEY"], [sab_body("movieCategory", "movies-4k", "recentMoviePriority", "olderMoviePriority"), tr_body("movieCategory", "radarr-uhd")]), (8990, os.environ["SONARR4K_KEY"], [sab_body("tvCategory", "tv-4k", "recentTvPriority", "olderTvPriority"), tr_body("tvCategory", "sonarr-uhd")]), ] for port, key, bodies in plan: have = {c["name"] for c in dc_get(port, key)} for b in bodies: if b["name"] not in have: dc_post(port, key, b); changed = True print("CHANGED" if changed else "NOCHANGE") - name: Ensure 4K download clients + sabnzbd categories (idempotent, cats movies-4k/tv-4k) ansible.builtin.command: "python3 {{ app_dir }}/ensure-arrs-4k-dl.py" environment: RADARR4K_KEY: "{{ vault_radarr4k_apikey }}" SONARR4K_KEY: "{{ vault_sonarr4k_apikey }}" SAB_KEY: "{{ vault_sabnzbd_apikey }}" register: dl_result changed_when: "'CHANGED' in dl_result.stdout" no_log: true