zfs_unstable: 2.3.0-rc3 -> 2.3.0-rc4 (#365045)
[NixPkgs.git] / nixos / modules / services / audio / snapserver.nix
blob86ba7ec7183d81e0219f1cbd5353fe049d9fc0d4
2   config,
3   options,
4   lib,
5   pkgs,
6   ...
7 }:
8 let
10   name = "snapserver";
12   cfg = config.services.snapserver;
14   # Using types.nullOr to inherit upstream defaults.
15   sampleFormat = lib.mkOption {
16     type = with lib.types; nullOr str;
17     default = null;
18     description = ''
19       Default sample format.
20     '';
21     example = "48000:16:2";
22   };
24   codec = lib.mkOption {
25     type = with lib.types; nullOr str;
26     default = null;
27     description = ''
28       Default audio compression method.
29     '';
30     example = "flac";
31   };
33   streamToOption =
34     name: opt:
35     let
36       os = val: lib.optionalString (val != null) "${val}";
37       os' = prefix: val: lib.optionalString (val != null) (prefix + "${val}");
38       toQueryString = key: value: "&${key}=${value}";
39     in
40     "--stream.stream=\"${opt.type}://"
41     + os opt.location
42     + "?"
43     + os' "name=" name
44     + os' "&sampleformat=" opt.sampleFormat
45     + os' "&codec=" opt.codec
46     + lib.concatStrings (lib.mapAttrsToList toQueryString opt.query)
47     + "\"";
49   optionalNull = val: ret: lib.optional (val != null) ret;
51   optionString = lib.concatStringsSep " " (
52     lib.mapAttrsToList streamToOption cfg.streams
53     # global options
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"
61     # tcp json rpc
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}"
66     ]
67     # http json rpc
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}"
72     ]
73     ++ lib.optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\""
74   );
78   imports = [
79     (lib.mkRenamedOptionModule
80       [ "services" "snapserver" "controlPort" ]
81       [ "services" "snapserver" "tcp" "port" ]
82     )
83   ];
85   ###### interface
87   options = {
89     services.snapserver = {
91       enable = lib.mkOption {
92         type = lib.types.bool;
93         default = false;
94         description = ''
95           Whether to enable snapserver.
96         '';
97       };
99       package = lib.options.mkPackageOption pkgs "snapcast" { };
101       listenAddress = lib.mkOption {
102         type = lib.types.str;
103         default = "::";
104         example = "0.0.0.0";
105         description = ''
106           The address where snapclients can connect.
107         '';
108       };
110       port = lib.mkOption {
111         type = lib.types.port;
112         default = 1704;
113         description = ''
114           The port that snapclients can connect to.
115         '';
116       };
118       openFirewall = lib.mkOption {
119         type = lib.types.bool;
120         default = false;
121         description = ''
122           Whether to automatically open the specified ports in the firewall.
123         '';
124       };
126       inherit sampleFormat;
127       inherit codec;
129       streamBuffer = lib.mkOption {
130         type = with lib.types; nullOr int;
131         default = null;
132         description = ''
133           Stream read (input) buffer in ms.
134         '';
135         example = 20;
136       };
138       buffer = lib.mkOption {
139         type = with lib.types; nullOr int;
140         default = null;
141         description = ''
142           Network buffer in ms.
143         '';
144         example = 1000;
145       };
147       sendToMuted = lib.mkOption {
148         type = lib.types.bool;
149         default = false;
150         description = ''
151           Send audio to muted clients.
152         '';
153       };
155       tcp.enable = lib.mkOption {
156         type = lib.types.bool;
157         default = true;
158         description = ''
159           Whether to enable the JSON-RPC via TCP.
160         '';
161       };
163       tcp.listenAddress = lib.mkOption {
164         type = lib.types.str;
165         default = "::";
166         example = "0.0.0.0";
167         description = ''
168           The address where the TCP JSON-RPC listens on.
169         '';
170       };
172       tcp.port = lib.mkOption {
173         type = lib.types.port;
174         default = 1705;
175         description = ''
176           The port where the TCP JSON-RPC listens on.
177         '';
178       };
180       http.enable = lib.mkOption {
181         type = lib.types.bool;
182         default = true;
183         description = ''
184           Whether to enable the JSON-RPC via HTTP.
185         '';
186       };
188       http.listenAddress = lib.mkOption {
189         type = lib.types.str;
190         default = "::";
191         example = "0.0.0.0";
192         description = ''
193           The address where the HTTP JSON-RPC listens on.
194         '';
195       };
197       http.port = lib.mkOption {
198         type = lib.types.port;
199         default = 1780;
200         description = ''
201           The port where the HTTP JSON-RPC listens on.
202         '';
203       };
205       http.docRoot = lib.mkOption {
206         type = with lib.types; nullOr path;
207         default = pkgs.snapweb;
208         defaultText = lib.literalExpression "pkgs.snapweb";
209         description = ''
210           Path to serve from the HTTP servers root.
211         '';
212       };
214       streams = lib.mkOption {
215         type =
216           with lib.types;
217           attrsOf (submodule {
218             options = {
219               location = lib.mkOption {
220                 type = lib.types.oneOf [
221                   lib.types.path
222                   lib.types.str
223                 ];
224                 description = ''
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.
230                 '';
231                 example = lib.literalExpression ''
232                   "/path/to/pipe"
233                   "/path/to/librespot"
234                   "192.168.1.2:4444"
235                   "/MyTCP/Spotify/MyPipe"
236                 '';
237               };
238               type = lib.mkOption {
239                 type = lib.types.enum [
240                   "pipe"
241                   "librespot"
242                   "airplay"
243                   "file"
244                   "process"
245                   "tcp"
246                   "alsa"
247                   "spotify"
248                   "meta"
249                 ];
250                 default = "pipe";
251                 description = ''
252                   The type of input stream.
253                 '';
254               };
255               query = lib.mkOption {
256                 type = attrsOf str;
257                 default = { };
258                 description = ''
259                   Key-value pairs that convey additional parameters about a stream.
260                 '';
261                 example = lib.literalExpression ''
262                   # for type == "pipe":
263                   {
264                     mode = "create";
265                   };
266                   # for type == "process":
267                   {
268                     params = "--param1 --param2";
269                     logStderr = "true";
270                   };
271                   # for type == "tcp":
272                   {
273                     mode = "client";
274                   }
275                   # for type == "alsa":
276                   {
277                     device = "hw:0,0";
278                   }
279                 '';
280               };
281               inherit sampleFormat;
282               inherit codec;
283             };
284           });
285         default = {
286           default = { };
287         };
288         description = ''
289           The definition for an input source.
290         '';
291         example = lib.literalExpression ''
292           {
293             mpd = {
294               type = "pipe";
295               location = "/run/snapserver/mpd";
296               sampleFormat = "48000:16:2";
297               codec = "pcm";
298             };
299           };
300         '';
301       };
302     };
303   };
305   ###### implementation
307   config = lib.mkIf cfg.enable {
309     warnings =
310       # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
311       lib.filter (w: w != "") (
312         lib.mapAttrsToList (
313           k: v:
314           lib.optionalString (v.type == "spotify") ''
315             services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
316           ''
317         ) cfg.streams
318       );
320     systemd.services.snapserver = {
321       after = [
322         "network.target"
323         "nss-lookup.target"
324       ];
325       description = "Snapserver";
326       wantedBy = [ "multi-user.target" ];
327       before = [
328         "mpd.service"
329         "mopidy.service"
330       ];
332       serviceConfig = {
333         DynamicUser = true;
334         ExecStart = "${cfg.package}/bin/snapserver --daemon ${optionString}";
335         Type = "forking";
336         LimitRTPRIO = 50;
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;
348       };
349     };
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;
355   };
357   meta = {
358     maintainers = with lib.maintainers; [ tobim ];
359   };