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