diff --git a/hosts/fw/configuration.nix b/hosts/fw/configuration.nix index 9437bb2..be2f706 100644 --- a/hosts/fw/configuration.nix +++ b/hosts/fw/configuration.nix @@ -49,7 +49,7 @@ ./modules/firefox-sync.nix ./modules/fivefilters.nix - ./modules/pyload.nix + ./modules/pyload # home assistant ./modules/home-assistant diff --git a/hosts/fw/modules/pyload.nix b/hosts/fw/modules/pyload.nix deleted file mode 100644 index 101063b..0000000 --- a/hosts/fw/modules/pyload.nix +++ /dev/null @@ -1,320 +0,0 @@ -{ 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"; - }; - }; -} diff --git a/hosts/fw/modules/pyload/default.nix b/hosts/fw/modules/pyload/default.nix new file mode 100644 index 0000000..758e58a --- /dev/null +++ b/hosts/fw/modules/pyload/default.nix @@ -0,0 +1,153 @@ +{ config, pkgs, ... }: +let + cids = import ../staticids.nix; + networkPrefix = config.networkPrefix; + filebotScript = pkgs.callPackage ./filebot-process.nix {}; + + 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}/bin/filebot-process" + ]; + + # 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) + ]; + + imports = [ + ./pyload.nix + ./jellyfin.nix + ]; + + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ + "unrar" + "filebot" + ]; + + networking = { + hostName = "pyload"; + useHostResolvConf = false; + defaultGateway = { + address = "${networkPrefix}.97.1"; + interface = "eth0"; + }; + nameservers = [ "${networkPrefix}.97.1" ]; + firewall.enable = false; + }; + + # 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"; + }; + }; +} diff --git a/hosts/fw/modules/pyload/filebot-process.nix b/hosts/fw/modules/pyload/filebot-process.nix new file mode 100644 index 0000000..404a3fb --- /dev/null +++ b/hosts/fw/modules/pyload/filebot-process.nix @@ -0,0 +1,81 @@ +{ pkgs }: + +pkgs.writeShellScriptBin "filebot-process" '' + #!/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 +'' diff --git a/hosts/fw/modules/pyload/jellyfin.nix b/hosts/fw/modules/pyload/jellyfin.nix new file mode 100644 index 0000000..12bf825 --- /dev/null +++ b/hosts/fw/modules/pyload/jellyfin.nix @@ -0,0 +1,36 @@ +{ lib, pkgs, ... }: { + # 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"; + }; + + 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 + }; + }; +} diff --git a/hosts/fw/modules/pyload/pyload.nix b/hosts/fw/modules/pyload/pyload.nix new file mode 100644 index 0000000..c4c9401 --- /dev/null +++ b/hosts/fw/modules/pyload/pyload.nix @@ -0,0 +1,57 @@ +{ pkgs, ... }: +{ + environment.systemPackages = with pkgs; [ + unrar # Required for RAR archive extraction + p7zip # Required for 7z and other archive formats + ]; + + services.pyload = { + enable = true; + downloadDirectory = "/downloads"; + listenAddress = "0.0.0.0"; + port = 8000; + }; + + # 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" ]; + }; + }; +}