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