{ config, pkgs, ... }: let cids = import ./staticids.nix; networkPrefix = config.networkPrefix; # FileBot post-processing script for PyLoad hooks filebotScript = pkgs.writeShellScript "filebot-process.sh" '' #!/usr/bin/env bash set -euo pipefail # FileBot AMC script for automated media organization # Called by PyLoad's package_extracted hook with parameters: # $1 = package_id # $2 = package_name # $3 = download_folder (actual path to extracted files) # $4 = password (optional) PACKAGE_ID="''${1:-}" PACKAGE_NAME="''${2:-unknown}" DOWNLOAD_DIR="''${3:-/downloads}" PASSWORD="''${4:-}" OUTPUT_DIR="/multimedia" LOG_FILE="/var/lib/pyload/filebot-amc.log" EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt" # Ensure FileBot data directory exists mkdir -p /var/lib/pyload/.local/share/filebot/data mkdir -p "$(dirname "$LOG_FILE")" touch "$EXCLUDE_LIST" # Install FileBot license if not already installed if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then echo "$(date): Installing FileBot license..." >> "$LOG_FILE" ${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true fi echo "===========================================" >> "$LOG_FILE" echo "$(date): PyLoad package extracted hook triggered" >> "$LOG_FILE" echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE" echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE" echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE" echo "===========================================" >> "$LOG_FILE" # Check if download directory exists and has media files if [ ! -d "$DOWNLOAD_DIR" ]; then echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE" exit 0 fi # Check if there are any video/media files to process if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE" echo "$(date): Skipping FileBot processing" >> "$LOG_FILE" exit 0 fi echo "$(date): Starting FileBot processing" >> "$LOG_FILE" # Run FileBot AMC script ${pkgs.filebot}/bin/filebot \ -script fn:amc \ --output "$OUTPUT_DIR" \ --action move \ --conflict auto \ -non-strict \ --log-file "$LOG_FILE" \ --def \ excludeList="$EXCLUDE_LIST" \ movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \ seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \ ut_dir="$DOWNLOAD_DIR" \ ut_kind=multi \ clean=y \ skipExtract=y || { echo "$(date): FileBot processing failed with exit code $?" >> "$LOG_FILE" exit 0 # Don't fail the hook even if FileBot fails } # Clean up empty directories find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE" exit 0 ''; pyloadUser = { isSystemUser = true; uid = cids.uids.pyload; group = "pyload"; home = "/var/lib/pyload"; createHome = true; extraGroups = [ "jellyfin" ]; # Access to multimedia directories }; pyloadGroup = { gid = cids.gids.pyload; }; jellyfinUser = { isSystemUser = true; uid = cids.uids.jellyfin; group = "jellyfin"; home = "/var/lib/jellyfin"; createHome = true; extraGroups = [ "render" "video" ]; }; jellyfinGroup = { gid = cids.gids.jellyfin; }; in { users.users.pyload = pyloadUser; users.groups.pyload = pyloadGroup; users.users.jellyfin = jellyfinUser; users.groups.jellyfin = jellyfinGroup; # Create the directory structure on the host systemd.tmpfiles.rules = [ "d /var/lib/downloads 0755 pyload pyload - -" "d /var/lib/multimedia 0775 root jellyfin - -" "d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -" "d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -" "d /var/lib/multimedia/music 0755 jellyfin jellyfin - -" "d /var/lib/jellyfin 0755 jellyfin jellyfin - -" # PyLoad hook scripts directory "d /var/lib/pyload/config 0755 pyload pyload - -" "d /var/lib/pyload/config/scripts 0755 pyload pyload - -" "d /var/lib/pyload/config/scripts/package_extracted 0755 pyload pyload - -" "L+ /var/lib/pyload/config/scripts/package_extracted/filebot-process.sh - - - - ${filebotScript}" ]; # FileBot license secret sops.secrets.filebot-license = { mode = "0440"; owner = "pyload"; group = "pyload"; }; containers.pyload = { autoStart = true; ephemeral = false; privateNetwork = true; hostBridge = "server"; hostAddress = "${networkPrefix}.97.1"; localAddress = "${networkPrefix}.97.11/24"; # GPU device passthrough for hardware transcoding allowedDevices = [ { modifier = "rwm"; node = "/dev/dri/card0"; } { modifier = "rwm"; node = "/dev/dri/renderD128"; } ]; bindMounts = { "/dev/dri" = { hostPath = "/dev/dri"; isReadOnly = false; }; "/run/opengl-driver" = { hostPath = "/run/opengl-driver"; isReadOnly = true; }; "/nix/store" = { hostPath = "/nix/store"; isReadOnly = true; }; "/var/lib/pyload" = { hostPath = "/var/lib/pyload"; isReadOnly = false; }; "/var/lib/jellyfin" = { hostPath = "/var/lib/jellyfin"; isReadOnly = false; }; "/downloads" = { hostPath = "/var/lib/downloads"; isReadOnly = false; }; "/multimedia" = { hostPath = "/var/lib/multimedia"; isReadOnly = false; }; "/var/lib/pyload/filebot-license.psm" = { hostPath = config.sops.secrets.filebot-license.path; isReadOnly = true; }; }; config = { lib, config, pkgs, ... }: { nixpkgs.overlays = [ (import ../utils/overlays/packages.nix) ]; nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "unrar" "filebot" ]; environment.systemPackages = with pkgs; [ unrar # Required for RAR archive extraction p7zip # Required for 7z and other archive formats filebot # Automated media file organization ]; # Intel graphics support for hardware transcoding hardware.graphics = { enable = true; extraPackages = with pkgs; [ intel-media-driver vpl-gpu-rt intel-compute-runtime ]; }; # Set VA-API driver to iHD (modern Intel driver for N100) environment.sessionVariables = { LIBVA_DRIVER_NAME = "iHD"; }; networking = { hostName = "pyload"; useHostResolvConf = false; defaultGateway = { address = "${networkPrefix}.97.1"; interface = "eth0"; }; nameservers = [ "${networkPrefix}.97.1" ]; firewall.enable = false; }; services.pyload = { enable = true; downloadDirectory = "/downloads"; listenAddress = "0.0.0.0"; port = 8000; }; services.jellyfin = { enable = true; openFirewall = true; }; # Override systemd hardening for GPU access systemd.services.jellyfin = { serviceConfig = { PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access DeviceAllow = [ "/dev/dri/card0 rw" "/dev/dri/renderD128 rw" ]; SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access }; environment = { LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable }; }; # Configure pyload service systemd.services.pyload = { # Add extraction tools to service PATH path = with pkgs; [ unrar # For RAR extraction p7zip # For 7z extraction ]; environment = { # Disable SSL certificate verification PYLOAD__GENERAL__SSL_VERIFY = "0"; # Download speed limiting (150 Mbit/s = 19200 KiB/s) PYLOAD__DOWNLOAD__LIMIT_SPEED = "1"; PYLOAD__DOWNLOAD__MAX_SPEED = "19200"; # Enable ExtractArchive plugin PYLOAD__EXTRACTARCHIVE__ENABLED = "1"; PYLOAD__EXTRACTARCHIVE__DELETE = "1"; PYLOAD__EXTRACTARCHIVE__DELTOTRASH = "0"; PYLOAD__EXTRACTARCHIVE__REPAIR = "1"; PYLOAD__EXTRACTARCHIVE__RECURSIVE = "1"; PYLOAD__EXTRACTARCHIVE__FULLPATH = "1"; # Enable ExternalScripts plugin for hooks PYLOAD__EXTERNALSCRIPTS__ENABLED = "1"; PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously }; # Bind-mount DNS configuration files into the chroot serviceConfig = { BindReadOnlyPaths = [ "/etc/resolv.conf" "/etc/nsswitch.conf" "/etc/hosts" "/etc/ssl" "/etc/static/ssl" ]; # Bind mount multimedia directory as writable for FileBot hook scripts BindPaths = [ "/multimedia" ]; }; }; # Ensure render/video groups exist with consistent GIDs for GPU access users.groups.render = { gid = 303; }; users.groups.video = { gid = 26; }; users.users.pyload = pyloadUser; users.groups.pyload = pyloadGroup; users.users.jellyfin = jellyfinUser; users.groups.jellyfin = jellyfinGroup; system.stateVersion = "24.05"; }; }; }