From 0a1f6f86c13278c3fd36ed61e8b0d422a94e6065 Mon Sep 17 00:00:00 2001 From: Gabriel Fontes Date: Mon, 17 Aug 2026 00:29:12 -0300 Subject: [PATCH] feat(merope/books): replace Calibre with Kavita Serve the Readarr-owned library through read-only Kavita, with a persisted machine-local token key and the existing public books.m7.rs endpoint. Assisted-by: pi (gpt-5.6-sol) --- .../merope/services/media/calibre-web.nix | 57 ----------------- hosts/nixos/merope/services/media/default.nix | 2 +- hosts/nixos/merope/services/media/kavita.nix | 63 +++++++++++++++++++ hosts/nixos/merope/services/media/readarr.nix | 3 +- 4 files changed, 65 insertions(+), 60 deletions(-) delete mode 100644 hosts/nixos/merope/services/media/calibre-web.nix create mode 100644 hosts/nixos/merope/services/media/kavita.nix diff --git a/hosts/nixos/merope/services/media/calibre-web.nix b/hosts/nixos/merope/services/media/calibre-web.nix deleted file mode 100644 index 47d867da..00000000 --- a/hosts/nixos/merope/services/media/calibre-web.nix +++ /dev/null @@ -1,57 +0,0 @@ -{ - config, - lib, - ... -}: { - # Calibre Content Server maintains Readarr's library. Calibre-Web consumes - # the resulting database and files read-only, like Jellyfin. - users.users.calibre-server.extraGroups = [config.services.readarr.group]; - - services = { - calibre-server = { - enable = true; - libraries = ["/srv/media/books"]; - # Readarr connects locally and uses the Content Server to update the - # Calibre database without racing direct metadata.db writes. - extraFlags = ["--enable-local-write"]; - }; - - calibre-web = { - enable = true; - options.calibreLibrary = lib.head config.services.calibre-server.libraries; - }; - - nginx.virtualHosts."books.m7.rs" = { - forceSSL = true; - enableACME = true; - locations."/" = { - proxyPass = "http://localhost:${toString config.services.calibre-web.listen.port}"; - proxyWebsockets = true; - }; - }; - }; - - systemd.services = { - calibre-server.serviceConfig = { - CPUWeight = 50; - IOWeight = 50; - # Calibre-Web reads the library through the world-readable bits. - UMask = "0002"; - }; - - calibre-web.serviceConfig = { - CPUWeight = 50; - IOWeight = 50; - ReadWritePaths = lib.mkForce []; - }; - }; - - environment.persistence."/persist".directories = [ - { - directory = "/var/lib/calibre-web"; - user = config.services.calibre-web.user; - group = config.services.calibre-web.group; - mode = "0700"; - } - ]; -} diff --git a/hosts/nixos/merope/services/media/default.nix b/hosts/nixos/merope/services/media/default.nix index f1dcd33c..aa83910b 100644 --- a/hosts/nixos/merope/services/media/default.nix +++ b/hosts/nixos/merope/services/media/default.nix @@ -3,7 +3,7 @@ ./jellyfin.nix ./lidarr.nix ./readarr.nix - ./calibre-web.nix + ./kavita.nix ./sonarr.nix ./radarr.nix ./bazarr.nix diff --git a/hosts/nixos/merope/services/media/kavita.nix b/hosts/nixos/merope/services/media/kavita.nix new file mode 100644 index 00000000..f074eef1 --- /dev/null +++ b/hosts/nixos/merope/services/media/kavita.nix @@ -0,0 +1,63 @@ +{ + config, + pkgs, + ... +}: let + cfg = config.services.kavita; + tokenKeyFile = "${cfg.dataDir}/token-key"; +in { + services = { + kavita = { + enable = true; + inherit tokenKeyFile; + }; + + nginx.virtualHosts."books.m7.rs" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://localhost:${toString cfg.settings.Port}"; + proxyWebsockets = true; + }; + }; + }; + + # Kavita needs a stable, machine-local signing key. Generate it once in the + # persisted data directory before systemd loads it as a credential. + systemd.services = { + kavita-token = { + description = "Generate Kavita token key"; + before = ["kavita.service"]; + requiredBy = ["kavita.service"]; + unitConfig.RequiresMountsFor = cfg.dataDir; + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.user; + ExecStart = pkgs.writeShellScript "generate-kavita-token" '' + set -eu + if [[ ! -s ${tokenKeyFile} ]]; then + umask 077 + ${pkgs.coreutils}/bin/head -c 64 /dev/urandom \ + | ${pkgs.coreutils}/bin/base64 --wrap=0 > ${tokenKeyFile} + fi + ''; + }; + }; + + kavita.serviceConfig = { + CPUWeight = 50; + IOWeight = 50; + ReadOnlyPaths = ["/srv/media/books"]; + }; + }; + + environment.persistence."/persist".directories = [ + { + directory = cfg.dataDir; + user = cfg.user; + group = cfg.user; + mode = "0700"; + } + ]; +} diff --git a/hosts/nixos/merope/services/media/readarr.nix b/hosts/nixos/merope/services/media/readarr.nix index 9943ed24..11304be3 100644 --- a/hosts/nixos/merope/services/media/readarr.nix +++ b/hosts/nixos/merope/services/media/readarr.nix @@ -39,8 +39,7 @@ } ]; - # Readarr owns the library; Calibre Content Server writes through Readarr's - # group, while read-only consumers use the world-readable bits. + # Readarr owns the library; read-only consumers use the world-readable bits. systemd.tmpfiles.settings.srv-media-books."/srv/media/books".d = { user = config.services.readarr.user; group = config.services.readarr.group;