This repository has no description
0

Configure Feed

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

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