forked from
lass.pds.lassul.us/superconfig
This repository has no description
14 kB
468 lines
1{ config, pkgs, ... }:
2{
3 # sabnzbd hard-depends on cheetah3, whose PyPI dist is `ct3` while nixpkgs
4 # sets pname="cheetah3". pythonMetadataCheckPhase then looks up metadata for
5 # "cheetah3", finds only "ct3", and fails the build with PackageNotFoundError.
6 # Skip that spurious check until nixpkgs fixes the pname/dist mismatch.
7 nixpkgs.overlays = [
8 (_final: prev: {
9 pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [
10 (_pyfinal: pyprev: {
11 cheetah3 = pyprev.cheetah3.overridePythonAttrs (_: {
12 dontCheckPythonMetadata = true;
13 });
14 })
15 ];
16 })
17 ];
18 users.users.download = {
19 isSystemUser = true;
20 uid = 1001;
21 group = "download";
22 };
23 users.groups.download.members = [
24 "transmission"
25 "sabnzbd"
26 ];
27 services.transmission = {
28 enable = true;
29 package = pkgs.transmission_4;
30 home = "/var/state/transmission";
31 group = "download";
32 downloadDirPermissions = "775";
33 settings = {
34 download-dir = "/var/download/transmission";
35 incomplete-dir-enabled = false;
36 rpc-bind-address = "::";
37 message-level = 1;
38 umask = 18;
39 rpc-whitelist-enabled = false;
40 rpc-host-whitelist-enabled = false;
41 # stop seeding after reaching 5.0 ratio
42 ratio-limit = 5;
43 ratio-limit-enabled = true;
44 # limit concurrent seeding to reduce resource usage
45 seed-queue-enabled = true;
46 seed-queue-size = 20;
47 };
48 };
49
50 # garbage collection for old torrents
51 systemd.services.transmission-gc = {
52 description = "Remove old completed torrents from Transmission";
53 after = [ "transmission.service" ];
54 path = [
55 pkgs.transmission_4
56 pkgs.jq
57 pkgs.coreutils
58 pkgs.gawk
59 pkgs.gnugrep
60 pkgs.findutils
61 ];
62 script = ''
63 set -efu
64 TRANSMISSION_HOST="128.0.0.1:9091"
65 MAX_AGE_DAYS=30
66 MIN_RATIO=5.0
67
68 # get list of all torrents as JSON
69 torrents=$(transmission-remote "$TRANSMISSION_HOST" -l 2>/dev/null | tail -n +2 | head -n -1 || true)
70
71 if [ -z "$torrents" ]; then
72 echo "No torrents found"
73 exit 0
74 fi
75
76 # process each torrent
77 echo "$torrents" | while read -r line; do
78 id=$(echo "$line" | awk '{print $1}' | tr -d '*')
79 # skip if not a valid ID
80 [ -z "$id" ] || [ "$id" = "ID" ] && continue
81
82 # get torrent info
83 info=$(transmission-remote "$TRANSMISSION_HOST" -t "$id" -i 2>/dev/null || continue)
84
85 # extract ratio and completion status
86 ratio=$(echo "$info" | grep "Ratio:" | awk '{print $2}')
87 percent=$(echo "$info" | grep "Percent Done:" | awk '{print $3}' | tr -d '%')
88 state=$(echo "$info" | grep "State:" | cut -d: -f2- | xargs)
89
90 # skip if not 100% complete
91 [ "$percent" != "100" ] && continue
92
93 # check if ratio reached
94 ratio_reached=false
95 if [ -n "$ratio" ] && [ "$ratio" != "None" ]; then
96 if awk "BEGIN {exit !($ratio >= $MIN_RATIO)}"; then
97 ratio_reached=true
98 fi
99 fi
100
101 # check age via date added
102 date_added=$(echo "$info" | grep "Date added:" | cut -d: -f2- | xargs)
103 if [ -n "$date_added" ]; then
104 added_epoch=$(date -d "$date_added" +%s 2>/dev/null || echo 0)
105 now_epoch=$(date +%s)
106 age_days=$(( (now_epoch - added_epoch) / 86400 ))
107 else
108 age_days=0
109 fi
110
111 # remove if ratio reached OR older than max age
112 if [ "$ratio_reached" = "true" ]; then
113 echo "Removing torrent $id (ratio: $ratio >= $MIN_RATIO)"
114 transmission-remote "$TRANSMISSION_HOST" -t "$id" --remove-and-delete
115 elif [ "$age_days" -ge "$MAX_AGE_DAYS" ]; then
116 echo "Removing torrent $id (age: $age_days days >= $MAX_AGE_DAYS)"
117 transmission-remote "$TRANSMISSION_HOST" -t "$id" --remove-and-delete
118 fi
119 done
120 '';
121 serviceConfig = {
122 Type = "oneshot";
123 User = "transmission";
124 };
125 };
126
127 systemd.timers.transmission-gc = {
128 wantedBy = [ "timers.target" ];
129 timerConfig = {
130 OnCalendar = "daily";
131 Persistent = true;
132 RandomizedDelaySec = "1h";
133 };
134 };
135 systemd.services.transmission-watcher = {
136 wantedBy = [ "multi-user.target" ];
137 startAt = "*:0/5";
138 path = [
139 pkgs.curl
140 pkgs.systemdMinimal
141 ];
142 script = ''
143 set -efu -o pipefail
144 # don't restart if transmission is currently starting or stopping
145 state=$(systemctl show -p ActiveState --value transmission.service)
146 if [ "$state" = "activating" ] || [ "$state" = "deactivating" ]; then
147 echo "transmission is $state, skipping"
148 exit 0
149 fi
150 # check if transmission responds (with timeout)
151 if ! curl -SsfL --max-time 10 http://transmission.r; then
152 echo "transmission not responding, restarting"
153 systemctl restart transmission.service
154 fi
155 '';
156 };
157
158 # pause downloads when /var/download runs low, so a full disk can no longer
159 # hard-wedge sabnzbd with an unrecoverable "Disk full! Forcing Pause".
160 # sabnzbd pauses itself once free disk drops below download_free/complete_free
161 # (auto-resuming above); this guard keeps those set to THRESH GiB. Transmission
162 # has no equivalent, so below the threshold the guard stops all its torrents
163 # (restarting them on recovery) and posts a Matrix alert on each transition.
164 systemd.services.download-space-guard = {
165 wantedBy = [ "multi-user.target" ];
166 after = [ "sabnzbd.service" ];
167 startAt = "*:0/5";
168 path = [
169 pkgs.curl
170 pkgs.coreutils
171 pkgs.gnugrep
172 pkgs.gawk
173 pkgs.jq
174 pkgs.transmission_4
175 ];
176 script = ''
177 set -efu
178 DIR=/var/download
179 THRESH=100
180 # !MRjkvAaFPsjdYCpeiZ:lassul.us, URL-encoded for the API path
181 ROOM=%21MRjkvAaFPsjdYCpeiZ%3Alassul.us
182 STATE="$STATE_DIRECTORY/state"
183
184 avail=$(df -Pk "$DIR" | awk 'NR==2 {print int($4 / 1024 / 1024)}')
185
186 INI=/var/lib/sabnzbd/sabnzbd.ini
187 key=$(grep -E '^api_key' "$INI" | head -1 | cut -d= -f2 | tr -d ' ')
188 sab() {
189 curl -fsS --max-time 15 "http://127.0.0.1:8080/api?output=json&apikey=$key&$1" >/dev/null || :
190 }
191 tr_remote() {
192 transmission-remote 128.0.0.1:9091 "$@" >/dev/null || :
193 }
194 notify() {
195 token=$(cat "$CREDENTIALS_DIRECTORY/matrix-token")
196 body=$(jq -nc --arg b "$1" '{msgtype: "m.text", body: $b}')
197 curl -fsS --max-time 15 -X POST \
198 -H "Authorization: Bearer $token" -H 'Content-Type: application/json' \
199 "https://matrix.lassul.us/_matrix/client/v3/rooms/$ROOM/send/m.room.message" \
200 --data "$body" >/dev/null || :
201 }
202
203 # sabnzbd pauses continuously once free disk drops below these (and
204 # auto-resumes above). Keep them at THRESH GiB; re-assert only if changed.
205 for k in download_free complete_free; do
206 grep -qxF "$k = ''${THRESH}G" "$INI" \
207 || sab "mode=set_config§ion=misc&keyword=$k&value=''${THRESH}G"
208 done
209
210 prev=ok
211 if [ -f "$STATE" ]; then prev=$(cat "$STATE"); fi
212 if [ "$avail" -lt "$THRESH" ]; then cur=low; else cur=ok; fi
213
214 if [ "$cur" = low ]; then
215 # stop everything each run so newly-grabbed torrents can't fill the disk
216 tr_remote -t all --stop
217 if [ "$prev" != low ]; then
218 echo "free ''${avail}G < ''${THRESH}G: stopped transmission, alerting"
219 notify "⚠️ yellow: /var/download down to ''${avail} GiB free (< ''${THRESH} GiB). Stopped all transmission torrents; sabnzbd auto-paused via download_free."
220 fi
221 elif [ "$prev" = low ]; then
222 echo "free ''${avail}G >= ''${THRESH}G: resumed transmission, alerting"
223 tr_remote -t all --start
224 notify "✅ yellow: /var/download recovered to ''${avail} GiB free. Restarted transmission torrents."
225 fi
226 echo "$cur" > "$STATE"
227 '';
228 serviceConfig = {
229 Type = "oneshot";
230 User = "root";
231 StateDirectory = "download-space-guard";
232 LoadCredential = [
233 "matrix-token:${config.clan.core.vars.generators.archiver-matrix.files."matrix-access-token".path}"
234 ];
235 };
236 };
237
238 security.acme.defaults.email = "spam@krebsco.de";
239 security.acme.acceptTerms = true;
240 security.acme.certs."yellow.r".server = config.krebs.ssl.acmeURL;
241 security.acme.certs."jelly.r".server = config.krebs.ssl.acmeURL;
242 security.acme.certs."radar.r".server = config.krebs.ssl.acmeURL;
243 security.acme.certs."sonar.r".server = config.krebs.ssl.acmeURL;
244 security.acme.certs."transmission.r".server = config.krebs.ssl.acmeURL;
245 services.nginx = {
246 enable = true;
247 package = pkgs.nginx.override {
248 modules = with pkgs.nginxModules; [
249 fancyindex
250 ];
251 };
252 virtualHosts."yellow.r" = {
253 serverAliases = [ "flix.r" ];
254 default = true;
255 enableACME = true;
256 addSSL = true;
257 locations."/" = {
258 root = "/var/download";
259 extraConfig = ''
260 fancyindex on;
261 include ${pkgs.nginx}/conf/mime.types;
262 include ${pkgs.writeText "extrMime" ''
263 types {
264 video/webm mkv;
265 }
266 ''};
267 create_full_put_path on;
268 '';
269 };
270
271 };
272 virtualHosts."jelly.r" = {
273 enableACME = true;
274 addSSL = true;
275 locations."/".extraConfig = ''
276 proxy_pass http://localhost:8096/;
277 proxy_set_header Accept-Encoding "";
278 '';
279 };
280 virtualHosts."transmission.r" = {
281 enableACME = true;
282 addSSL = true;
283 locations."/" = {
284 proxyWebsockets = true;
285 proxyPass = "http://128.0.0.1:9091";
286 };
287 };
288 virtualHosts."radar.r" = {
289 enableACME = true;
290 addSSL = true;
291 locations."/" = {
292 proxyWebsockets = true;
293 proxyPass = "http://localhost:7878";
294 };
295 };
296 virtualHosts."sonar.r" = {
297 enableACME = true;
298 addSSL = true;
299 locations."/" = {
300 proxyWebsockets = true;
301 proxyPass = "http://localhost:8989";
302 # sonar.r now has forced authentication
303 extraConfig = ''
304 proxy_set_header Authorization "Basic a3JlYnM6YWlkc2JhbGxz";
305 '';
306 };
307 };
308 };
309
310 services.samba = {
311 enable = true;
312 enableNmbd = false;
313 settings.global = {
314 "hosts allow" = "42::/16 10.243.0.0/16 10.244.0.0/16 fdcc:c5da:5295:c853:d499::/80";
315 "use sendfile" = "true";
316 "disable netbios" = "true";
317 "mangled names" = "illegal";
318 "socket options" = "TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=65536 SO_SNDBUF=65536";
319 "load printers" = "false";
320 "disable spoolss" = "true";
321 "printcap name" = "/dev/null";
322 "map to guest" = "Bad User";
323 "max log size" = "50";
324 "dns proxy" = "no";
325 "security" = "user";
326 "syslog only" = "yes";
327 };
328 shares.public = {
329 comment = "Warez";
330 path = "/var/download";
331 public = "yes";
332 "only guest" = "yes";
333 "create mask" = "0644";
334 "directory mask" = "2777";
335 writable = "no";
336 printable = "no";
337 };
338 };
339
340 networking.firewall.allowedTCPPorts = [
341 80 # nginx
342 443 # nginx
343 9091 # transmission web
344 8096 # jellyfin
345 8920 # jellyfin
346 51413 # transmission traffic
347 445 # smbd
348 111 # smbd
349 2049 # smbd
350 4000 # smbd
351 4001 # smbd
352 4002 # smbd
353 ];
354 networking.firewall.allowedUDPPorts = [
355 51413 # transmission traffic
356 1900 # jellyfin
357 7359 # jellyfin
358 111 # smbd
359 2049 # smbd
360 4000 # smbd
361 4001 # smbd
362 4002 # smbd
363 ];
364 krebs.iptables = {
365 enable = true;
366 tables.nat.PREROUTING.rules = [
367 # transmission rpc port
368 {
369 predicate = "-i retiolum -p tcp --dport 9091";
370 target = "DNAT --to-destination fdb4:3310:947::2";
371 v4 = false;
372 }
373 ];
374 tables.filter.FORWARD.policy = "ACCEPT"; # we need this so we can forward into the the transmission network namespace
375 };
376
377 systemd.services.flix-index = {
378 wantedBy = [ "multi-user.target" ];
379 path = [
380 pkgs.coreutils
381 pkgs.findutils
382 pkgs.inotify-tools
383 ];
384 startAt = "hourly";
385 serviceConfig = {
386 Type = "oneshot";
387 ExecStart = pkgs.writers.writeDash "flix-index" ''
388 set -efu
389 index(){
390 # /var/download churns constantly (download temp files appearing and
391 # vanishing), so find can exit non-zero when an entry disappears
392 # mid-scan. Ignore that — the entries we did enumerate are still valid
393 # — but only replace the live index if we actually produced one.
394 find . -type f > "$DIR"/index.tmp || :
395 if [ -s "$DIR"/index.tmp ]; then
396 mv "$DIR"/index.tmp "$DIR"/index
397 fi
398 }
399
400 DIR=/var/download
401 cd "$DIR"
402 index
403 '';
404 };
405 };
406
407 services.jellyfin = {
408 enable = true;
409 group = "download";
410 };
411
412 # request managment
413 services.jellyseerr = {
414 enable = true;
415 openFirewall = true;
416 };
417
418 # movies
419 services.radarr = {
420 enable = true;
421 openFirewall = true;
422 user = "download";
423 group = "download";
424 };
425
426 # shows
427 services.sonarr = {
428 enable = true;
429 openFirewall = true;
430 user = "download";
431 group = "download";
432 };
433 # sonarr needs unsecure packages
434 nixpkgs.config.permittedInsecurePackages = [
435 "aspnetcore-runtime-wrapped-6.0.36"
436 "aspnetcore-runtime-6.0.36"
437 "dotnet-sdk-wrapped-6.0.428"
438 "dotnet-sdk-6.0.428"
439 "olm-3.2.16"
440 ];
441
442 # indexers
443 services.prowlarr = {
444 enable = true;
445 openFirewall = true;
446 };
447
448 # flaresolverr for bypassing cloudflare on some indexers
449 services.flaresolverr = {
450 enable = true;
451 openFirewall = true;
452 };
453
454 # subtitles
455 services.bazarr = {
456 enable = true;
457 openFirewall = true;
458 user = "download";
459 group = "download";
460 };
461
462 # usenet download client
463 services.sabnzbd = {
464 enable = true;
465 group = "download";
466 openFirewall = true;
467 };
468}