This repository has no description
13 kB
468 lines
1{ config, pkgs, ... }:
2{
3 users.users.download = {
4 isSystemUser = true;
5 uid = 1001;
6 group = "download";
7 };
8 users.groups.download.members = [
9 "transmission"
10 "sabnzbd"
11 ];
12 services.transmission = {
13 enable = true;
14 package = pkgs.transmission_4;
15 home = "/var/state/transmission";
16 group = "download";
17 downloadDirPermissions = "775";
18 settings = {
19 download-dir = "/var/download/transmission";
20 incomplete-dir-enabled = false;
21 rpc-bind-address = "::";
22 message-level = 1;
23 umask = 18;
24 rpc-whitelist-enabled = false;
25 rpc-host-whitelist-enabled = false;
26 # stop seeding after reaching 5.0 ratio
27 ratio-limit = 5;
28 ratio-limit-enabled = true;
29 # limit concurrent seeding to reduce resource usage
30 seed-queue-enabled = true;
31 seed-queue-size = 20;
32 };
33 };
34
35 # garbage collection for old torrents
36 systemd.services.transmission-gc = {
37 description = "Remove old completed torrents from Transmission";
38 after = [ "transmission.service" ];
39 path = [
40 pkgs.transmission_4
41 pkgs.jq
42 pkgs.coreutils
43 pkgs.gawk
44 pkgs.gnugrep
45 pkgs.findutils
46 ];
47 script = ''
48 set -efu
49 TRANSMISSION_HOST="128.0.0.1:9091"
50 MAX_AGE_DAYS=30
51 MIN_RATIO=5.0
52
53 # get list of all torrents as JSON
54 torrents=$(transmission-remote "$TRANSMISSION_HOST" -l 2>/dev/null | tail -n +2 | head -n -1 || true)
55
56 if [ -z "$torrents" ]; then
57 echo "No torrents found"
58 exit 0
59 fi
60
61 # process each torrent
62 echo "$torrents" | while read -r line; do
63 id=$(echo "$line" | awk '{print $1}' | tr -d '*')
64 # skip if not a valid ID
65 [ -z "$id" ] || [ "$id" = "ID" ] && continue
66
67 # get torrent info
68 info=$(transmission-remote "$TRANSMISSION_HOST" -t "$id" -i 2>/dev/null || continue)
69
70 # extract ratio and completion status
71 ratio=$(echo "$info" | grep "Ratio:" | awk '{print $2}')
72 percent=$(echo "$info" | grep "Percent Done:" | awk '{print $3}' | tr -d '%')
73 state=$(echo "$info" | grep "State:" | cut -d: -f2- | xargs)
74
75 # skip if not 100% complete
76 [ "$percent" != "100" ] && continue
77
78 # check if ratio reached
79 ratio_reached=false
80 if [ -n "$ratio" ] && [ "$ratio" != "None" ]; then
81 if awk "BEGIN {exit !($ratio >= $MIN_RATIO)}"; then
82 ratio_reached=true
83 fi
84 fi
85
86 # check age via date added
87 date_added=$(echo "$info" | grep "Date added:" | cut -d: -f2- | xargs)
88 if [ -n "$date_added" ]; then
89 added_epoch=$(date -d "$date_added" +%s 2>/dev/null || echo 0)
90 now_epoch=$(date +%s)
91 age_days=$(( (now_epoch - added_epoch) / 86400 ))
92 else
93 age_days=0
94 fi
95
96 # remove if ratio reached OR older than max age
97 if [ "$ratio_reached" = "true" ]; then
98 echo "Removing torrent $id (ratio: $ratio >= $MIN_RATIO)"
99 transmission-remote "$TRANSMISSION_HOST" -t "$id" --remove-and-delete
100 elif [ "$age_days" -ge "$MAX_AGE_DAYS" ]; then
101 echo "Removing torrent $id (age: $age_days days >= $MAX_AGE_DAYS)"
102 transmission-remote "$TRANSMISSION_HOST" -t "$id" --remove-and-delete
103 fi
104 done
105 '';
106 serviceConfig = {
107 Type = "oneshot";
108 User = "transmission";
109 };
110 };
111
112 systemd.timers.transmission-gc = {
113 wantedBy = [ "timers.target" ];
114 timerConfig = {
115 OnCalendar = "daily";
116 Persistent = true;
117 RandomizedDelaySec = "1h";
118 };
119 };
120 systemd.services.transmission-watcher = {
121 wantedBy = [ "multi-user.target" ];
122 startAt = "*:0/5";
123 path = [
124 pkgs.curl
125 pkgs.systemdMinimal
126 ];
127 script = ''
128 set -efu -o pipefail
129 # don't restart if transmission is currently starting or stopping
130 state=$(systemctl show -p ActiveState --value transmission.service)
131 if [ "$state" = "activating" ] || [ "$state" = "deactivating" ]; then
132 echo "transmission is $state, skipping"
133 exit 0
134 fi
135 # check if transmission responds (with timeout)
136 if ! curl -SsfL --max-time 10 http://transmission.r; then
137 echo "transmission not responding, restarting"
138 systemctl restart transmission.service
139 fi
140 '';
141 };
142
143 security.acme.defaults.email = "spam@krebsco.de";
144 security.acme.acceptTerms = true;
145 security.acme.certs."yellow.r".server = config.krebs.ssl.acmeURL;
146 security.acme.certs."jelly.r".server = config.krebs.ssl.acmeURL;
147 security.acme.certs."radar.r".server = config.krebs.ssl.acmeURL;
148 security.acme.certs."sonar.r".server = config.krebs.ssl.acmeURL;
149 security.acme.certs."transmission.r".server = config.krebs.ssl.acmeURL;
150 services.nginx = {
151 enable = true;
152 package = pkgs.nginx.override {
153 modules = with pkgs.nginxModules; [
154 fancyindex
155 ];
156 };
157 virtualHosts."yellow.r" = {
158 serverAliases = [ "flix.r" ];
159 default = true;
160 enableACME = true;
161 addSSL = true;
162 locations."/" = {
163 root = "/var/download";
164 extraConfig = ''
165 fancyindex on;
166 fancyindex_footer "/fancy.html";
167 include ${pkgs.nginx}/conf/mime.types;
168 include ${pkgs.writeText "extrMime" ''
169 types {
170 video/webm mkv;
171 }
172 ''};
173 create_full_put_path on;
174 '';
175 };
176 locations."/chatty" = {
177 proxyPass = "http://localhost:3000";
178 extraConfig = ''
179 rewrite /chatty/(.*) /$1 break;
180 proxy_set_header Host $host;
181 '';
182 };
183 locations."= /fancy.html".extraConfig = ''
184 alias ${pkgs.writeText "nginx_footer" ''
185 <div id="mydiv">
186 <!-- Include a header DIV with the same name as the draggable DIV, followed by "header" -->
187 <div id="mydivheader">Click here to move</div>
188 <iframe src="/chatty/index.html"></iframe>
189 </div>
190 <style>
191 #mydiv {
192 position: absolute;
193 z-index: 9;
194 background-color: #f1f1f1;
195 border: 1px solid #d3d3d3;
196 text-align: center;
197 }
198
199 #mydivheader {
200 padding: 10px;
201 cursor: move;
202 z-index: 10;
203 background-color: #2196F3;
204 color: #fff;
205 }
206 </style>
207 <script>
208 // Make the DIV element draggable:
209 dragElement(document.getElementById("mydiv"));
210
211 function dragElement(elmnt) {
212 var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
213 if (document.getElementById(elmnt.id + "header")) {
214 // if present, the header is where you move the DIV from:
215 document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
216 } else {
217 // otherwise, move the DIV from anywhere inside the DIV:
218 elmnt.onmousedown = dragMouseDown;
219 }
220
221 function dragMouseDown(e) {
222 e = e || window.event;
223 e.preventDefault();
224 // get the mouse cursor position at startup:
225 pos3 = e.clientX;
226 pos4 = e.clientY;
227 document.onmouseup = closeDragElement;
228 // call a function whenever the cursor moves:
229 document.onmousemove = elementDrag;
230 }
231
232 function elementDrag(e) {
233 e = e || window.event;
234 e.preventDefault();
235 // calculate the new cursor position:
236 pos1 = pos3 - e.clientX;
237 pos2 = pos4 - e.clientY;
238 pos3 = e.clientX;
239 pos4 = e.clientY;
240 // set the element's new position:
241 elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
242 elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
243 }
244
245 function closeDragElement() {
246 // stop moving when mouse button is released:
247 document.onmouseup = null;
248 document.onmousemove = null;
249 }
250 }
251 </script>
252 ''};
253 '';
254 };
255 virtualHosts."jelly.r" = {
256 enableACME = true;
257 addSSL = true;
258 locations."/".extraConfig = ''
259 proxy_pass http://localhost:8096/;
260 proxy_set_header Accept-Encoding "";
261 '';
262 };
263 virtualHosts."transmission.r" = {
264 enableACME = true;
265 addSSL = true;
266 locations."/" = {
267 proxyWebsockets = true;
268 proxyPass = "http://128.0.0.1:9091";
269 };
270 };
271 virtualHosts."radar.r" = {
272 enableACME = true;
273 addSSL = true;
274 locations."/" = {
275 proxyWebsockets = true;
276 proxyPass = "http://localhost:7878";
277 };
278 };
279 virtualHosts."sonar.r" = {
280 enableACME = true;
281 addSSL = true;
282 locations."/" = {
283 proxyWebsockets = true;
284 proxyPass = "http://localhost:8989";
285 # sonar.r now has forced authentication
286 extraConfig = ''
287 proxy_set_header Authorization "Basic a3JlYnM6YWlkc2JhbGxz";
288 '';
289 };
290 };
291 };
292
293 services.samba = {
294 enable = true;
295 enableNmbd = false;
296 settings.global = {
297 "hosts allow" = "42::/16 10.243.0.0/16 10.244.0.0/16 fdcc:c5da:5295:c853:d499::/80";
298 "use sendfile" = "true";
299 "disable netbios" = "true";
300 "mangled names" = "illegal";
301 "socket options" = "TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=65536 SO_SNDBUF=65536";
302 "load printers" = "false";
303 "disable spoolss" = "true";
304 "printcap name" = "/dev/null";
305 "map to guest" = "Bad User";
306 "max log size" = "50";
307 "dns proxy" = "no";
308 "security" = "user";
309 "syslog only" = "yes";
310 };
311 shares.public = {
312 comment = "Warez";
313 path = "/var/download";
314 public = "yes";
315 "only guest" = "yes";
316 "create mask" = "0644";
317 "directory mask" = "2777";
318 writable = "no";
319 printable = "no";
320 };
321 };
322
323 systemd.services.bruellwuerfel =
324 let
325 bruellwuerfelSrc = pkgs.fetchFromGitHub {
326 owner = "krebs";
327 repo = "bruellwuerfel";
328 rev = "dc73adf69249fb63a4b024f1f3fbc9e541b27015";
329 sha256 = "078jp1gbavdp8lnwa09xa5m6bbbd05fi4x5ldkkgin5z04hwlhmd";
330 };
331 in
332 {
333 wantedBy = [ "multi-user.target" ];
334 after = [ "network-online.target" ];
335 environment = {
336 IRC_CHANNEL = "#flix";
337 IRC_NICK = "bruelli";
338 IRC_SERVER = "irc.r";
339 IRC_HISTORY_FILE = "/tmp/bruelli.history";
340 };
341 serviceConfig = {
342 ExecStart = "${pkgs.deno}/bin/deno run -A ${bruellwuerfelSrc}/src/index.ts";
343 };
344 };
345
346 networking.firewall.allowedTCPPorts = [
347 80 # nginx
348 443 # nginx
349 9091 # transmission web
350 8096 # jellyfin
351 8920 # jellyfin
352 51413 # transmission traffic
353 445 # smbd
354 111 # smbd
355 2049 # smbd
356 4000 # smbd
357 4001 # smbd
358 4002 # smbd
359 ];
360 networking.firewall.allowedUDPPorts = [
361 51413 # transmission traffic
362 1900 # jellyfin
363 7359 # jellyfin
364 111 # smbd
365 2049 # smbd
366 4000 # smbd
367 4001 # smbd
368 4002 # smbd
369 ];
370 krebs.iptables = {
371 enable = true;
372 tables.nat.PREROUTING.rules = [
373 # transmission rpc port
374 {
375 predicate = "-i retiolum -p tcp --dport 9091";
376 target = "DNAT --to-destination fdb4:3310:947::2";
377 v4 = false;
378 }
379 ];
380 tables.filter.FORWARD.policy = "ACCEPT"; # we need this so we can forward into the the transmission network namespace
381 };
382
383 systemd.services.flix-index = {
384 wantedBy = [ "multi-user.target" ];
385 path = [
386 pkgs.coreutils
387 pkgs.findutils
388 pkgs.inotify-tools
389 ];
390 startAt = "hourly";
391 serviceConfig = {
392 Type = "oneshot";
393 ExecStart = pkgs.writers.writeDash "flix-index" ''
394 set -efu
395 index(){
396 find . -type f > "$DIR"/index.tmp
397 mv "$DIR"/index.tmp "$DIR"/index
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}