nixos/snapserver: init

A nixos module for configuring the server side of pkgs.snapcast.
The module is named "snapserver" following upstream convention.
This commit does not provide module for the corresponding client.

Fix handling of port and controlPort

Fix stream uri generation & address review

Remove unused streams options & add description

Add missing description & Remove default fs path

Use types.port for ports & formatting improvements

Force mpd and mopidy to wait for snapserver
This commit is contained in:
Tobias Mayer 2019-02-17 11:08:48 +01:00
parent 4b2336ea28
commit 085751b63b
2 changed files with 218 additions and 0 deletions

@ -180,6 +180,7 @@
./services/audio/mpd.nix
./services/audio/mopidy.nix
./services/audio/slimserver.nix
./services/audio/snapserver.nix
./services/audio/squeezelite.nix
./services/audio/ympd.nix
./services/backup/bacula.nix

@ -0,0 +1,217 @@
{ config, lib, pkgs, ... }:
with lib;
let
package = "snapcast";
name = "snapserver";
cfg = config.services.snapserver;
# Using types.nullOr to inherit upstream defaults.
sampleFormat = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Default sample format.
'';
example = "48000:16:2";
};
codec = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Default audio compression method.
'';
example = "flac";
};
streamToOption = name: opt:
let
os = val:
optionalString (val != null) "${val}";
os' = prefixx: val:
optionalString (val != null) (prefixx + "${val}");
flatten = key: value:
"&${key}=${value}";
in
"-s ${opt.type}://" + os opt.location + "?" + os' "name=" name
+ concatStrings (mapAttrsToList flatten opt.query);
optionalNull = val: ret:
optional (val != null) ret;
optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
++ ["-p ${toString cfg.port}"]
++ ["--controlPort ${toString cfg.controlPort}"]
++ optionalNull cfg.sampleFormat "--sampleFormat ${cfg.sampleFormat}"
++ optionalNull cfg.codec "-c ${cfg.codec}"
++ optionalNull cfg.streamBuffer "--streamBuffer ${cfg.streamBuffer}"
++ optionalNull cfg.buffer "-b ${cfg.buffer}"
++ optional cfg.sendToMuted "--sendToMuted");
in {
###### interface
options = {
services.snapserver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable snapserver.
'';
};
port = mkOption {
type = types.port;
default = 1704;
description = ''
The port that snapclients can connect to.
'';
};
controlPort = mkOption {
type = types.port;
default = 1705;
description = ''
The port for control connections (JSON-RPC).
'';
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = ''
Whether to automatically open the specified ports in the firewall.
'';
};
inherit sampleFormat;
inherit codec;
streams = mkOption {
type = with types; attrsOf (submodule {
options = {
location = mkOption {
type = types.path;
description = ''
The location of the pipe.
'';
};
type = mkOption {
type = types.enum [ "pipe" "file" "process" "spotify" "airplay" ];
default = "pipe";
description = ''
The type of input stream.
'';
};
query = mkOption {
type = attrsOf str;
default = {};
description = ''
Key-value pairs that convey additional parameters about a stream.
'';
example = literalExample ''
# for type == "pipe":
{
mode = "listen";
};
# for type == "process":
{
params = "--param1 --param2";
logStderr = "true";
};
'';
};
inherit sampleFormat;
inherit codec;
};
});
default = { default = {}; };
description = ''
The definition for an input source.
'';
example = literalExample ''
{
mpd = {
type = "pipe";
location = "/run/snapserver/mpd";
sampleFormat = "48000:16:2";
codec = "pcm";
};
};
'';
};
streamBuffer = mkOption {
type = with types; nullOr int;
default = null;
description = ''
Stream read (input) buffer in ms.
'';
example = 20;
};
buffer = mkOption {
type = with types; nullOr int;
default = null;
description = ''
Network buffer in ms.
'';
example = 1000;
};
sendToMuted = mkOption {
type = types.bool;
default = false;
description = ''
Send audio to muted clients.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.snapserver = {
after = [ "network.target" ];
description = "Snapserver";
wantedBy = [ "multi-user.target" ];
before = [ "mpd.service" "mopidy.service" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
Type = "forking";
LimitRTPRIO = 50;
LimitRTTIME = "infinity";
NoNewPrivileges = true;
PIDFile = "/run/${name}/pid";
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
RestrictNamespaces = true;
RuntimeDirectory = name;
StateDirectory = name;
};
};
networking.firewall.allowedTCPPorts = optionals cfg.openFirewall [ cfg.port cfg.controlPort ];
};
meta = {
maintainers = with maintainers; [ tobim ];
};
}