1 { config, options, lib, pkgs, ... }:
6 cfg = config.services.snapserver;
8 # Using types.nullOr to inherit upstream defaults.
9 sampleFormat = lib.mkOption {
10 type = with lib.types; nullOr str;
13 Default sample format.
15 example = "48000:16:2";
18 codec = lib.mkOption {
19 type = with lib.types; nullOr str;
22 Default audio compression method.
27 streamToOption = name: opt:
30 lib.optionalString (val != null) "${val}";
32 lib.optionalString (val != null) (prefix + "${val}");
33 toQueryString = key: value:
36 "--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name
37 + os' "&sampleformat=" opt.sampleFormat + os' "&codec=" opt.codec
38 + lib.concatStrings (lib.mapAttrsToList toQueryString opt.query) + "\"";
40 optionalNull = val: ret:
41 lib.optional (val != null) ret;
43 optionString = lib.concatStringsSep " " (lib.mapAttrsToList streamToOption cfg.streams
45 ++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
46 ++ [ "--stream.port=${toString cfg.port}" ]
47 ++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
48 ++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
49 ++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}"
50 ++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}"
51 ++ lib.optional cfg.sendToMuted "--stream.send_to_muted"
53 ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
54 ++ lib.optionals cfg.tcp.enable [
55 "--tcp.bind_to_address=${cfg.tcp.listenAddress}"
56 "--tcp.port=${toString cfg.tcp.port}" ]
58 ++ [ "--http.enabled=${toString cfg.http.enable}" ]
59 ++ lib.optionals cfg.http.enable [
60 "--http.bind_to_address=${cfg.http.listenAddress}"
61 "--http.port=${toString cfg.http.port}"
62 ] ++ lib.optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"");
66 (lib.mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
73 services.snapserver = {
75 enable = lib.mkOption {
76 type = lib.types.bool;
79 Whether to enable snapserver.
83 listenAddress = lib.mkOption {
88 The address where snapclients can connect.
93 type = lib.types.port;
96 The port that snapclients can connect to.
100 openFirewall = lib.mkOption {
101 type = lib.types.bool;
104 Whether to automatically open the specified ports in the firewall.
108 inherit sampleFormat;
111 streamBuffer = lib.mkOption {
112 type = with lib.types; nullOr int;
115 Stream read (input) buffer in ms.
120 buffer = lib.mkOption {
121 type = with lib.types; nullOr int;
124 Network buffer in ms.
129 sendToMuted = lib.mkOption {
130 type = lib.types.bool;
133 Send audio to muted clients.
137 tcp.enable = lib.mkOption {
138 type = lib.types.bool;
141 Whether to enable the JSON-RPC via TCP.
145 tcp.listenAddress = lib.mkOption {
146 type = lib.types.str;
150 The address where the TCP JSON-RPC listens on.
154 tcp.port = lib.mkOption {
155 type = lib.types.port;
158 The port where the TCP JSON-RPC listens on.
162 http.enable = lib.mkOption {
163 type = lib.types.bool;
166 Whether to enable the JSON-RPC via HTTP.
170 http.listenAddress = lib.mkOption {
171 type = lib.types.str;
175 The address where the HTTP JSON-RPC listens on.
179 http.port = lib.mkOption {
180 type = lib.types.port;
183 The port where the HTTP JSON-RPC listens on.
187 http.docRoot = lib.mkOption {
188 type = with lib.types; nullOr path;
191 Path to serve from the HTTP servers root.
195 streams = lib.mkOption {
196 type = with lib.types; attrsOf (submodule {
198 location = lib.mkOption {
199 type = lib.types.oneOf [ lib.types.path lib.types.str ];
201 For type `pipe` or `file`, the path to the pipe or file.
202 For type `librespot`, `airplay` or `process`, the path to the corresponding binary.
203 For type `tcp`, the `host:port` address to connect to or listen on.
204 For type `meta`, a list of stream names in the form `/one/two/...`. Don't forget the leading slash.
205 For type `alsa`, use an empty string.
207 example = lib.literalExpression ''
211 "/MyTCP/Spotify/MyPipe"
214 type = lib.mkOption {
215 type = lib.types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
218 The type of input stream.
221 query = lib.mkOption {
225 Key-value pairs that convey additional parameters about a stream.
227 example = lib.literalExpression ''
228 # for type == "pipe":
232 # for type == "process":
234 params = "--param1 --param2";
241 # for type == "alsa":
247 inherit sampleFormat;
251 default = { default = {}; };
253 The definition for an input source.
255 example = lib.literalExpression ''
259 location = "/run/snapserver/mpd";
260 sampleFormat = "48000:16:2";
270 ###### implementation
272 config = lib.mkIf cfg.enable {
275 # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
276 lib.filter (w: w != "") (lib.mapAttrsToList (k: v: lib.optionalString (v.type == "spotify") ''
277 services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
280 systemd.services.snapserver = {
281 after = [ "network.target" "nss-lookup.target" ];
282 description = "Snapserver";
283 wantedBy = [ "multi-user.target" ];
284 before = [ "mpd.service" "mopidy.service" ];
288 ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
291 LimitRTTIME = "infinity";
292 NoNewPrivileges = true;
293 PIDFile = "/run/${name}/pid";
294 ProtectKernelTunables = true;
295 ProtectControlGroups = true;
296 ProtectKernelModules = true;
297 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
298 RestrictNamespaces = true;
299 RuntimeDirectory = name;
300 StateDirectory = name;
304 networking.firewall.allowedTCPPorts =
305 lib.optionals cfg.openFirewall [ cfg.port ]
306 ++ lib.optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port
307 ++ lib.optional (cfg.openFirewall && cfg.http.enable) cfg.http.port;
311 maintainers = with lib.maintainers; [ tobim ];