From ecaf765bb52c1b05276e3295882e915b904d8251 Mon Sep 17 00:00:00 2001 From: matt1432 <matt@nelim.org> Date: Sat, 22 Mar 2025 19:30:22 -0400 Subject: [PATCH] feat: init kapowarr setup --- .../cluster/modules/caddy/default.nix | 1 + configurations/nos/modules/comics/default.nix | 1 + .../nos/modules/comics/kapowarr/default.nix | 11 ++ .../nos/modules/comics/kapowarr/module.nix | 116 ++++++++++++++ packages/default.nix | 2 + packages/kapowarr/bencoding/default.nix | 17 ++ packages/kapowarr/default.nix | 14 ++ packages/kapowarr/main/default.nix | 145 ++++++++++++++++++ packages/kapowarr/main/raise-errors.patch | 26 ++++ packages/kapowarr/tenacity/default.nix | 49 ++++++ .../kapowarr/typing-extensions/default.nix | 23 +++ 11 files changed, 405 insertions(+) create mode 100644 configurations/nos/modules/comics/kapowarr/default.nix create mode 100644 configurations/nos/modules/comics/kapowarr/module.nix create mode 100644 packages/kapowarr/bencoding/default.nix create mode 100644 packages/kapowarr/default.nix create mode 100644 packages/kapowarr/main/default.nix create mode 100644 packages/kapowarr/main/raise-errors.patch create mode 100644 packages/kapowarr/tenacity/default.nix create mode 100644 packages/kapowarr/typing-extensions/default.nix diff --git a/configurations/cluster/modules/caddy/default.nix b/configurations/cluster/modules/caddy/default.nix index 4e4f153c..945e391d 100644 --- a/configurations/cluster/modules/caddy/default.nix +++ b/configurations/cluster/modules/caddy/default.nix @@ -147,6 +147,7 @@ in { prowlarr.reverseProxy = "${nosIP}:9696"; radarr.reverseProxy = "${nosIP}:7878"; sonarr.reverseProxy = "${nosIP}:8989"; + kapowarr.reverseProxy = "${nosIP}:5676"; jdownloader2 = { subDirName = "jd2"; diff --git a/configurations/nos/modules/comics/default.nix b/configurations/nos/modules/comics/default.nix index fdbb6e1a..4b2aa8a0 100644 --- a/configurations/nos/modules/comics/default.nix +++ b/configurations/nos/modules/comics/default.nix @@ -1,6 +1,7 @@ {...}: { imports = [ ./jdownloader2 + ./kapowarr ./komga ]; } diff --git a/configurations/nos/modules/comics/kapowarr/default.nix b/configurations/nos/modules/comics/kapowarr/default.nix new file mode 100644 index 00000000..4792821a --- /dev/null +++ b/configurations/nos/modules/comics/kapowarr/default.nix @@ -0,0 +1,11 @@ +{mainUser, ...}: { + imports = [./module.nix]; + + services.kapowarr = { + enable = true; + port = 5676; + + user = mainUser; + group = "users"; + }; +} diff --git a/configurations/nos/modules/comics/kapowarr/module.nix b/configurations/nos/modules/comics/kapowarr/module.nix new file mode 100644 index 00000000..ba4891b5 --- /dev/null +++ b/configurations/nos/modules/comics/kapowarr/module.nix @@ -0,0 +1,116 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit + (lib) + getExe + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; + + cfg = config.services.kapowarr; +in { + options.services.kapowarr = { + enable = mkEnableOption "kapowarr"; + package = mkPackageOption pkgs.selfPackages "kapowarr" {}; + + user = mkOption { + type = types.str; + default = "kapowarr"; + description = "The user account under which Kapowarr runs."; + }; + + group = mkOption { + type = types.str; + default = "kapowarr"; + description = "The group under which Kapowarr runs."; + }; + + port = mkOption { + type = types.port; + default = 5656; + description = "Port where kapowarr should listen for incoming requests."; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/kapowarr/"; + description = "The directory where Kapowarr stores its data files."; + }; + + downloadDir = mkOption { + type = types.path; + default = "${cfg.dataDir}/temp_downloads"; + defaultText = "/var/lib/kapowarr/temp_downloads"; + description = "The directory where Kapowarr stores its downloaded files."; + }; + + logDir = mkOption { + type = types.path; + default = cfg.dataDir; + defaultText = "/var/lib/kapowarr"; + description = "The directory where Kapowarr stores its log file."; + }; + + openFirewall = mkEnableOption "Open ports in the firewall for Kapowarr."; + }; + + config = mkIf cfg.enable { + systemd.services.kapowarr = { + description = "Kapowarr"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + + environment = { + KAPOWARR_PORT = toString cfg.port; + KAPOWARR_LOG_DIR = cfg.logDir; + KAPOWARR_STATE_DIR = cfg.dataDir; + KAPOWARR_DOWNLOAD_DIR = cfg.downloadDir; + }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + StateDirectory = mkIf (cfg.dataDir == "/var/lib/kapowar") "kapowarr"; + ExecStart = "${getExe cfg.package} -d ${cfg.dataDir}"; + + # Hardening from komga service + RemoveIPC = true; + NoNewPrivileges = true; + CapabilityBoundingSet = ""; + SystemCallFilter = ["@system-service"]; + ProtectSystem = "full"; + PrivateTmp = true; + ProtectProc = "invisible"; + ProtectClock = true; + ProcSubset = "pid"; + PrivateUsers = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + LockPersonality = true; + RestrictNamespaces = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + SystemCallArchitectures = "native"; + RestrictSUIDSGID = true; + RestrictRealtime = true; + }; + }; + + networking.firewall = mkIf cfg.openFirewall {allowedTCPPorts = [cfg.port];}; + }; +} diff --git a/packages/default.nix b/packages/default.nix index 8ea6ce80..97d84229 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -19,6 +19,8 @@ jmusicbot = final.callPackage ./jmusicbot {}; + kapowarr = import ./kapowarr final; + komf = final.callPackage ./komf {}; libratbag = final.callPackage ./libratbag { diff --git a/packages/kapowarr/bencoding/default.nix b/packages/kapowarr/bencoding/default.nix new file mode 100644 index 00000000..31f8f3a3 --- /dev/null +++ b/packages/kapowarr/bencoding/default.nix @@ -0,0 +1,17 @@ +{ + # nix build inputs + buildPythonPackage, + fetchPypi, + ... +}: let + pname = "bencoding"; + version = "0.2.6"; +in + buildPythonPackage { + inherit pname version; + + src = fetchPypi { + inherit pname version; + hash = "sha256-Q8zjHUhj4p1rxhFVHU6fJlK+KZXp1eFbRtg4PxgNREA="; + }; + } diff --git a/packages/kapowarr/default.nix b/packages/kapowarr/default.nix new file mode 100644 index 00000000..0e142c2a --- /dev/null +++ b/packages/kapowarr/default.nix @@ -0,0 +1,14 @@ +{ + # nix build inputs + python3Packages, + ... +}: let + pyPkgs = python3Packages.override { + overrides = pyFinal: pyPrev: { + bencoding = pyFinal.callPackage ./bencoding {}; + tenacity = pyFinal.callPackage ./tenacity {}; + typing-extensions = pyFinal.callPackage ./typing-extensions {}; + }; + }; +in + pyPkgs.callPackage ./main {} diff --git a/packages/kapowarr/main/default.nix b/packages/kapowarr/main/default.nix new file mode 100644 index 00000000..189b1905 --- /dev/null +++ b/packages/kapowarr/main/default.nix @@ -0,0 +1,145 @@ +{ + # nix build inputs + lib, + buildPythonApplication, + fetchFromGitHub, + python, + # deps + aiohttp, + beautifulsoup4, + bencoding, # from overrides + flask, + flask-socketio, + pycryptodome, + requests, + setuptools, + simplejson, + tenacity, # from overrides + typing-extensions, # from overrides + waitress, + ... +}: let + pname = "kapowarr"; + version = "1.1.1"; +in + buildPythonApplication { + inherit pname version; + + src = fetchFromGitHub { + owner = "Casvt"; + repo = "Kapowarr"; + rev = "V${version}"; + hash = "sha256-EeDzgi37f0cA86lQ1Z6hzLgpE3ORfz0YPoMWp5R4uPs="; + }; + + patches = [./raise-errors.patch]; + + postPatch = '' + # Insert import for following substituteInPlace + sed -i '/# -\*- coding: utf-8 -\*-/a from os import environ' ./backend/base/logging.py + + substituteInPlace ./backend/base/logging.py --replace-fail \ + "return folder_path(Constants.LOGGER_FILENAME)" \ + "return f\"{environ.get('KAPOWARR_LOG_DIR')}/{Constants.LOGGER_FILENAME}\"" + + substituteInPlace ./backend/internals/settings.py \ + --replace-fail \ + "from os import urandom" \ + "from os import urandom, environ" \ + --replace-fail \ + "port: int = 5656" \ + "port: int = int(environ.get('KAPOWARR_PORT'))" \ + --replace-fail \ + "download_folder: str = folder_path('temp_downloads')" \ + "download_folder: str = environ.get('KAPOWARR_DOWNLOAD_DIR')" \ + --replace-fail \ + "filename = folder_path('frontend', 'static', 'json', 'pwa_manifest.json')" \ + "filename = f\"{environ.get('KAPOWARR_STATE_DIR')}/pwa_manifest.json\"" + ''; + + build-system = [setuptools]; + + dependencies = [ + typing-extensions + requests + beautifulsoup4 + flask + waitress + pycryptodome + tenacity + bencoding + simplejson + aiohttp + flask-socketio + ]; + + preBuild = '' + cat > setup.py << EOF + from setuptools import setup, find_packages, find_namespace_packages + + with open('requirements.txt') as f: + install_requires = f.read().splitlines() + + setup( + name='${pname}', + version = '${version}', + install_requires=install_requires, + packages=[ + 'frontend', + 'backend', + 'backend.base', + 'backend.features', + 'backend.implementations', + 'backend.implementations.torrent_clients', + 'backend.internals', + 'backend.lib', + ], + scripts=[ + 'Kapowarr.py' + ], + ) + EOF + ''; + + # Use XDG-ish dirs for configuration. These would otherwise be in the kapowarr package. + # + # Using --run as `makeWrapper` evaluates variables for --set and --set-default at build + # time and then single quotes the vars in the wrapper, thus they wouldn't get expanded. + # But using --run allows setting default vars that are evaluated on run and not during + # build time. + makeWrapperArgs = [ + "--set-default KAPOWARR_PORT 5656" + '' + --run "OUTDIR=\"$out\"" + --run ' + configDir="''${XDG_CONFIG_HOME:-$HOME/.config}/kapowarr" + export KAPOWARR_STATE_DIR="''${KAPOWARR_STATE_DIR-$configDir}" + export KAPOWARR_LOG_DIR="''${KAPOWARR_LOG_DIR-$configDir}" + export KAPOWARR_DOWNLOAD_DIR="''${KAPOWARR_DOWNLOAD_DIR-$configDir/temp_downloads}" + mkdir -p "$KAPOWARR_STATE_DIR" "$KAPOWARR_LOG_DIR" + + if [ ! -f "$KAPOWARR_STATE_DIR/pwa_manifest.json" ]; then + cat "$OUTDIR/${python.sitePackages}/frontend/static/json/pwa_manifest.json" > "$KAPOWARR_STATE_DIR/pwa_manifest.json" + fi + ' + '' + ]; + + postFixup = '' + # I prefer a clean name for the executable + mv $out/bin/Kapowarr.py $out/bin/${pname} + + # Add missing resources that Kapowarr uses at runtime in sitePackages + cp -r ./frontend/{static,templates} "$out/${python.sitePackages}/frontend" + ''; + + meta = { + mainProgram = pname; + license = lib.licenses.gpl3Only; + homepage = "https://casvt.github.io/Kapowarr"; + description = '' + Kapowarr is a software to build and manage a comic book library, + fitting in the *arr suite of software + ''; + }; + } diff --git a/packages/kapowarr/main/raise-errors.patch b/packages/kapowarr/main/raise-errors.patch new file mode 100644 index 00000000..4c5e76e9 --- /dev/null +++ b/packages/kapowarr/main/raise-errors.patch @@ -0,0 +1,26 @@ + Kapowarr.py | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/Kapowarr.py b/Kapowarr.py +index 0e9da56..306976c 100644 +--- a/Kapowarr.py ++++ b/Kapowarr.py +@@ -185,8 +185,13 @@ if __name__ == "__main__": + db_folder=db_folder + ) + +- except ValueError: +- parser.error("The value for -d/--DatabaseFolder is not a folder") ++ except ValueError as e: ++ if e.args and e.args[0] == 'Database location is not a folder': ++ parser.error( ++ "The value for -d/--DatabaseFolder is not a folder" ++ ) ++ else: ++ raise e + + else: + rc = Kapowarr() +-- +2.48.1 + diff --git a/packages/kapowarr/tenacity/default.nix b/packages/kapowarr/tenacity/default.nix new file mode 100644 index 00000000..a3f0a2d3 --- /dev/null +++ b/packages/kapowarr/tenacity/default.nix @@ -0,0 +1,49 @@ +{ + # nix build inputs + lib, + buildPythonPackage, + fetchPypi, + # deps + pbr, + pytest-asyncio, + pytestCheckHook, + pythonOlder, + setuptools-scm, + tornado, + typeguard, + ... +}: let + pname = "tenacity"; + version = "8.2.3"; +in + buildPythonPackage { + inherit pname version; + format = "pyproject"; + + disabled = pythonOlder "3.6"; + + src = fetchPypi { + inherit pname version; + hash = "sha256-U5jvDXjmP0AAfB+0wL/5bhkROU0vqNGU93YZwF/2zIo="; + }; + + nativeBuildInputs = [ + pbr + setuptools-scm + ]; + + nativeCheckInputs = [ + pytest-asyncio + pytestCheckHook + tornado + typeguard + ]; + + pythonImportsCheck = [pname]; + + meta = { + homepage = "https://github.com/jd/tenacity"; + description = "Retrying library for Python"; + license = lib.licenses.asl20; + }; + } diff --git a/packages/kapowarr/typing-extensions/default.nix b/packages/kapowarr/typing-extensions/default.nix new file mode 100644 index 00000000..dbd15b3b --- /dev/null +++ b/packages/kapowarr/typing-extensions/default.nix @@ -0,0 +1,23 @@ +{ + # nix build inputs + buildPythonPackage, + fetchPypi, + # deps + flit-core, + ... +}: let + pname = "typing_extensions"; + version = "4.12.2"; +in + buildPythonPackage { + inherit pname version; + format = "pyproject"; + + build-system = [flit-core]; + dependencies = [flit-core]; + + src = fetchPypi { + inherit pname version; + hash = "sha256-Gn6tVcflWd1N7ohW46iLQSJav+HOjfV7fBORX+Eh/7g="; + }; + }