feat: init kapowarr setup
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2025-03-22 19:30:22 -04:00
parent 3dbdacdd6e
commit ecaf765bb5
11 changed files with 405 additions and 0 deletions

View file

@ -147,6 +147,7 @@ in {
prowlarr.reverseProxy = "${nosIP}:9696";
radarr.reverseProxy = "${nosIP}:7878";
sonarr.reverseProxy = "${nosIP}:8989";
kapowarr.reverseProxy = "${nosIP}:5676";
jdownloader2 = {
subDirName = "jd2";

View file

@ -1,6 +1,7 @@
{...}: {
imports = [
./jdownloader2
./kapowarr
./komga
];
}

View file

@ -0,0 +1,11 @@
{mainUser, ...}: {
imports = [./module.nix];
services.kapowarr = {
enable = true;
port = 5676;
user = mainUser;
group = "users";
};
}

View file

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

View file

@ -19,6 +19,8 @@
jmusicbot = final.callPackage ./jmusicbot {};
kapowarr = import ./kapowarr final;
komf = final.callPackage ./komf {};
libratbag = final.callPackage ./libratbag {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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