Compare commits

...

3 Commits

Author SHA1 Message Date
3e7b8c93e3 feat: split pyload 2025-11-26 22:39:07 +01:00
3e2f46377e fix: pyload and filebot 2025-11-26 21:08:58 +01:00
38bead3dc8 fix: pyload 2025-11-26 12:48:24 +01:00
9 changed files with 371 additions and 302 deletions

View File

@@ -49,7 +49,7 @@
./modules/firefox-sync.nix
./modules/fivefilters.nix
./modules/pyload.nix
./modules/pyload
# home assistant
./modules/home-assistant

View File

@@ -1,300 +0,0 @@
{ config, pkgs, ... }:
let
cids = import ./staticids.nix;
networkPrefix = config.networkPrefix;
# FileBot post-processing script
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)
DOWNLOAD_DIR="''${1:-/downloads}"
OUTPUT_DIR="/multimedia"
LOG_FILE="/var/lib/filebot/amc.log"
EXCLUDE_LIST="/var/lib/filebot/amc-exclude-list.txt"
# Ensure log directory exists
mkdir -p "$(dirname "$LOG_FILE")"
touch "$EXCLUDE_LIST"
echo "$(date): Starting FileBot processing for: $DOWNLOAD_DIR" >> "$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
# Clean up empty directories
find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true
echo "$(date): FileBot processing completed" >> "$LOG_FILE"
'';
pyloadUser = {
isSystemUser = true;
uid = cids.uids.pyload;
group = "pyload";
home = "/var/lib/pyload";
createHome = true;
};
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;
};
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 = [
"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 - -"
"d /var/lib/filebot 0755 filebot filebot - -"
];
# FileBot license secret
sops.secrets.filebot-license = {
mode = "0440";
owner = config.users.users.root.name;
group = config.users.groups.root.name;
};
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/filebot" = {
hostPath = "/var/lib/filebot";
isReadOnly = false;
};
"/var/lib/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
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
};
};
# Disable SSL certificate verification
systemd.services.pyload = {
environment = {
PYLOAD__GENERAL__SSL_VERIFY = "0";
# 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";
};
# Bind-mount DNS configuration files and system tools into the chroot
serviceConfig = {
BindReadOnlyPaths = [
"/etc/resolv.conf"
"/etc/nsswitch.conf"
"/etc/hosts"
"/etc/ssl"
"/etc/static/ssl"
# Make all system packages (including unrar and filebot) accessible
"/run/current-system/sw/bin"
];
};
};
# 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";
};
};
# 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;
users.users.filebot = filebotUser;
users.groups.filebot = filebotGroup;
system.stateVersion = "24.05";
};
};
}

View File

@@ -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";
};
};
}

View File

@@ -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
''

View File

@@ -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
};
};
}

View File

@@ -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" ];
};
};
}

View File

@@ -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 { });
}

View File

@@ -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

View File

@@ -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...")