diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md
index 32f2bab23a62..b8a10ea95106 100644
--- a/nixos/doc/manual/release-notes/rl-2611.section.md
+++ b/nixos/doc/manual/release-notes/rl-2611.section.md
@@ -24,6 +24,8 @@
- [Freescout](https://freescout.net/), a free, open source Helpdesk and shared mailbox. Available as [services.freescout](#opt-services.freescout.enable).
+- [Koito](https://koito.io/), a modern, themeable scrobbler that you can use with any program that scrobbles to a custom ListenBrainz URL. Available as [services.koito](#opt-services.koito.enable).
+
- [FlapAlerted](https://github.com/Kioubit/FlapAlerted), detects BGP flapping events and provides statistics based on BGP update messages. Available as [services.flap-alerted](#opt-services.flap-alerted.enable).
- [Unpackerr](https://unpackerr.zip), extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder. Available as [services.unpackerr](#opt-services.unpackerr.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 6c9f8c03dc5e..9431d4f768e0 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1707,6 +1707,7 @@
./services/web-apps/kavita.nix
./services/web-apps/keycloak.nix
./services/web-apps/kimai.nix
+ ./services/web-apps/koito.nix
./services/web-apps/komga.nix
./services/web-apps/lanraragi.nix
./services/web-apps/lasuite-docs.nix
diff --git a/nixos/modules/services/web-apps/koito.nix b/nixos/modules/services/web-apps/koito.nix
new file mode 100644
index 000000000000..f4b78770aed5
--- /dev/null
+++ b/nixos/modules/services/web-apps/koito.nix
@@ -0,0 +1,140 @@
+{
+ lib,
+ config,
+ pkgs,
+ ...
+}:
+let
+ cfg = config.services.koito;
+
+ inherit (lib)
+ getExe
+ mkEnableOption
+ mkIf
+ mkOption
+ mkPackageOption
+ types
+ ;
+in
+{
+ options.services.koito = {
+ enable = mkEnableOption "koito";
+
+ package = mkPackageOption pkgs "koito" { };
+
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Open the appropriate ports in the firewall for Koito.";
+ };
+
+ environment = mkOption {
+ type = types.submodule {
+ freeformType = types.attrsOf types.str;
+ options = {
+ KOITO_BIND_ADDR = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ example = "0.0.0.0";
+ description = "The IP address to bind the Koito server to.";
+ };
+ KOITO_LISTEN_PORT = mkOption {
+ type = types.port;
+ default = 4110;
+ description = "TCP port for the Koito server.";
+ };
+ KOITO_CONFIG_DIR = mkOption {
+ type = types.path;
+ default = "/var/lib/koito";
+ description = "Directory for Koito import folders and image caches.";
+ };
+ };
+ };
+ default = { };
+ example = {
+ KOITO_DEFAULT_THEME = "black";
+ KOITO_LOGIN_GATE = "true";
+ };
+ description = ''
+ Environment variables to pass to the Koito service.
+ See for available options.
+ '';
+ };
+
+ environmentFile = mkOption {
+ type = types.nullOr types.path;
+ example = "/run/secrets/koito";
+ default = null;
+ description = ''
+ Path of a file with extra environment variables to be loaded from disk.
+ This file is not added to the nix store, so it can be used to pass secrets to Koito.
+ See for available options.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.koito = {
+ description = "Koito - modern scrobbler";
+ after = [ "network-online.target" ];
+ wants = [ "network-online.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ Environment = lib.mapAttrsToList (k: v: "${k}=${if builtins.isInt v then toString v else v}") (
+ lib.filterAttrs (_: v: v != null) cfg.environment
+ );
+ DynamicUser = true;
+ ExecStart = getExe cfg.package;
+ StateDirectory = "koito";
+ EnvironmentFile = cfg.environmentFile;
+
+ 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 = [ cfg.environment.KOITO_LISTEN_PORT ];
+ };
+ };
+
+ meta = {
+ maintainers = with lib.maintainers; [ iv-nn ];
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 6a1c6f8e1065..5b3601d1822e 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -889,6 +889,7 @@ in
kmonad = runTest ./kmonad.nix;
kmscon = runTest ./kmscon.nix;
knot = runTest ./knot.nix;
+ koito = runTest ./web-apps/koito.nix;
komga = runTest ./komga.nix;
komodo-periphery = runTest ./komodo-periphery.nix;
krb5 = discoverTests (import ./krb5);
diff --git a/nixos/tests/web-apps/koito.nix b/nixos/tests/web-apps/koito.nix
new file mode 100644
index 000000000000..8839d4bfc57f
--- /dev/null
+++ b/nixos/tests/web-apps/koito.nix
@@ -0,0 +1,20 @@
+{ ... }:
+{
+ name = "koito";
+
+ nodes.machine = {
+ services.koito.enable = true;
+ };
+
+ testScript =
+ { nodes, ... }:
+ let
+ port = toString nodes.machine.services.koito.environment.KOITO_LISTEN_PORT;
+ in
+ ''
+ machine.wait_for_unit('koito.service')
+
+ machine.wait_for_open_port(${port})
+ machine.succeed('curl --fail http://localhost:${port}')
+ '';
+}
diff --git a/pkgs/by-name/ko/koito/package.nix b/pkgs/by-name/ko/koito/package.nix
index a95823d4b7c7..1b088365b8f8 100644
--- a/pkgs/by-name/ko/koito/package.nix
+++ b/pkgs/by-name/ko/koito/package.nix
@@ -6,6 +6,7 @@
pkg-config,
vips,
makeWrapper,
+ nixosTests,
}:
buildGoModule (finalAttrs: {
pname = "koito";
@@ -44,10 +45,15 @@ buildGoModule (finalAttrs: {
'';
passthru = {
- updateScript = ./update.sh;
client = callPackage ./client.nix {
inherit (finalAttrs) src version;
};
+
+ tests = {
+ inherit (nixosTests) koito;
+ };
+
+ updateScript = ./update.sh;
};
meta = {