nixos/feishin: init module

(cherry picked from commit fca2615fa3)
This commit is contained in:
Harish Rajagopal
2026-07-24 20:43:56 +02:00
parent 74b58cab88
commit 482ea6f02c
7 changed files with 235 additions and 0 deletions

View File

@@ -24,6 +24,8 @@
- [Nezha](https://github.com/nezhahq/nezha), a self-hosted, lightweight server and website monitoring and O&M tool. Available as [services.nezha](#opt-services.nezha.enable).
- [feishin](https://github.com/jeffvli/feishin), a modern self-hosted music player. Available as [services.feishin](#opt-services.feishin.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).
## Backward Incompatibilities {#sec-release-26.11-incompatibilities}

View File

@@ -1651,6 +1651,7 @@
./services/web-apps/engelsystem.nix
./services/web-apps/ente.nix
./services/web-apps/fediwall.nix
./services/web-apps/feishin.nix
./services/web-apps/fider.nix
./services/web-apps/filebrowser.nix
./services/web-apps/firefly-iii-data-importer.nix

View File

@@ -0,0 +1,156 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.feishin;
in
{
options.services.feishin = {
enable = lib.mkEnableOption "the web version of Feishin";
package = lib.mkPackageOption pkgs "feishin-web" { };
domain = lib.mkOption {
description = "Domain to use for the virtualhost.";
type = lib.types.str;
};
pathbase = lib.mkOption {
description = "URL path base to use for the virtualhost.";
type = lib.types.str;
default = "";
example = "/music";
};
settings = lib.mkOption {
description = ''
Configuration for the web version of Feishin, specified as key-value pairs.
Refer to <https://github.com/jeffvli/feishin#configuration>, and
<https://github.com/jeffvli/feishin/blob/development/settings.js.template>
for the template.
'';
type = lib.types.attrsOf lib.types.str;
default = {
ANALYTICS_DISABLED = "true";
};
example = {
ANALYTICS_DISABLED = "true";
SERVER_NAME = "My Server";
SERVER_LOCK = "true";
};
};
nginx = {
enable = lib.mkEnableOption "a virtualhost to serve Feishin through nginx";
virtualHost = lib.mkOption {
description = "Extra configuration for the nginx virtualhost for Feishin.";
type = lib.types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
default = { };
example = lib.literalExpression ''
{
serverAliases = [ "feishin.''${config.networking.domain}" ];
}
'';
};
};
caddy = {
enable = lib.mkEnableOption "a virtualhost to serve Feishin through Caddy";
virtualHost = lib.mkOption {
description = "Extra configuration for the Caddy virtualhost for Feishin.";
type = lib.types.submodule (
import ../web-servers/caddy/vhost-options.nix { cfg = config.services.caddy; }
);
default = { };
example = lib.literalExpression ''
{
serverAliases = [ "feishin.''${config.networking.domain}" ];
}
'';
};
};
};
config =
let
resolvedSettings =
cfg.settings // lib.optionalAttrs (cfg.pathbase != "") { PUBLIC_PATH = cfg.pathbase; };
settingsJs = ''
"use strict";
${lib.concatMapAttrsStringSep "\n" (name: value: ''window.${name} = "${value}";'') resolvedSettings}
'';
settingsJsDir = pkgs.writeTextDir "settings.js" settingsJs;
in
lib.mkIf cfg.enable {
services.nginx = lib.mkIf cfg.nginx.enable {
enable = lib.mkDefault true;
virtualHosts."${cfg.domain}" = lib.mkMerge [
cfg.nginx.virtualHost
{
root = lib.mkForce "${cfg.package}";
extraConfig = ''
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
'';
locations."${cfg.pathbase}/" = {
tryFiles = "$uri $uri/ /index.html =404";
};
locations."=${cfg.pathbase}/settings.js" = {
alias = settingsJsDir + "/settings.js";
extraConfig = ''
add_header Cache-Control "no-store";
'';
};
}
];
};
services.caddy = lib.mkIf cfg.caddy.enable {
enable = lib.mkDefault true;
virtualHosts."${cfg.domain}" = lib.mkMerge [
cfg.caddy.virtualHost
{
hostName = lib.mkForce cfg.domain;
extraConfig =
let
baseConfig = ''
encode
handle /settings.js {
header Cache-Control no-store
root ${settingsJsDir}
file_server
}
handle {
root ${cfg.package}
try_files {path} /index.html
file_server
}
'';
in
if (cfg.pathbase == "") then
baseConfig
else
''
handle_path ${cfg.pathbase}/* {
${baseConfig}
}
'';
}
];
};
};
meta.maintainers = with lib.maintainers; [
rharish
BatteredBunny
];
}

View File

@@ -560,6 +560,7 @@ in
fastnetmon-advanced = runTest ./fastnetmon-advanced.nix;
fcitx5 = runTest ./fcitx5;
fedimintd = runTest ./fedimintd.nix;
feishin = handleTest ./feishin { };
ferm = runTest ./ferm.nix;
ferretdb = import ./ferretdb.nix { inherit pkgs runTest; };
fider = runTest ./fider.nix;

View File

@@ -0,0 +1,36 @@
import ../make-test-python.nix (
{ lib, ... }:
{
name = "feishin-caddy";
meta.maintainers = with lib.maintainers; [
rharish
BatteredBunny
];
nodes.machine =
{ pkgs, ... }:
{
services.feishin = {
enable = true;
domain = "localhost:80";
pathbase = "/feishin";
settings = {
ANALYTICS_DISABLED = "true";
};
caddy.enable = true;
caddy.virtualHost.extraConfig = "tls internal";
};
# disable letsencrypt cert fetching
services.caddy.globalConfig = "auto_https disable_certs";
};
testScript = ''
machine.wait_for_unit("caddy.service")
machine.wait_for_open_port(80)
machine.succeed("curl -vvv --fail --show-error --silent --location --insecure http://localhost/feishin/")
assert "<title>Feishin</title>" in machine.succeed("curl --fail --show-error --silent --location --insecure http://localhost/feishin/")
assert "window.ANALYTICS_DISABLED = \"true\";" in machine.succeed("curl --fail --show-error --silent --location --insecure http://localhost/feishin/settings.js")
'';
}
)

View File

@@ -0,0 +1,6 @@
{ system, pkgs, ... }:
{
caddy = import ./caddy.nix { inherit system pkgs; };
nginx = import ./nginx.nix { inherit system pkgs; };
}

View File

@@ -0,0 +1,33 @@
import ../make-test-python.nix (
{ lib, ... }:
{
name = "feishin-nginx";
meta.maintainers = with lib.maintainers; [
rharish
BatteredBunny
];
nodes.machine =
{ pkgs, ... }:
{
services.feishin = {
enable = true;
domain = "localhost";
pathbase = "/feishin";
settings = {
ANALYTICS_DISABLED = "true";
};
nginx.enable = true;
};
};
testScript = ''
machine.wait_for_unit("nginx.service")
machine.wait_for_open_port(80)
machine.succeed("curl -vvv --fail --show-error --silent --location --insecure http://localhost/feishin/")
assert "<title>Feishin</title>" in machine.succeed("curl --fail --show-error --silent --location --insecure http://localhost/feishin/")
assert "window.ANALYTICS_DISABLED = \"true\";" in machine.succeed("curl --fail --show-error --silent --location --insecure http://localhost/feishin/settings.js")
'';
}
)