From 38bead3dc89d590e931ff55d2baf87cd42f27644 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Wed, 26 Nov 2025 12:48:24 +0100 Subject: [PATCH 1/3] fix: pyload --- hosts/fw/modules/pyload.nix | 14 +++++-- utils/overlays/packages.nix | 2 +- .../default.nix} | 4 ++ .../patches/declarative-env-config.patch | 38 +++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) rename utils/pkgs/{pyload-ng-updated.nix => pyload-ng/default.nix} (90%) create mode 100644 utils/pkgs/pyload-ng/patches/declarative-env-config.patch diff --git a/hosts/fw/modules/pyload.nix b/hosts/fw/modules/pyload.nix index 616e9e5..96c48c1 100644 --- a/hosts/fw/modules/pyload.nix +++ b/hosts/fw/modules/pyload.nix @@ -178,6 +178,7 @@ in environment.systemPackages = with pkgs; [ unrar # Required for RAR archive extraction + p7zip # Required for 7z and other archive formats filebot # Automated media file organization ]; @@ -234,9 +235,16 @@ in }; }; - # Disable SSL certificate verification + # 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"; # Enable ExtractArchive plugin @@ -248,7 +256,7 @@ in PYLOAD__EXTRACTARCHIVE__FULLPATH = "1"; }; - # Bind-mount DNS configuration files and system tools into the chroot + # Bind-mount DNS configuration files into the chroot serviceConfig = { BindReadOnlyPaths = [ "/etc/resolv.conf" @@ -256,8 +264,6 @@ in "/etc/hosts" "/etc/ssl" "/etc/static/ssl" - # Make all system packages (including unrar and filebot) accessible - "/run/current-system/sw/bin" ]; }; }; diff --git a/utils/overlays/packages.nix b/utils/overlays/packages.nix index 811ae3b..56e0abf 100644 --- a/utils/overlays/packages.nix +++ b/utils/overlays/packages.nix @@ -14,7 +14,7 @@ self: super: { }; python3Packages = self.python3.pkgs; - pyload-ng = self.callPackage ../pkgs/pyload-ng-updated.nix { pyload-ng = super.pyload-ng; }; + pyload-ng = self.callPackage ../pkgs/pyload-ng { pyload-ng = super.pyload-ng; }; # vscode-insiders = (super.callPackage ../pkgs/vscode-insiders.nix { }); } diff --git a/utils/pkgs/pyload-ng-updated.nix b/utils/pkgs/pyload-ng/default.nix similarity index 90% rename from utils/pkgs/pyload-ng-updated.nix rename to utils/pkgs/pyload-ng/default.nix index 195eebf..01003bf 100644 --- a/utils/pkgs/pyload-ng-updated.nix +++ b/utils/pkgs/pyload-ng/default.nix @@ -10,6 +10,10 @@ pyload-ng.overridePythonAttrs (oldAttrs: rec { hash = "sha256-g1eEeNnr3Axtr+0BJzMcNQomTEX4EsUG1Jxt+huPyoc="; }; + patches = [ + ./patches/declarative-env-config.patch + ]; + # Add new dependencies required in newer versions propagatedBuildInputs = (oldAttrs.propagatedBuildInputs or []) ++ (with python3Packages; [ mini-racer diff --git a/utils/pkgs/pyload-ng/patches/declarative-env-config.patch b/utils/pkgs/pyload-ng/patches/declarative-env-config.patch new file mode 100644 index 0000000..589a041 --- /dev/null +++ b/utils/pkgs/pyload-ng/patches/declarative-env-config.patch @@ -0,0 +1,38 @@ +diff --git a/src/pyload/core/__init__.py b/src/pyload/core/__init__.py +index 4324fc700..5d915a85e 100644 +--- a/src/pyload/core/__init__.py ++++ b/src/pyload/core/__init__.py +@@ -130,6 +130,14 @@ class Core: + else: + self._debug = max(0, int(debug)) + ++ # Process core config environment variables (NixOS declarative config) ++ for env, value in os.environ.items(): ++ if not env.startswith("PYLOAD__"): ++ continue ++ parts = env.removeprefix("PYLOAD__").lower().split("__", 1) ++ if len(parts) == 2 and parts[0] in self.config.config: ++ self.config.set(parts[0], parts[1], value) ++ + # If no argument set, read storage dir from config file, + # otherwise save setting to config dir + if storagedir is None: +@@ -227,6 +235,18 @@ class Core: + self.thm = self.thread_manager = ThreadManager(self) + self.cpm = self.captcha_manager = CaptchaManager(self) + self.adm = self.addon_manager = AddonManager(self) ++ ++ # Process plugin config environment variables after plugins are loaded (NixOS declarative config) ++ # Build case-insensitive lookup map for plugin names ++ plugin_name_map = {name.lower(): name for name in self.config.plugin.keys()} ++ ++ for env, value in os.environ.items(): ++ if not env.startswith("PYLOAD__"): ++ continue ++ parts = env.removeprefix("PYLOAD__").lower().split("__", 1) ++ if len(parts) == 2 and parts[0] in plugin_name_map: ++ actual_plugin_name = plugin_name_map[parts[0]] ++ self.config.set_plugin(actual_plugin_name, parts[1], value) + + def _setup_permissions(self): + self.log.debug("Setup permissions...") From 3e2f46377eadaec0eae418398b87773d57270902 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Wed, 26 Nov 2025 21:08:58 +0100 Subject: [PATCH 2/3] fix: pyload and filebot --- hosts/fw/modules/pyload.nix | 122 ++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 54 deletions(-) diff --git a/hosts/fw/modules/pyload.nix b/hosts/fw/modules/pyload.nix index 96c48c1..101063b 100644 --- a/hosts/fw/modules/pyload.nix +++ b/hosts/fw/modules/pyload.nix @@ -3,24 +3,59 @@ let cids = import ./staticids.nix; networkPrefix = config.networkPrefix; - # FileBot post-processing script + # 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 - # Arguments: $1 = download directory (passed by pyload) + # 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:-}" - DOWNLOAD_DIR="''${1:-/downloads}" OUTPUT_DIR="/multimedia" - LOG_FILE="/var/lib/filebot/amc.log" - EXCLUDE_LIST="/var/lib/filebot/amc-exclude-list.txt" + LOG_FILE="/var/lib/pyload/filebot-amc.log" + EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt" - # Ensure log directory exists + # Ensure FileBot data directory exists + mkdir -p /var/lib/pyload/.local/share/filebot/data mkdir -p "$(dirname "$LOG_FILE")" touch "$EXCLUDE_LIST" - echo "$(date): Starting FileBot processing for: $DOWNLOAD_DIR" >> "$LOG_FILE" + # 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 \ @@ -37,12 +72,16 @@ let ut_dir="$DOWNLOAD_DIR" \ ut_kind=multi \ clean=y \ - skipExtract=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" >> "$LOG_FILE" + echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE" + exit 0 ''; pyloadUser = { @@ -51,6 +90,7 @@ let group = "pyload"; home = "/var/lib/pyload"; createHome = true; + extraGroups = [ "jellyfin" ]; # Access to multimedia directories }; pyloadGroup = { gid = cids.gids.pyload; @@ -67,26 +107,12 @@ let jellyfinGroup = { gid = cids.gids.jellyfin; }; - - filebotUser = { - isSystemUser = true; - uid = cids.uids.filebot; - group = "filebot"; - home = "/var/lib/filebot"; - createHome = true; - extraGroups = [ "pyload" "jellyfin" ]; # Access to both download and media directories - }; - filebotGroup = { - gid = cids.gids.filebot; - }; in { users.users.pyload = pyloadUser; users.groups.pyload = pyloadGroup; users.users.jellyfin = jellyfinUser; users.groups.jellyfin = jellyfinGroup; - users.users.filebot = filebotUser; - users.groups.filebot = filebotGroup; # Create the directory structure on the host systemd.tmpfiles.rules = [ @@ -96,14 +122,19 @@ in "d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -" "d /var/lib/multimedia/music 0755 jellyfin jellyfin - -" "d /var/lib/jellyfin 0755 jellyfin jellyfin - -" - "d /var/lib/filebot 0755 filebot filebot - -" + + # 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 = config.users.users.root.name; - group = config.users.groups.root.name; + owner = "pyload"; + group = "pyload"; }; containers.pyload = { @@ -155,11 +186,7 @@ in hostPath = "/var/lib/multimedia"; isReadOnly = false; }; - "/var/lib/filebot" = { - hostPath = "/var/lib/filebot"; - isReadOnly = false; - }; - "/var/lib/filebot/license.psm" = { + "/var/lib/pyload/filebot-license.psm" = { hostPath = config.sops.secrets.filebot-license.path; isReadOnly = true; }; @@ -247,6 +274,10 @@ in # 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"; @@ -254,6 +285,10 @@ in 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 @@ -265,27 +300,8 @@ in "/etc/ssl" "/etc/static/ssl" ]; - }; - }; - - # FileBot processing service - systemd.services.filebot-process = { - description = "FileBot media file processing"; - serviceConfig = { - Type = "oneshot"; - User = "filebot"; - Group = "filebot"; - ExecStart = "${filebotScript}"; - }; - }; - - # Watch for completed downloads and trigger FileBot - systemd.paths.filebot-watch = { - description = "Watch for completed downloads"; - wantedBy = [ "multi-user.target" ]; - pathConfig = { - PathModified = "/downloads"; - Unit = "filebot-process.service"; + # Bind mount multimedia directory as writable for FileBot hook scripts + BindPaths = [ "/multimedia" ]; }; }; @@ -297,8 +313,6 @@ in users.groups.pyload = pyloadGroup; users.users.jellyfin = jellyfinUser; users.groups.jellyfin = jellyfinGroup; - users.users.filebot = filebotUser; - users.groups.filebot = filebotGroup; system.stateVersion = "24.05"; }; From 3e7b8c93e3a26d3b44608651f43222774d3d1787 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Wed, 26 Nov 2025 22:39:07 +0100 Subject: [PATCH 3/3] feat: split pyload --- hosts/fw/configuration.nix | 2 +- hosts/fw/modules/pyload.nix | 320 -------------------- hosts/fw/modules/pyload/default.nix | 153 ++++++++++ hosts/fw/modules/pyload/filebot-process.nix | 81 +++++ hosts/fw/modules/pyload/jellyfin.nix | 36 +++ hosts/fw/modules/pyload/pyload.nix | 57 ++++ 6 files changed, 328 insertions(+), 321 deletions(-) delete mode 100644 hosts/fw/modules/pyload.nix create mode 100644 hosts/fw/modules/pyload/default.nix create mode 100644 hosts/fw/modules/pyload/filebot-process.nix create mode 100644 hosts/fw/modules/pyload/jellyfin.nix create mode 100644 hosts/fw/modules/pyload/pyload.nix 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" ]; + }; + }; +}