diff --git a/.gitignore b/.gitignore
index 60ca337..9e63007 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
keys/
wireguard/
+certs/
*.vault
*.secret
ansible/vault_pass
diff --git a/ansible/inventory/group_vars/all/vault.yml b/ansible/inventory/group_vars/all/vault.yml
index 61f4230..4199043 100644
--- a/ansible/inventory/group_vars/all/vault.yml
+++ b/ansible/inventory/group_vars/all/vault.yml
@@ -1,35 +1,38 @@
$ANSIBLE_VAULT;1.1;AES256
-30353336323330616638366131616265353935386336623962313139666630386233333435376132
-6632383465666533323435613938636564636166353265300a636531383030323130356439663538
-39373933303438346138306430393831663236626132353031623837643563353536383432393866
-3963613139663531620a613631636261653764663539323363326538373438326666646666643432
-66663430333263653439613937326134653830663631363236663238663933613363343039393734
-62313035653337363836633333636231643635656134396639653431646236353737353133343631
-30343237373232356532363732366236343565353739353065363562643932386164366163343235
-33363837626434306339623733623464646633656431643732303330353232656439313639363362
-33376364343965323464663634386530303462613833333136623635366537363265646163653038
-38376563353865636437313838313236333662376430303930356432663261383834626563343139
-66643730373239623737383138663935313031373062646363663865366233383663336330333134
-30333032366131363630303735653265636337616531656133343935306136303830333439656230
-36303565643939616138323932393637373935396264666162356538313165656438306232613665
-64666334306134323730383665666664303930646630343636653335653735323561393061323031
-65643831353464353235626535346337663263396432356232323434653562656365386663636131
-39663136313664353331373164616331626135356234623961663430653661316465393439643033
-64653562383533383839386237323439376139333962396336386633653566656563636533366336
-35313366336266633636383763643463376134316366393235313964313239373836336665353339
-66303531336333646333326265646331353363653036313261663462626462653964306465383665
-66313331656538336534653166363535336232316363303335663835656566623132383131373436
-36366235643935373061366565623761333335666163326464326636626462663661346163323438
-64653234333236383039393963336235643163653834303237366363633330613639353332616535
-61323265346161313561396435643630383435633466636639316665353366343266306232663439
-32653662666631636662323833646562353861333365303430326130633832313162316337643063
-34393038646164363837306461303634653464343232663139363237353939323137333238613537
-34616139376632373931343437316337383332343162363939633634303831353336333433633935
-39343439313865343036376634653863653137653131363866363761653065313738316663383239
-38383232626262303463393164326366633138313932366530663136616233313831303337316537
-38343661663264303337346161323135366164373039663934343261663237396563383239663262
-39383765643765396331333938666335663862303962353562663361323764643031373431306664
-61646561353732323765363034326362663239666634316639356232633934663130663431613236
-64336335653664646430653061313836386361316564353334336364626332303839363335343363
-65653931363331383834373364616538336135633936363533383461373931666666373863386438
-3231333561313139393966383964613731616139666536613536
+31323435376435353462306465366666306330303932626563666533616264343239336236653865
+6362303830313335363234663639323132313064383234650a613661353637613934666538653036
+65343031666139326139626634653861633161633565333539663339633435306664396564656564
+3361396638313836340a616539323161333465336162363063643832363231653037346234363533
+66313661343562643663623132333138663536643062323064353632666365653435623233643634
+61363034363562643733323164633130666133623034373639383663613661323065383230396265
+65366331636235633534636237383332643436336665663366653164313266353236363362383864
+32646131613136393863383031646163356463396662346231366365363436393961333462303434
+64313738393132636537653438393030623363373037343930636137373436646630303137393035
+33666238363039623266666538623634323165363262636565393134393530343137383965303662
+30336536646431633235623636316132613437306338646435383037633534306336393538666237
+39656330376339323464313134326165316163393965633964343136633035653934386633646133
+32386235373031383037613835373966356334383539383162393136616566376335366239323961
+38616334343238373361306561656635653334633561366139386465333462336161333637643234
+63393231353166376362323434396265303630663530373538373665326662386264383762623835
+37366234313930376366386336303963376232393066373133616332613439343430303664656137
+37646630373332636136653634346363323766376635343838336531613934626362393961656266
+65363333346664633261666132633166656665316537343464653436393662656565393136613839
+30633932333062653836616637366365333937366134613965623139323163316332333636646439
+64363238346430346437666363373463376630363539613365313231373237366666353737383264
+39633261623231323962383564346265386633646664386461646332646637613363613134333163
+65396362313139633436623839333032346436626633336165343665346133353330623833333530
+30623362373261633630356563346231616462666336316165376230623166306362623734376538
+36373039333538376634376534376431363462383764366462643436323964313361643732393964
+37633635346433653636333432653365636666633963363230646636393035613335336231303138
+32623735353465653230333264393137396265373866363666303264656135393464343633653063
+33396539393566363735663537626634326432643836393161373837643561653432393237386337
+32353962363964383363393833666165363334303462653736346538663762353133666165303034
+39313833353465316531643238353238316231303638373964363861623766326565316438313839
+38393735643635633431643634633436626335353462623766363663323132636263383235616237
+65326334346462316630376532656338306438303639353537373766333566376238646265663237
+65623366373766376636303033616566313131386564316135323436653563326464643662383534
+38393061346339346461323438656239336236646166343738663232373565393763313365363439
+61633639623661323132616138346433376134616334356534346332356635316333373931353265
+35346432653231353864363935383966333336303962646563336133613336353239646361376532
+31303830386335306237376366316663646136643462636162383134306431633065393338646139
+3063
diff --git a/ansible/inventory/group_vars/proxmox_hosts/vars.yml b/ansible/inventory/group_vars/proxmox_hosts/vars.yml
index 1fe0565..eb36729 100644
--- a/ansible/inventory/group_vars/proxmox_hosts/vars.yml
+++ b/ansible/inventory/group_vars/proxmox_hosts/vars.yml
@@ -15,3 +15,6 @@ wireguard_peers:
- name: fedora
public_key: "ivtchk9pxEwYwusMzmn7Uq89LFV3uB1I8iYto0EJuy4="
allowed_ips: 10.10.10.2/32
+ - name: phone
+ public_key: "72fMMJTC/Ew38E61as+oEjFKsgKk4DAK8FqDuQOAcEU="
+ allowed_ips: 10.10.10.3/32
diff --git a/ansible/playbooks/wireguard-README.md b/ansible/playbooks/wireguard-README.md
index d3c4c2f..6ea49a3 100644
--- a/ansible/playbooks/wireguard-README.md
+++ b/ansible/playbooks/wireguard-README.md
@@ -6,6 +6,7 @@
|--------|--------------|
| server | 10.10.10.1 |
| fedora | 10.10.10.2 |
+| phone | 10.10.10.3 |
Pick the next free IP (10.10.10.3, 10.10.10.4, …) for each new machine.
diff --git a/ansible/roles/jellyfin/defaults/main.yml b/ansible/roles/jellyfin/defaults/main.yml
index 8080c4e..01e5db1 100644
--- a/ansible/roles/jellyfin/defaults/main.yml
+++ b/ansible/roles/jellyfin/defaults/main.yml
@@ -13,6 +13,8 @@ jellyfin_pgid: "1000"
nvidia_driver_version: "580.159.03"
+qbittorrent_webui_password: "{{ vault_qbittorrent_password }}"
+
jellyfin_config_dirs:
- /containers/jellyfin/config
- /containers/qbittorrent/config
diff --git a/ansible/roles/jellyfin/tasks/configure_services.yml b/ansible/roles/jellyfin/tasks/configure_services.yml
new file mode 100644
index 0000000..3dd5c8d
--- /dev/null
+++ b/ansible/roles/jellyfin/tasks/configure_services.yml
@@ -0,0 +1,566 @@
+---
+# --- Media library directories ---
+
+- name: Create media library directories
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: directory
+ owner: "{{ jellyfin_puid }}"
+ group: "{{ jellyfin_pgid }}"
+ mode: "0755"
+ loop:
+ - /media/movies
+ - /media/tv
+
+# --- qBittorrent: pre-seed credentials on fresh install ---
+
+- name: Check if qBittorrent config exists
+ ansible.builtin.stat:
+ path: /containers/qbittorrent/config/qBittorrent/qBittorrent.conf
+ register: _qbt_conf
+
+- name: Deploy qBittorrent credential seed script
+ ansible.builtin.template:
+ src: qbittorrent_seed.py.j2
+ dest: /tmp/qbt_seed.py
+ mode: "0700"
+ when: not _qbt_conf.stat.exists
+
+- name: Pre-seed qBittorrent credentials
+ ansible.builtin.command:
+ cmd: python3 /tmp/qbt_seed.py
+ when: not _qbt_conf.stat.exists
+
+# --- Wait for services to be ready ---
+
+- name: Read Radarr config
+ ansible.builtin.slurp:
+ src: /containers/radarr/config/config.xml
+ register: _radarr_config
+
+- name: Read Sonarr config
+ ansible.builtin.slurp:
+ src: /containers/sonarr/config/config.xml
+ register: _sonarr_config
+
+- name: Read Prowlarr config
+ ansible.builtin.slurp:
+ src: /containers/prowlarr/config/config.xml
+ register: _prowlarr_config
+
+- name: Set API key facts
+ ansible.builtin.set_fact:
+ _radarr_key: "{{ (_radarr_config.content | b64decode | regex_search('(.*?)', '\\1'))[0] }}"
+ _sonarr_key: "{{ (_sonarr_config.content | b64decode | regex_search('(.*?)', '\\1'))[0] }}"
+ _prowlarr_key: "{{ (_prowlarr_config.content | b64decode | regex_search('(.*?)', '\\1'))[0] }}"
+
+- name: Wait for Radarr
+ ansible.builtin.uri:
+ url: "http://localhost:7878/api/v3/system/status"
+ headers:
+ X-Api-Key: "{{ _radarr_key }}"
+ register: _r
+ until: _r.status == 200
+ retries: 12
+ delay: 5
+
+- name: Wait for Sonarr
+ ansible.builtin.uri:
+ url: "http://localhost:8989/api/v3/system/status"
+ headers:
+ X-Api-Key: "{{ _sonarr_key }}"
+ register: _r
+ until: _r.status == 200
+ retries: 12
+ delay: 5
+
+- name: Wait for Prowlarr
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/system/status"
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ register: _r
+ until: _r.status == 200
+ retries: 12
+ delay: 5
+
+# --- Prowlarr: fix TPB season search ---
+# apibay.org fulltext indexes "s05e01" as a single token, not "s05". When Prowlarr
+# appends the season number to the query (the.boys.s05), apibay returns 0 results.
+# Removing season/ep from the tv-search capability makes Prowlarr search with just
+# the show title; Sonarr's parser then filters the results by season/episode.
+# The file is locked read-only so Prowlarr cannot revert it on definition refresh.
+
+- name: Wait for Prowlarr to write TPB definition file
+ ansible.builtin.stat:
+ path: /containers/prowlarr/config/Definitions/thepiratebay.yml
+ register: _tpb_def
+ until: _tpb_def.stat.exists
+ retries: 12
+ delay: 5
+
+- name: Fix TPB tv-search to not append season to query
+ ansible.builtin.lineinfile:
+ path: /containers/prowlarr/config/Definitions/thepiratebay.yml
+ regexp: '^\s+tv-search: \[q, season, ep\]'
+ line: ' tv-search: [q]'
+ register: _tpb_fix
+
+- name: Lock TPB definition read-only to prevent Prowlarr reverting the fix
+ ansible.builtin.file:
+ path: /containers/prowlarr/config/Definitions/thepiratebay.yml
+ mode: "0444"
+
+- name: Restart Prowlarr to apply TPB definition fix
+ ansible.builtin.command:
+ cmd: docker restart prowlarr
+ changed_when: true
+ when: _tpb_fix.changed
+
+- name: Wait for Prowlarr after restart
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/system/status"
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ register: _r
+ until: _r.status == 200
+ retries: 12
+ delay: 5
+ when: _tpb_fix.changed
+
+# --- Radarr: download client ---
+
+- name: Get Radarr download clients
+ ansible.builtin.uri:
+ url: "http://localhost:7878/api/v3/downloadclient"
+ headers:
+ X-Api-Key: "{{ _radarr_key }}"
+ register: _radarr_dc
+
+- name: Add qBittorrent to Radarr
+ ansible.builtin.uri:
+ url: "http://localhost:7878/api/v3/downloadclient"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _radarr_key }}"
+ body_format: json
+ body:
+ name: qBittorrent
+ enable: true
+ protocol: torrent
+ priority: 1
+ implementation: QBittorrent
+ configContract: QBittorrentSettings
+ fields:
+ - name: host
+ value: qbittorrent
+ - name: port
+ value: 5080
+ - name: useSsl
+ value: false
+ - name: username
+ value: admin
+ - name: password
+ value: "{{ qbittorrent_webui_password }}"
+ - name: movieCategory
+ value: radarr
+ tags: []
+ status_code: 201
+ when: _radarr_dc.json | selectattr('name', 'equalto', 'qBittorrent') | list | length == 0
+
+# --- Radarr: root folder ---
+
+- name: Get Radarr root folders
+ ansible.builtin.uri:
+ url: "http://localhost:7878/api/v3/rootfolder"
+ headers:
+ X-Api-Key: "{{ _radarr_key }}"
+ register: _radarr_rf
+
+- name: Add movies root folder to Radarr
+ ansible.builtin.uri:
+ url: "http://localhost:7878/api/v3/rootfolder"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _radarr_key }}"
+ body_format: json
+ body:
+ path: /downloads/movies
+ status_code: 201
+ when: _radarr_rf.json | selectattr('path', 'equalto', '/downloads/movies') | list | length == 0
+
+# --- Sonarr: download client ---
+
+- name: Get Sonarr download clients
+ ansible.builtin.uri:
+ url: "http://localhost:8989/api/v3/downloadclient"
+ headers:
+ X-Api-Key: "{{ _sonarr_key }}"
+ register: _sonarr_dc
+
+- name: Add qBittorrent to Sonarr
+ ansible.builtin.uri:
+ url: "http://localhost:8989/api/v3/downloadclient"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _sonarr_key }}"
+ body_format: json
+ body:
+ name: qBittorrent
+ enable: true
+ protocol: torrent
+ priority: 1
+ implementation: QBittorrent
+ configContract: QBittorrentSettings
+ fields:
+ - name: host
+ value: qbittorrent
+ - name: port
+ value: 5080
+ - name: useSsl
+ value: false
+ - name: username
+ value: admin
+ - name: password
+ value: "{{ qbittorrent_webui_password }}"
+ - name: tvCategory
+ value: sonarr
+ tags: []
+ status_code: 201
+ when: _sonarr_dc.json | selectattr('name', 'equalto', 'qBittorrent') | list | length == 0
+
+# --- Sonarr: root folder ---
+
+- name: Get Sonarr root folders
+ ansible.builtin.uri:
+ url: "http://localhost:8989/api/v3/rootfolder"
+ headers:
+ X-Api-Key: "{{ _sonarr_key }}"
+ register: _sonarr_rf
+
+- name: Add TV root folder to Sonarr
+ ansible.builtin.uri:
+ url: "http://localhost:8989/api/v3/rootfolder"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _sonarr_key }}"
+ body_format: json
+ body:
+ path: /downloads/tv
+ status_code: 201
+ when: _sonarr_rf.json | selectattr('path', 'equalto', '/downloads/tv') | list | length == 0
+
+# --- Prowlarr: apps ---
+
+- name: Get Prowlarr applications
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/applications"
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ register: _prowlarr_apps
+
+- name: Add Radarr to Prowlarr
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/applications"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: Radarr
+ syncLevel: fullSync
+ implementation: Radarr
+ configContract: RadarrSettings
+ fields:
+ - name: prowlarrUrl
+ value: "http://prowlarr:9696"
+ - name: baseUrl
+ value: "http://radarr:7878"
+ - name: apiKey
+ value: "{{ _radarr_key }}"
+ - name: syncCategories
+ value: [2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060]
+ tags: []
+ status_code: 201
+ when: _prowlarr_apps.json | selectattr('name', 'equalto', 'Radarr') | list | length == 0
+
+- name: Add Sonarr to Prowlarr
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/applications"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: Sonarr
+ syncLevel: fullSync
+ implementation: Sonarr
+ configContract: SonarrSettings
+ fields:
+ - name: prowlarrUrl
+ value: "http://prowlarr:9696"
+ - name: baseUrl
+ value: "http://sonarr:8989"
+ - name: apiKey
+ value: "{{ _sonarr_key }}"
+ - name: syncCategories
+ value: [5000, 5010, 5020, 5030, 5040, 5045, 5050, 5060, 5070, 5080]
+ tags: []
+ status_code: 201
+ when: _prowlarr_apps.json | selectattr('name', 'equalto', 'Sonarr') | list | length == 0
+
+# --- Prowlarr: FlareSolverr proxy ---
+
+- name: Get Prowlarr indexer proxies
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexerproxy"
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ register: _prowlarr_proxies
+
+- name: Add FlareSolverr proxy to Prowlarr
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexerproxy"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: FlareSolverr
+ implementation: FlareSolverr
+ configContract: FlareSolverrSettings
+ tags: [1]
+ fields:
+ - name: host
+ value: "http://flaresolverr:8191"
+ - name: requestTimeout
+ value: 60
+ status_code: 201
+ when: _prowlarr_proxies.json | selectattr('name', 'equalto', 'FlareSolverr') | list | length == 0
+
+# --- Prowlarr: indexers ---
+
+- name: Get Prowlarr indexers
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexer"
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ register: _prowlarr_indexers
+
+- name: Add YTS indexer
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexer"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: YTS
+ enable: true
+ appProfileId: 1
+ priority: 25
+ implementation: Cardigann
+ configContract: CardigannSettings
+ tags: []
+ fields:
+ - name: definitionFile
+ value: yts
+ - name: baseSettings.limitsUnit
+ value: 0
+ - name: torrentBaseSettings.preferMagnetUrl
+ value: false
+ - name: apiurl
+ value: movies-api.accel.li
+ status_code: 201
+ when: _prowlarr_indexers.json | selectattr('name', 'equalto', 'YTS') | list | length == 0
+
+- name: Add The Pirate Bay indexer
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexer"
+ method: POST
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: The Pirate Bay
+ enable: true
+ appProfileId: 1
+ priority: 25
+ implementation: Cardigann
+ configContract: CardigannSettings
+ tags: []
+ fields:
+ - name: definitionFile
+ value: thepiratebay
+ - name: baseSettings.limitsUnit
+ value: 0
+ - name: torrentBaseSettings.preferMagnetUrl
+ value: false
+ - name: apiurl
+ value: apibay.org
+ status_code: 201
+ when: _prowlarr_indexers.json | selectattr('name', 'equalto', 'The Pirate Bay') | list | length == 0
+
+- name: Add EZTV indexer
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexer"
+ method: POST
+ timeout: 90
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: EZTV
+ enable: true
+ appProfileId: 1
+ priority: 25
+ implementation: Cardigann
+ configContract: CardigannSettings
+ tags: [1]
+ fields:
+ - name: definitionFile
+ value: eztv
+ - name: baseSettings.limitsUnit
+ value: 0
+ - name: torrentBaseSettings.preferMagnetUrl
+ value: false
+ status_code: 201
+ when: _prowlarr_indexers.json | selectattr('name', 'equalto', 'EZTV') | list | length == 0
+ ignore_errors: true
+
+- name: Add 1337x indexer
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/indexer"
+ method: POST
+ timeout: 90
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ body_format: json
+ body:
+ name: 1337x
+ enable: true
+ appProfileId: 1
+ priority: 25
+ implementation: Cardigann
+ configContract: CardigannSettings
+ tags: [1]
+ fields:
+ - name: definitionFile
+ value: 1337x
+ - name: baseSettings.limitsUnit
+ value: 0
+ - name: torrentBaseSettings.preferMagnetUrl
+ value: false
+ - name: downloadlink
+ value: 0
+ - name: downloadlink2
+ value: 1
+ - name: sort
+ value: 2
+ - name: type
+ value: 1
+ status_code: 201
+ when: _prowlarr_indexers.json | selectattr('name', 'equalto', '1337x') | list | length == 0
+ ignore_errors: true
+
+- name: Sync Prowlarr indexers to apps
+ ansible.builtin.uri:
+ url: "http://localhost:9696/api/v1/applications/syncindexers"
+ method: GET
+ headers:
+ X-Api-Key: "{{ _prowlarr_key }}"
+ status_code: [200, 202]
+ ignore_errors: true
+
+# --- Jellyseerr: connect Sonarr and Radarr ---
+
+- name: Check if Jellyseerr is initialized
+ ansible.builtin.stat:
+ path: /containers/jellyseerr/config/settings.json
+ register: _js_settings
+
+- name: Read Jellyseerr settings
+ ansible.builtin.slurp:
+ src: /containers/jellyseerr/config/settings.json
+ register: _js_raw
+ when: _js_settings.stat.exists
+
+- name: Set Jellyseerr API key fact
+ ansible.builtin.set_fact:
+ _js_key: "{{ (_js_raw.content | b64decode | from_json).main.apiKey }}"
+ when: _js_settings.stat.exists
+
+- name: Get Jellyseerr Sonarr servers
+ ansible.builtin.uri:
+ url: "http://localhost:5055/api/v1/settings/sonarr"
+ headers:
+ X-Api-Key: "{{ _js_key }}"
+ register: _js_sonarr
+ when: _js_settings.stat.exists
+
+- name: Configure Sonarr in Jellyseerr
+ ansible.builtin.uri:
+ url: "http://localhost:5055/api/v1/settings/sonarr/{{ '0' if (_js_sonarr.json | length == 0) else (_js_sonarr.json[0].id | string) }}"
+ method: "{{ 'POST' if (_js_sonarr.json | length == 0) else 'PUT' }}"
+ headers:
+ X-Api-Key: "{{ _js_key }}"
+ body_format: json
+ body:
+ name: sonarr
+ hostname: sonarr
+ port: 8989
+ apiKey: "{{ _sonarr_key }}"
+ useSsl: false
+ baseUrl: ""
+ activeProfileId: 4
+ activeProfileName: HD-1080p
+ activeDirectory: /downloads/tv
+ tags: []
+ animeTags: []
+ is4k: false
+ isDefault: true
+ enableSeasonFolders: true
+ syncEnabled: false
+ preventSearch: false
+ tagRequests: false
+ status_code: [200, 201]
+ when: >
+ _js_settings.stat.exists and
+ (_js_sonarr.json | length == 0 or not _js_sonarr.json[0].isDefault)
+
+- name: Get Jellyseerr Radarr servers
+ ansible.builtin.uri:
+ url: "http://localhost:5055/api/v1/settings/radarr"
+ headers:
+ X-Api-Key: "{{ _js_key }}"
+ register: _js_radarr
+ when: _js_settings.stat.exists
+
+- name: Configure Radarr in Jellyseerr
+ ansible.builtin.uri:
+ url: "http://localhost:5055/api/v1/settings/radarr/{{ '0' if (_js_radarr.json | length == 0) else (_js_radarr.json[0].id | string) }}"
+ method: "{{ 'POST' if (_js_radarr.json | length == 0) else 'PUT' }}"
+ headers:
+ X-Api-Key: "{{ _js_key }}"
+ body_format: json
+ body:
+ name: radarr
+ hostname: radarr
+ port: 7878
+ apiKey: "{{ _radarr_key }}"
+ useSsl: false
+ baseUrl: ""
+ activeProfileId: 4
+ activeProfileName: HD-1080p
+ activeDirectory: /downloads/movies
+ is4k: false
+ minimumAvailability: released
+ tags: []
+ isDefault: true
+ syncEnabled: false
+ preventSearch: false
+ tagRequests: false
+ status_code: [200, 201]
+ when: >
+ _js_settings.stat.exists and
+ (_js_radarr.json | length == 0 or not _js_radarr.json[0].isDefault)
diff --git a/ansible/roles/jellyfin/tasks/main.yml b/ansible/roles/jellyfin/tasks/main.yml
index 96081c5..7e624ab 100644
--- a/ansible/roles/jellyfin/tasks/main.yml
+++ b/ansible/roles/jellyfin/tasks/main.yml
@@ -152,3 +152,6 @@
enabled: true
state: started
daemon_reload: true
+
+- name: Configure services
+ ansible.builtin.include_tasks: configure_services.yml
diff --git a/ansible/roles/jellyfin/templates/qbittorrent_seed.py.j2 b/ansible/roles/jellyfin/templates/qbittorrent_seed.py.j2
new file mode 100644
index 0000000..e17af2a
--- /dev/null
+++ b/ansible/roles/jellyfin/templates/qbittorrent_seed.py.j2
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+import hashlib, os, base64
+
+password = "{{ qbittorrent_webui_password }}"
+salt = os.urandom(16)
+dk = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000)
+pw_hash = "@ByteArray(" + base64.b64encode(salt).decode() + ":" + base64.b64encode(dk).decode() + ")"
+
+os.makedirs("/containers/qbittorrent/config/qBittorrent", exist_ok=True)
+with open("/containers/qbittorrent/config/qBittorrent/qBittorrent.conf", "w") as f:
+ f.write("[Preferences]\n")
+ f.write('WebUI\\Password_PBKDF2="{}"\n'.format(pw_hash))
+ f.write("WebUI\\Port=5080\n")
+ f.write("WebUI\\Username=admin\n")
+ f.write("WebUI\\LocalHostAuth=false\n")
+ f.write("WebUI\\ServerDomains=*\n")
+ f.write("WebUI\\Address=*\n")
diff --git a/ansible/roles/pihole/defaults/main.yml b/ansible/roles/pihole/defaults/main.yml
index 0fabb99..48e942c 100644
--- a/ansible/roles/pihole/defaults/main.yml
+++ b/ansible/roles/pihole/defaults/main.yml
@@ -11,7 +11,7 @@ pihole_dns_hosts:
- { ip: "10.10.1.3", host: "grafana.homelab" }
- { ip: "10.10.1.3", host: "proxmox.homelab" }
- { ip: "10.10.1.3", host: "adyrem.duckdns.org" }
- - { ip: "10.10.1.3", host: "mtg.homelab" }
+ - { ip: "10.10.1.3", host: "devbox.homelab" }
- { ip: "10.10.1.3", host: "jellyfin.homelab" }
- { ip: "10.10.1.3", host: "jellyseerr.homelab" }
- { ip: "10.10.1.3", host: "radarr.homelab" }
diff --git a/ansible/roles/pihole/templates/update_hosts.py.j2 b/ansible/roles/pihole/templates/update_hosts.py.j2
index fa3d8db..502038b 100644
--- a/ansible/roles/pihole/templates/update_hosts.py.j2
+++ b/ansible/roles/pihole/templates/update_hosts.py.j2
@@ -7,15 +7,18 @@ hosts = [
{% endfor %}
]
-hosts_str = "[" + ", ".join('"' + h + '"' for h in hosts) + "]"
+hosts_str = "[\n" + "".join(' "' + h + '",\n' for h in hosts) + " ]"
with open("/etc/pihole/pihole.toml", "r") as f:
content = f.read()
+# Only replace the first hosts= (DNS custom records section, not DHCP leases)
content = re.sub(
- r"(?m)^( *hosts\s*=\s*)\[.*\]",
+ r"( *hosts\s*=\s*)\[.*?\]",
r"\g<1>" + hosts_str,
content,
+ count=1,
+ flags=re.DOTALL,
)
with open("/etc/pihole/pihole.toml", "w") as f:
diff --git a/ansible/roles/proxmox_host/files/pve_firewall.py b/ansible/roles/proxmox_host/files/pve_firewall.py
index c51227a..5da4726 100644
--- a/ansible/roles/proxmox_host/files/pve_firewall.py
+++ b/ansible/roles/proxmox_host/files/pve_firewall.py
@@ -10,6 +10,7 @@ DESIRED = [
{'proto': 'tcp', 'source': '10.10.10.0/24', 'dport': '2222'},
{'proto': 'tcp', 'source': '192.168.1.0/24', 'dport': '8006'},
{'proto': 'tcp', 'source': '10.10.10.0/24', 'dport': '8006'},
+ {'proto': 'tcp', 'source': '10.10.1.3', 'dport': '8006'},
{'proto': 'tcp', 'source': '192.168.1.0/24', 'dport': '3128'},
{'proto': 'udp', 'source': '10.10.0.0/16', 'dport': '53'},
{'proto': 'tcp', 'source': '10.10.0.0/16', 'dport': '53'},
diff --git a/ansible/roles/traefik/defaults/main.yml b/ansible/roles/traefik/defaults/main.yml
index bc8ee3a..28e135b 100644
--- a/ansible/roles/traefik/defaults/main.yml
+++ b/ansible/roles/traefik/defaults/main.yml
@@ -13,6 +13,10 @@ traefik_services:
public_host: "adyrem.duckdns.org"
public_path: "/git"
strip_prefix: "/git"
+ - name: devbox
+ host: "devbox.homelab"
+ backend: "http://10.10.2.10:4001"
+ public_host: "adyrem.duckdns.org"
- name: mtg
host: "mtg.homelab"
backend: "http://10.10.2.10:3000"
diff --git a/ansible/roles/traefik/tasks/main.yml b/ansible/roles/traefik/tasks/main.yml
index 02e3e92..4e2d3f8 100644
--- a/ansible/roles/traefik/tasks/main.yml
+++ b/ansible/roles/traefik/tasks/main.yml
@@ -60,6 +60,41 @@
when: traefik_version not in (traefik_installed_version.stdout | default(''))
notify: Restart traefik
+- name: Create traefik certs directory
+ ansible.builtin.file:
+ path: /etc/traefik/certs
+ state: directory
+ owner: traefik
+ group: traefik
+ mode: "0750"
+
+- name: Copy homelab TLS certificate
+ ansible.builtin.copy:
+ src: "{{ playbook_dir }}/../../certs/wildcard.homelab.pem"
+ dest: /etc/traefik/certs/homelab.pem
+ owner: traefik
+ group: traefik
+ mode: "0640"
+ notify: Restart traefik
+
+- name: Copy homelab TLS key
+ ansible.builtin.copy:
+ src: "{{ playbook_dir }}/../../certs/wildcard.homelab-key.pem"
+ dest: /etc/traefik/certs/homelab-key.pem
+ owner: traefik
+ group: traefik
+ mode: "0640"
+ notify: Restart traefik
+
+- name: Write Traefik TLS config
+ ansible.builtin.template:
+ src: tls.yml.j2
+ dest: /etc/traefik/conf.d/tls.yml
+ owner: traefik
+ group: traefik
+ mode: "0640"
+ notify: Restart traefik
+
- name: Write Traefik static config
ansible.builtin.template:
src: traefik.yml.j2
diff --git a/ansible/roles/traefik/templates/tls.yml.j2 b/ansible/roles/traefik/templates/tls.yml.j2
new file mode 100644
index 0000000..66f5814
--- /dev/null
+++ b/ansible/roles/traefik/templates/tls.yml.j2
@@ -0,0 +1,6 @@
+tls:
+ stores:
+ default:
+ defaultCertificate:
+ certFile: /etc/traefik/certs/homelab.pem
+ keyFile: /etc/traefik/certs/homelab-key.pem