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 479 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 # and only auto-resumes above if fulldisk_autoresume is set (default off); 162 # this guard keeps those at THRESH GiB / on, and additionally sends a resume 163 # on recovery. Transmission has no equivalent, so below the threshold the 164 # guard stops all its torrents (restarting them on recovery) and posts a 165 # Matrix alert on each transition. 166 systemd.services.download-space-guard = { 167 wantedBy = [ "multi-user.target" ]; 168 after = [ "sabnzbd.service" ]; 169 startAt = "*:0/5"; 170 path = [ 171 pkgs.curl 172 pkgs.coreutils 173 pkgs.gnugrep 174 pkgs.gawk 175 pkgs.jq 176 pkgs.transmission_4 177 ]; 178 script = '' 179 set -efu 180 DIR=/var/download 181 THRESH=100 182 # !MRjkvAaFPsjdYCpeiZ:lassul.us, URL-encoded for the API path 183 ROOM=%21MRjkvAaFPsjdYCpeiZ%3Alassul.us 184 STATE="$STATE_DIRECTORY/state" 185 186 avail=$(df -Pk "$DIR" | awk 'NR==2 {print int($4 / 1024 / 1024)}') 187 188 INI=/var/lib/sabnzbd/sabnzbd.ini 189 key=$(grep -E '^api_key' "$INI" | head -1 | cut -d= -f2 | tr -d ' ') 190 sab() { 191 curl -fsS --max-time 15 "http://127.0.0.1:8080/api?output=json&apikey=$key&$1" >/dev/null || : 192 } 193 tr_remote() { 194 transmission-remote 128.0.0.1:9091 "$@" >/dev/null || : 195 } 196 notify() { 197 token=$(cat "$CREDENTIALS_DIRECTORY/matrix-token") 198 body=$(jq -nc --arg b "$1" '{msgtype: "m.text", body: $b}') 199 curl -fsS --max-time 15 -X POST \ 200 -H "Authorization: Bearer $token" -H 'Content-Type: application/json' \ 201 "https://matrix.lassul.us/_matrix/client/v3/rooms/$ROOM/send/m.room.message" \ 202 --data "$body" >/dev/null || : 203 } 204 205 # sabnzbd pauses continuously once free disk drops below these; keep 206 # them at THRESH GiB and auto-resume on; re-assert only if changed. 207 for k in download_free complete_free; do 208 grep -qxF "$k = ''${THRESH}G" "$INI" \ 209 || sab "mode=set_config&section=misc&keyword=$k&value=''${THRESH}G" 210 done 211 grep -qxF "fulldisk_autoresume = 1" "$INI" \ 212 || sab "mode=set_config&section=misc&keyword=fulldisk_autoresume&value=1" 213 214 prev=ok 215 if [ -f "$STATE" ]; then prev=$(cat "$STATE"); fi 216 if [ "$avail" -lt "$THRESH" ]; then cur=low; else cur=ok; fi 217 218 if [ "$cur" = low ]; then 219 # stop everything each run so newly-grabbed torrents can't fill the disk 220 tr_remote -t all --stop 221 if [ "$prev" != low ]; then 222 echo "free ''${avail}G < ''${THRESH}G: stopped transmission, alerting" 223 notify "⚠️ yellow: /var/download down to ''${avail} GiB free (< ''${THRESH} GiB). Stopped all transmission torrents; sabnzbd auto-paused via download_free." 224 fi 225 elif [ "$prev" = low ]; then 226 echo "free ''${avail}G >= ''${THRESH}G: resumed transmission and sabnzbd, alerting" 227 tr_remote -t all --start 228 sab "mode=resume" 229 notify "✅ yellow: /var/download recovered to ''${avail} GiB free. Resumed sabnzbd and transmission torrents." 230 fi 231 echo "$cur" > "$STATE" 232 ''; 233 serviceConfig = { 234 Type = "oneshot"; 235 User = "root"; 236 StateDirectory = "download-space-guard"; 237 LoadCredential = [ 238 "matrix-token:${config.clan.core.vars.generators.archiver-matrix.files."matrix-access-token".path}" 239 ]; 240 }; 241 }; 242 243 security.acme.defaults.email = "spam@krebsco.de"; 244 security.acme.acceptTerms = true; 245 security.acme.certs."yellow.r".server = config.krebs.ssl.acmeURL; 246 security.acme.certs."jelly.r".server = config.krebs.ssl.acmeURL; 247 security.acme.certs."radar.r".server = config.krebs.ssl.acmeURL; 248 security.acme.certs."sonar.r".server = config.krebs.ssl.acmeURL; 249 security.acme.certs."transmission.r".server = config.krebs.ssl.acmeURL; 250 services.nginx = { 251 enable = true; 252 package = pkgs.nginx.override { 253 modules = with pkgs.nginxModules; [ 254 fancyindex 255 ]; 256 }; 257 virtualHosts."yellow.r" = { 258 serverAliases = [ "flix.r" ]; 259 default = true; 260 enableACME = true; 261 addSSL = true; 262 locations."/" = { 263 root = "/var/download"; 264 extraConfig = '' 265 fancyindex on; 266 include ${pkgs.nginx}/conf/mime.types; 267 include ${pkgs.writeText "extrMime" '' 268 types { 269 video/webm mkv; 270 } 271 ''}; 272 create_full_put_path on; 273 ''; 274 }; 275 276 }; 277 virtualHosts."jelly.r" = { 278 enableACME = true; 279 addSSL = true; 280 locations."/".extraConfig = '' 281 proxy_pass http://localhost:8096/; 282 proxy_set_header Accept-Encoding ""; 283 ''; 284 }; 285 virtualHosts."transmission.r" = { 286 enableACME = true; 287 addSSL = true; 288 locations."/" = { 289 proxyWebsockets = true; 290 proxyPass = "http://128.0.0.1:9091"; 291 }; 292 }; 293 virtualHosts."radar.r" = { 294 enableACME = true; 295 addSSL = true; 296 locations."/" = { 297 proxyWebsockets = true; 298 proxyPass = "http://localhost:7878"; 299 }; 300 }; 301 virtualHosts."sonar.r" = { 302 enableACME = true; 303 addSSL = true; 304 locations."/" = { 305 proxyWebsockets = true; 306 proxyPass = "http://localhost:8989"; 307 # sonar.r now has forced authentication 308 extraConfig = '' 309 proxy_set_header Authorization "Basic a3JlYnM6YWlkc2JhbGxz"; 310 ''; 311 }; 312 }; 313 }; 314 315 services.samba = { 316 enable = true; 317 enableNmbd = false; 318 settings.global = { 319 "hosts allow" = "42::/16 10.243.0.0/16 10.244.0.0/16 fdcc:c5da:5295:c853:d499::/80"; 320 "use sendfile" = "true"; 321 "disable netbios" = "true"; 322 "mangled names" = "illegal"; 323 "socket options" = "TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=65536 SO_SNDBUF=65536"; 324 "load printers" = "false"; 325 "disable spoolss" = "true"; 326 "printcap name" = "/dev/null"; 327 "map to guest" = "Bad User"; 328 "max log size" = "50"; 329 "dns proxy" = "no"; 330 "security" = "user"; 331 "syslog only" = "yes"; 332 }; 333 shares.public = { 334 comment = "Warez"; 335 path = "/var/download"; 336 public = "yes"; 337 "only guest" = "yes"; 338 "create mask" = "0644"; 339 "directory mask" = "2777"; 340 writable = "no"; 341 printable = "no"; 342 }; 343 }; 344 345 networking.firewall.allowedTCPPorts = [ 346 80 # nginx 347 443 # nginx 348 9091 # transmission web 349 8096 # jellyfin 350 8920 # jellyfin 351 51413 # transmission traffic 352 445 # smbd 353 111 # smbd 354 2049 # smbd 355 4000 # smbd 356 4001 # smbd 357 4002 # smbd 358 ]; 359 networking.firewall.allowedUDPPorts = [ 360 51413 # transmission traffic 361 1900 # jellyfin 362 7359 # jellyfin 363 111 # smbd 364 2049 # smbd 365 4000 # smbd 366 4001 # smbd 367 4002 # smbd 368 ]; 369 krebs.iptables = { 370 enable = true; 371 tables.nat.PREROUTING.rules = [ 372 # transmission rpc port 373 { 374 predicate = "-i retiolum -p tcp --dport 9091"; 375 target = "DNAT --to-destination fdb4:3310:947::2"; 376 v4 = false; 377 } 378 ]; 379 tables.filter.FORWARD.policy = "ACCEPT"; # we need this so we can forward into the the transmission network namespace 380 }; 381 382 systemd.services.flix-index = { 383 wantedBy = [ "multi-user.target" ]; 384 path = [ 385 pkgs.coreutils 386 pkgs.findutils 387 pkgs.inotify-tools 388 ]; 389 startAt = "hourly"; 390 serviceConfig = { 391 Type = "oneshot"; 392 ExecStart = pkgs.writers.writeDash "flix-index" '' 393 set -efu 394 index(){ 395 # /var/download churns constantly (download temp files appearing and 396 # vanishing), so find can exit non-zero when an entry disappears 397 # mid-scan. Ignore that — the entries we did enumerate are still valid 398 # — but only replace the live index if we actually produced one. 399 find . -type f > "$DIR"/index.tmp || : 400 if [ -s "$DIR"/index.tmp ]; then 401 mv "$DIR"/index.tmp "$DIR"/index 402 fi 403 } 404 405 DIR=/var/download 406 cd "$DIR" 407 index 408 ''; 409 }; 410 }; 411 412 services.jellyfin = { 413 enable = true; 414 group = "download"; 415 }; 416 417 # request managment 418 services.jellyseerr = { 419 enable = true; 420 openFirewall = true; 421 }; 422 423 # movies 424 services.radarr = { 425 enable = true; 426 openFirewall = true; 427 user = "download"; 428 group = "download"; 429 }; 430 431 # shows 432 services.sonarr = { 433 enable = true; 434 openFirewall = true; 435 user = "download"; 436 group = "download"; 437 }; 438 # sonarr needs unsecure packages 439 nixpkgs.config.permittedInsecurePackages = [ 440 "aspnetcore-runtime-wrapped-6.0.36" 441 "aspnetcore-runtime-6.0.36" 442 "dotnet-sdk-wrapped-6.0.428" 443 "dotnet-sdk-6.0.428" 444 "olm-3.2.16" 445 ]; 446 447 # indexers 448 services.prowlarr = { 449 enable = true; 450 openFirewall = true; 451 }; 452 453 # flaresolverr for bypassing cloudflare on some indexers 454 services.flaresolverr = { 455 enable = true; 456 openFirewall = true; 457 }; 458 459 # subtitles 460 services.bazarr = { 461 enable = true; 462 openFirewall = true; 463 user = "download"; 464 group = "download"; 465 }; 466 467 # usenet download client 468 services.sabnzbd = { 469 enable = true; 470 group = "download"; 471 openFirewall = true; 472 }; 473 # sonarr/radarr copy on import and leave the originals in sabnzbd's 474 # complete_dir (/var/download/usenet) forever; purge anything untouched 475 # for a week. Active imports are always younger than that. 476 systemd.tmpfiles.rules = [ 477 "e /var/download/usenet - - - 7d" 478 ]; 479}