1 { config, options, lib, pkgs, ... }:
9 cfg = config.services.snapserver;
11 # Using types.nullOr to inherit upstream defaults.
12 sampleFormat = mkOption {
13 type = with types; nullOr str;
15 description = lib.mdDoc ''
16 Default sample format.
18 example = "48000:16:2";
22 type = with types; nullOr str;
24 description = lib.mdDoc ''
25 Default audio compression method.
30 streamToOption = name: opt:
33 optionalString (val != null) "${val}";
35 optionalString (val != null) (prefix + "${val}");
39 "--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name
40 + concatStrings (mapAttrsToList flatten opt.query) + "\"";
42 optionalNull = val: ret:
43 optional (val != null) ret;
45 optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
47 ++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
48 ++ [ "--stream.port=${toString cfg.port}" ]
49 ++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
50 ++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
51 ++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}"
52 ++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}"
53 ++ optional cfg.sendToMuted "--stream.send_to_muted"
55 ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
56 ++ optionals cfg.tcp.enable [
57 "--tcp.bind_to_address=${cfg.tcp.listenAddress}"
58 "--tcp.port=${toString cfg.tcp.port}" ]
60 ++ [ "--http.enabled=${toString cfg.http.enable}" ]
61 ++ optionals cfg.http.enable [
62 "--http.bind_to_address=${cfg.http.listenAddress}"
63 "--http.port=${toString cfg.http.port}"
64 ] ++ optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"");
68 (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
75 services.snapserver = {
80 description = lib.mdDoc ''
81 Whether to enable snapserver.
85 listenAddress = mkOption {
89 description = lib.mdDoc ''
90 The address where snapclients can connect.
97 description = lib.mdDoc ''
98 The port that snapclients can connect to.
102 openFirewall = mkOption {
104 # Make the behavior consistent with other services. Set the default to
105 # false and remove the accompanying warning after NixOS 22.05 is released.
107 description = lib.mdDoc ''
108 Whether to automatically open the specified ports in the firewall.
112 inherit sampleFormat;
115 streamBuffer = mkOption {
116 type = with types; nullOr int;
118 description = lib.mdDoc ''
119 Stream read (input) buffer in ms.
125 type = with types; nullOr int;
127 description = lib.mdDoc ''
128 Network buffer in ms.
133 sendToMuted = mkOption {
136 description = lib.mdDoc ''
137 Send audio to muted clients.
141 tcp.enable = mkOption {
144 description = lib.mdDoc ''
145 Whether to enable the JSON-RPC via TCP.
149 tcp.listenAddress = mkOption {
153 description = lib.mdDoc ''
154 The address where the TCP JSON-RPC listens on.
158 tcp.port = mkOption {
161 description = lib.mdDoc ''
162 The port where the TCP JSON-RPC listens on.
166 http.enable = mkOption {
169 description = lib.mdDoc ''
170 Whether to enable the JSON-RPC via HTTP.
174 http.listenAddress = mkOption {
178 description = lib.mdDoc ''
179 The address where the HTTP JSON-RPC listens on.
183 http.port = mkOption {
186 description = lib.mdDoc ''
187 The port where the HTTP JSON-RPC listens on.
191 http.docRoot = mkOption {
192 type = with types; nullOr path;
194 description = lib.mdDoc ''
195 Path to serve from the HTTP servers root.
200 type = with types; attrsOf (submodule {
202 location = mkOption {
203 type = types.oneOf [ types.path types.str ];
204 description = lib.mdDoc ''
205 For type `pipe` or `file`, the path to the pipe or file.
206 For type `librespot`, `airplay` or `process`, the path to the corresponding binary.
207 For type `tcp`, the `host:port` address to connect to or listen on.
208 For type `meta`, a list of stream names in the form `/one/two/...`. Don't forget the leading slash.
209 For type `alsa`, use an empty string.
211 example = literalExpression ''
215 "/MyTCP/Spotify/MyPipe"
219 type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
221 description = lib.mdDoc ''
222 The type of input stream.
228 description = lib.mdDoc ''
229 Key-value pairs that convey additional parameters about a stream.
231 example = literalExpression ''
232 # for type == "pipe":
236 # for type == "process":
238 params = "--param1 --param2";
245 # for type == "alsa":
251 inherit sampleFormat;
255 default = { default = {}; };
256 description = lib.mdDoc ''
257 The definition for an input source.
259 example = literalExpression ''
263 location = "/run/snapserver/mpd";
264 sampleFormat = "48000:16:2";
274 ###### implementation
276 config = mkIf cfg.enable {
279 # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
280 filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
281 services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
282 '' else "") cfg.streams)
283 # Remove this warning after NixOS 22.05 is released.
284 ++ optional (options.services.snapserver.openFirewall.highestPrio >= (mkOptionDefault null).priority) ''
285 services.snapserver.openFirewall will no longer default to true starting with NixOS 22.11.
286 Enable it explicitly if you need to control Snapserver remotely.
289 systemd.services.snapserver = {
290 after = [ "network.target" ];
291 description = "Snapserver";
292 wantedBy = [ "multi-user.target" ];
293 before = [ "mpd.service" "mopidy.service" ];
297 ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
300 LimitRTTIME = "infinity";
301 NoNewPrivileges = true;
302 PIDFile = "/run/${name}/pid";
303 ProtectKernelTunables = true;
304 ProtectControlGroups = true;
305 ProtectKernelModules = true;
306 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
307 RestrictNamespaces = true;
308 RuntimeDirectory = name;
309 StateDirectory = name;
313 networking.firewall.allowedTCPPorts =
314 optionals cfg.openFirewall [ cfg.port ]
315 ++ optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port
316 ++ optional (cfg.openFirewall && cfg.http.enable) cfg.http.port;
320 maintainers = with maintainers; [ tobim ];