me like nix
1{ inputs, ... }:
2
3let
4 jellyfinKodiSyncQueue = inputs.nixpkgs.legacyPackages.x86_64-linux.fetchzip {
5 url = "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_15.0.0.0.zip";
6 stripRoot = false;
7 hash = "sha256-xtlG3UQ/WClt/Hvxe+oId2CeJ+PWMDXBUJXh5+k+mZQ=";
8 };
9in
10{
11 flake.modules.nixos.media-server =
12 { pkgs, config, lib, ... }:
13 {
14 services.flaresolverr.enable = true;
15
16 services.immich = {
17 enable = true;
18 mediaLocation = "/mnt/storage2/immich";
19 host = "0.0.0.0";
20 openFirewall = true;
21 };
22
23 # Immich uses PostgreSQL via Unix socket; avoid conflicting with lbdt-postgres on TCP/5432.
24 services.postgresql.settings.listen_addresses = lib.mkForce "";
25
26 systemd.tmpfiles.rules = [
27 "d /mnt/storage2/immich 0750 immich immich -"
28 ];
29
30 # transmission.service bind-mounts these paths before ExecStartPre runs, while
31 # systemd-tmpfiles refuses to create them because /mnt/storage1 is user-owned.
32 # Create them in a small unsandboxed dependency before Transmission starts.
33 systemd.services.transmission-media-dirs = {
34 description = "Create Transmission media directories";
35 before = [ "transmission.service" ];
36 requiredBy = [ "transmission.service" ];
37 serviceConfig = {
38 Type = "oneshot";
39 RemainAfterExit = true;
40 };
41 script = ''
42 install -d -o transmission -g media -m 0755 \
43 /mnt/storage1/nixarr/media/torrents \
44 /mnt/storage1/nixarr/media/torrents/.incomplete \
45 /mnt/storage1/nixarr/media/torrents/.watch
46 '';
47 };
48 systemd.services.transmission = {
49 requires = [ "transmission-media-dirs.service" ];
50 after = [ "transmission-media-dirs.service" ];
51 };
52
53 # Keep manually-created Sonarr/Radarr SABnzbd download clients in sync with
54 # SABnzbd's generated API key. This avoids storing the SABnzbd API key in Nix
55 # while repairing clients after the settings-backed SABnzbd config rewrite.
56 systemd.services.sync-sabnzbd-download-client-keys = {
57 description = "Sync SABnzbd API key into Arr download clients";
58 after = [ "sabnzbd.service" "sonarr.service" "radarr.service" ];
59 wants = [ "sabnzbd.service" "sonarr.service" "radarr.service" ];
60 wantedBy = [ "multi-user.target" ];
61 serviceConfig = {
62 Type = "oneshot";
63 User = "root";
64 };
65 path = [ pkgs.python3 ];
66 script = ''
67 python3 ${pkgs.writeText "sync-sabnzbd-download-client-keys.py" ''
68 import json
69 import urllib.error
70 import urllib.request
71 from pathlib import Path
72
73 def read_key(path):
74 for line in Path(path).read_text().splitlines():
75 if line.strip().startswith("api_key"):
76 return line.split("=", 1)[1].strip()
77 raise RuntimeError(f"api_key not found in {path}")
78
79 sab_key = read_key("/var/lib/sabnzbd/sabnzbd.ini")
80
81 services = [
82 ("Sonarr", "http://127.0.0.1:8989", "/data/.state/nixarr/secrets/sonarr.api-key"),
83 ("Radarr", "http://127.0.0.1:7878", "/data/.state/nixarr/secrets/radarr.api-key"),
84 ]
85
86 def request(base, api_key, method, path, data=None):
87 body = None if data is None else json.dumps(data).encode()
88 req = urllib.request.Request(
89 base + path,
90 data=body,
91 method=method,
92 headers={"X-Api-Key": api_key, "Content-Type": "application/json"},
93 )
94 with urllib.request.urlopen(req, timeout=10) as resp:
95 raw = resp.read()
96 return None if not raw else json.loads(raw)
97
98 for name, base, key_file in services:
99 try:
100 api_key = Path(key_file).read_text().strip()
101 clients = request(base, api_key, "GET", "/api/v3/downloadclient")
102 changed = 0
103 for client in clients:
104 if (client.get("implementation") or "").lower() != "sabnzbd" and (client.get("name") or "").lower() != "sabnzbd":
105 continue
106 for field in client.get("fields", []):
107 if field.get("name") == "apiKey" and field.get("value") != sab_key:
108 field["value"] = sab_key
109 changed += 1
110 request(base, api_key, "PUT", f"/api/v3/downloadclient/{client['id']}", client)
111 print(f"{name}: updated {changed} SABnzbd apiKey field(s)")
112 except Exception as e:
113 print(f"{name}: failed to sync SABnzbd API key: {e}")
114 raise
115 ''}
116 '';
117 };
118
119 # nixarr configures SABnzbd through services.sabnzbd.settings on newer nixpkgs,
120 # but NixOS keeps the legacy configFile default when system.stateVersion < 26.05,
121 # causing those settings to be ignored. Force the new settings-backed path.
122 services.sabnzbd.configFile = lib.mkForce null;
123
124 age.secrets.wireguard.file = ../secrets/wireguard.age;
125
126 nixarr = {
127 enable = true;
128 mediaDir = "/mnt/storage1/nixarr/media";
129 vpn = {
130 enable = true;
131 wgConf = config.age.secrets.wireguard.path;
132 };
133
134 jellyfin = {
135 enable = true;
136 openFirewall = true;
137 };
138
139 transmission = {
140 enable = true;
141 vpn.enable = true;
142 peerPort = 51413;
143 };
144 sabnzbd = {
145 enable = true;
146 vpn.enable = true;
147 openFirewall = true;
148 };
149
150 prowlarr.enable = true;
151 radarr.enable = true;
152 sonarr.enable = true;
153 seerr = {
154 enable = true;
155 openFirewall = true;
156 };
157
158 recyclarr = {
159 enable = true;
160 configuration = {
161 sonarr = {
162 series = {
163 base_url = "http://localhost:8989";
164 api_key = "!env_var SONARR_API_KEY";
165 quality_definition = {
166 type = "series";
167 };
168 delete_old_custom_formats = true;
169 custom_formats = [
170 {
171 trash_ids = [
172 "85c61753df5da1fb2aab6f2a47426b09"
173 "9c11cd3f07101cdba90a2d81cf0e56b4"
174 ];
175 assign_scores_to = [
176 {
177 name = "WEB-DL (1080p)";
178 score = -10000;
179 }
180 ];
181 }
182 ];
183 };
184 };
185 radarr = {
186 movies = {
187 base_url = "http://localhost:7878";
188 api_key = "!env_var RADARR_API_KEY";
189 quality_definition = {
190 type = "movie";
191 };
192 delete_old_custom_formats = true;
193 custom_formats = [
194 {
195 trash_ids = [
196 "570bc9ebecd92723d2d21500f4be314c"
197 "eca37840c13c6ef2dd0262b141a5482f"
198 ];
199 assign_scores_to = [
200 {
201 name = "HD Bluray + WEB";
202 score = 25;
203 }
204 ];
205 }
206 ];
207 };
208 };
209 };
210 };
211 };
212
213 # Avoid jellyfin blocking shutdown for 2 minutes
214 systemd.services.jellyfin.serviceConfig.TimeoutStopSec = 10;
215
216 # Install Kodi Sync Queue plugin into Jellyfin
217 systemd.services.jellyfin.serviceConfig.ExecStartPre =
218 let
219 pluginDir = "/data/.state/nixarr/jellyfin/data/plugins/Kodi Sync Queue/15.0.0.0";
220 in
221 pkgs.writeShellScript "install-jellyfin-plugins" ''
222 mkdir -p "${pluginDir}"
223 cp -f ${jellyfinKodiSyncQueue}/*.dll ${jellyfinKodiSyncQueue}/meta.json "${pluginDir}/"
224 '';
225 };
226}