This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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