12 cfg = config.services.snapserver;
14 # Using types.nullOr to inherit upstream defaults.
15 sampleFormat = lib.mkOption {
16 type = with lib.types; nullOr str;
19 Default sample format.
21 example = "48000:16:2";
24 codec = lib.mkOption {
25 type = with lib.types; nullOr str;
28 Default audio compression method.
36 os = val: lib.optionalString (val != null) "${val}";
37 os' = prefix: val: lib.optionalString (val != null) (prefix + "${val}");
38 toQueryString = key: value: "&${key}=${value}";
40 "--stream.stream=\"${opt.type}://"
44 + os' "&sampleformat=" opt.sampleFormat
45 + os' "&codec=" opt.codec
46 + lib.concatStrings (lib.mapAttrsToList toQueryString opt.query)
49 optionalNull = val: ret: lib.optional (val != null) ret;
51 optionString = lib.concatStringsSep " " (
52 lib.mapAttrsToList streamToOption cfg.streams
54 ++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
55 ++ [ "--stream.port=${toString cfg.port}" ]
56 ++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
57 ++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
58 ++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}"
59 ++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}"
60 ++ lib.optional cfg.sendToMuted "--stream.send_to_muted"
62 ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
63 ++ lib.optionals cfg.tcp.enable [
64 "--tcp.bind_to_address=${cfg.tcp.listenAddress}"
65 "--tcp.port=${toString cfg.tcp.port}"
68 ++ [ "--http.enabled=${toString cfg.http.enable}" ]
69 ++ lib.optionals cfg.http.enable [
70 "--http.bind_to_address=${cfg.http.listenAddress}"
71 "--http.port=${toString cfg.http.port}"
73 ++ lib.optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\""
79 (lib.mkRenamedOptionModule
80 [ "services" "snapserver" "controlPort" ]
81 [ "services" "snapserver" "tcp" "port" ]
89 services.snapserver = {
91 enable = lib.mkOption {
92 type = lib.types.bool;
95 Whether to enable snapserver.
99 package = lib.options.mkPackageOption pkgs "snapcast" { };
101 listenAddress = lib.mkOption {
102 type = lib.types.str;
106 The address where snapclients can connect.
110 port = lib.mkOption {
111 type = lib.types.port;
114 The port that snapclients can connect to.
118 openFirewall = lib.mkOption {
119 type = lib.types.bool;
122 Whether to automatically open the specified ports in the firewall.
126 inherit sampleFormat;
129 streamBuffer = lib.mkOption {
130 type = with lib.types; nullOr int;
133 Stream read (input) buffer in ms.
138 buffer = lib.mkOption {
139 type = with lib.types; nullOr int;
142 Network buffer in ms.
147 sendToMuted = lib.mkOption {
148 type = lib.types.bool;
151 Send audio to muted clients.
155 tcp.enable = lib.mkOption {
156 type = lib.types.bool;
159 Whether to enable the JSON-RPC via TCP.
163 tcp.listenAddress = lib.mkOption {
164 type = lib.types.str;
168 The address where the TCP JSON-RPC listens on.
172 tcp.port = lib.mkOption {
173 type = lib.types.port;
176 The port where the TCP JSON-RPC listens on.
180 http.enable = lib.mkOption {
181 type = lib.types.bool;
184 Whether to enable the JSON-RPC via HTTP.
188 http.listenAddress = lib.mkOption {
189 type = lib.types.str;
193 The address where the HTTP JSON-RPC listens on.
197 http.port = lib.mkOption {
198 type = lib.types.port;
201 The port where the HTTP JSON-RPC listens on.
205 http.docRoot = lib.mkOption {
206 type = with lib.types; nullOr path;
207 default = pkgs.snapweb;
208 defaultText = lib.literalExpression "pkgs.snapweb";
210 Path to serve from the HTTP servers root.
214 streams = lib.mkOption {
219 location = lib.mkOption {
220 type = lib.types.oneOf [
225 For type `pipe` or `file`, the path to the pipe or file.
226 For type `librespot`, `airplay` or `process`, the path to the corresponding binary.
227 For type `tcp`, the `host:port` address to connect to or listen on.
228 For type `meta`, a list of stream names in the form `/one/two/...`. Don't forget the leading slash.
229 For type `alsa`, use an empty string.
231 example = lib.literalExpression ''
235 "/MyTCP/Spotify/MyPipe"
238 type = lib.mkOption {
239 type = lib.types.enum [
252 The type of input stream.
255 query = lib.mkOption {
259 Key-value pairs that convey additional parameters about a stream.
261 example = lib.literalExpression ''
262 # for type == "pipe":
266 # for type == "process":
268 params = "--param1 --param2";
275 # for type == "alsa":
281 inherit sampleFormat;
289 The definition for an input source.
291 example = lib.literalExpression ''
295 location = "/run/snapserver/mpd";
296 sampleFormat = "48000:16:2";
305 ###### implementation
307 config = lib.mkIf cfg.enable {
310 # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
311 lib.filter (w: w != "") (
314 lib.optionalString (v.type == "spotify") ''
315 services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
320 systemd.services.snapserver = {
325 description = "Snapserver";
326 wantedBy = [ "multi-user.target" ];
334 ExecStart = "${cfg.package}/bin/snapserver --daemon ${optionString}";
337 LimitRTTIME = "infinity";
338 NoNewPrivileges = true;
339 PIDFile = "/run/${name}/pid";
340 ProtectKernelTunables = true;
341 ProtectControlGroups = true;
342 ProtectKernelModules = true;
343 Restart = "on-failure";
344 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
345 RestrictNamespaces = true;
346 RuntimeDirectory = name;
347 StateDirectory = name;
351 networking.firewall.allowedTCPPorts =
352 lib.optionals cfg.openFirewall [ cfg.port ]
353 ++ lib.optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port
354 ++ lib.optional (cfg.openFirewall && cfg.http.enable) cfg.http.port;
358 maintainers = with lib.maintainers; [ tobim ];