diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index df81fa95aca2..3347adb1d145 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -957,6 +957,7 @@ ./services/misc/servarr/sonarr.nix ./services/misc/servarr/whisparr.nix ./services/misc/serviio.nix + ./services/misc/shelfmark.nix ./services/misc/shoko.nix ./services/misc/sickbeard.nix ./services/misc/snapper.nix diff --git a/nixos/modules/services/misc/shelfmark.nix b/nixos/modules/services/misc/shelfmark.nix new file mode 100644 index 000000000000..ebdaf812dbf1 --- /dev/null +++ b/nixos/modules/services/misc/shelfmark.nix @@ -0,0 +1,131 @@ +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.services.shelfmark; +in +{ + options.services.shelfmark = { + + enable = lib.mkEnableOption "Shelfmark, a self-hosted book and audiobook search and download interface"; + + package = lib.mkPackageOption pkgs "shelfmark" { }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Open the appropriate ports in the firewall for Shelfmark."; + }; + + environment = lib.mkOption { + type = lib.types.submodule { + freeformType = lib.types.attrsOf lib.types.str; + options = { + FLASK_HOST = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + example = "0.0.0.0"; + description = "The IP address to bind the Shelfmark server to."; + }; + + FLASK_PORT = lib.mkOption { + type = lib.types.port; + default = 8084; + apply = toString; + description = "TCP port for the Shelfmark web interface."; + }; + + ENABLE_LOGGING = lib.mkOption { + type = lib.types.bool; + apply = toString; + default = false; + description = "Whether to enable file logging. Disabled by default since systemd captures console output via journald."; + }; + + CONFIG_DIR = lib.mkOption { + type = lib.types.path; + default = "/var/lib/shelfmark"; + description = "Directory for Shelfmark configuration, database, and artwork cache."; + }; + }; + }; + default = { }; + example = { + SEARCH_MODE = "universal"; + LOG_LEVEL = "DEBUG"; + }; + description = '' + Environment variables to pass to the Shelfmark service. + See + for available options. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + + systemd.services.shelfmark = { + description = "Shelfmark - book and audiobook search and download interface"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + inherit (cfg) environment; + serviceConfig = { + DynamicUser = true; + ExecStart = "${lib.getExe cfg.package} -b ${cfg.environment.FLASK_HOST}:${cfg.environment.FLASK_PORT}"; + StateDirectory = "shelfmark"; + + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateMounts = true; + ProtectControlGroups = true; + ProtectKernelTunables = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + UMask = "0077"; + + CapabilityBoundingSet = [ "" ]; + NoNewPrivileges = true; + + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectClock = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + + PrivateUsers = true; + + LockPersonality = true; + ProtectHostname = true; + RestrictRealtime = true; + RestrictNamespaces = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + DeviceAllow = [ "" ]; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ (lib.toInt cfg.environment.FLASK_PORT) ]; + }; + }; + + meta = { + maintainers = with lib.maintainers; [ jamiemagee ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a6963a6ef6f4..badad067fe11 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1461,6 +1461,7 @@ in shadps4 = runTest ./shadps4.nix; sharkey = runTest ./web-apps/sharkey.nix; shattered-pixel-dungeon = runTest ./shattered-pixel-dungeon.nix; + shelfmark = runTest ./shelfmark.nix; shiori = runTest ./shiori.nix; shoko = import ./shoko.nix { inherit runTest; }; signal-desktop = runTest ./signal-desktop.nix; diff --git a/nixos/tests/shelfmark.nix b/nixos/tests/shelfmark.nix new file mode 100644 index 000000000000..da91edce5f97 --- /dev/null +++ b/nixos/tests/shelfmark.nix @@ -0,0 +1,20 @@ +{ lib, ... }: +{ + name = "shelfmark"; + meta.maintainers = with lib.maintainers; [ jamiemagee ]; + + nodes.machine = { + services.shelfmark = { + enable = true; + environment.FLASK_HOST = "0.0.0.0"; + }; + }; + + testScript = '' + machine.wait_for_unit("shelfmark.service") + machine.wait_for_open_port(8084) + + with subtest("Health endpoint responds"): + machine.succeed("curl --fail http://localhost:8084/api/health") + ''; +} diff --git a/pkgs/by-name/sh/shelfmark-frontend/package.nix b/pkgs/by-name/sh/shelfmark-frontend/package.nix new file mode 100644 index 000000000000..d8f448281fe8 --- /dev/null +++ b/pkgs/by-name/sh/shelfmark-frontend/package.nix @@ -0,0 +1,25 @@ +{ + buildNpmPackage, + shelfmark, +}: + +buildNpmPackage (finalAttrs: { + pname = "shelfmark-frontend"; + inherit (shelfmark) version src; + + sourceRoot = "${finalAttrs.src.name}/src/frontend"; + + npmDepsHash = "sha256-RAzotFGj0FGpfF7iyB5f2fdKFvMLcpJx142yplRwboU="; + + installPhase = '' + runHook preInstall + cp -r dist $out + runHook postInstall + ''; + + meta = { + description = "Shelfmark frontend"; + homepage = "https://github.com/calibrain/shelfmark/tree/main/src/frontend"; + inherit (shelfmark.meta) changelog license maintainers; + }; +}) diff --git a/pkgs/by-name/sh/shelfmark/package.nix b/pkgs/by-name/sh/shelfmark/package.nix new file mode 100644 index 000000000000..464e4755bef4 --- /dev/null +++ b/pkgs/by-name/sh/shelfmark/package.nix @@ -0,0 +1,96 @@ +{ + lib, + stdenv, + fetchFromGitHub, + python3Packages, + makeWrapper, + nixosTests, + shelfmark-frontend, + unrar-free, +}: + +let + pythonDeps = with python3Packages; [ + flask + flask-cors + flask-socketio + python-socketio + requests + pysocks + defusedxml + beautifulsoup4 + tqdm + dnspython + gunicorn + gevent + gevent-websocket + psutil + emoji + rarfile + qbittorrent-api + transmission-rpc + authlib + apprise + ]; +in +stdenv.mkDerivation (finalAttrs: { + pname = "shelfmark"; + version = "1.2.0"; + + src = fetchFromGitHub { + owner = "calibrain"; + repo = "shelfmark"; + tag = "v${finalAttrs.version}"; + hash = "sha256-t4t7je7Y/aezx/EX7paJIcsCq5qyZeU/+mPLeZ8oTPg="; + }; + + nativeBuildInputs = [ + python3Packages.wrapPython + makeWrapper + ]; + + pythonPath = pythonDeps; + + installPhase = '' + runHook preInstall + + wrapPythonPrograms + + mkdir -p $out/libexec $out/bin + + cp -r shelfmark $out/libexec/shelfmark + cp -r data $out/libexec/data + ln -s ${finalAttrs.passthru.frontend} $out/libexec/frontend-dist + + makeWrapper ${python3Packages.python.interpreter} $out/bin/shelfmark \ + --prefix PATH : ${ + lib.makeBinPath [ + python3Packages.python + unrar-free + ] + } \ + --set PYTHONPATH "$out/libexec:$program_PYTHONPATH" \ + --set USING_EXTERNAL_BYPASSER true \ + --add-flags "-m gunicorn.app.wsgiapp --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 shelfmark.main:app" + + runHook postInstall + ''; + + passthru = { + frontend = shelfmark-frontend.override { + shelfmark = finalAttrs.finalPackage; + }; + tests = { + inherit (nixosTests) shelfmark; + }; + }; + + meta = { + description = "Self-hosted web interface for searching and downloading books and audiobooks"; + homepage = "https://github.com/calibrain/shelfmark"; + changelog = "https://github.com/calibrain/shelfmark/releases/tag/${finalAttrs.src.tag}"; + license = lib.licenses.mit; + maintainers = [ lib.maintainers.jamiemagee ]; + mainProgram = "shelfmark"; + }; +})