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