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)
This commit is contained in:
Gabriel Fontes
2026-08-17 00:39:15 -03:00
parent e93143e18f
commit 0a1f6f86c1
4 changed files with 65 additions and 60 deletions
@@ -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";
}
];
}
@@ -3,7 +3,7 @@
./jellyfin.nix
./lidarr.nix
./readarr.nix
./calibre-web.nix
./kavita.nix
./sonarr.nix
./radarr.nix
./bazarr.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";
}
];
}
@@ -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;