python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / audio / snapserver.nix
blobfdc1f605bb32ab47b0da9231bb359c38c42ef3d3
1 { config, options, lib, pkgs, ... }:
3 with lib;
5 let
7   name = "snapserver";
9   cfg = config.services.snapserver;
11   # Using types.nullOr to inherit upstream defaults.
12   sampleFormat = mkOption {
13     type = with types; nullOr str;
14     default = null;
15     description = lib.mdDoc ''
16       Default sample format.
17     '';
18     example = "48000:16:2";
19   };
21   codec = mkOption {
22     type = with types; nullOr str;
23     default = null;
24     description = lib.mdDoc ''
25       Default audio compression method.
26     '';
27     example = "flac";
28   };
30   streamToOption = name: opt:
31     let
32       os = val:
33         optionalString (val != null) "${val}";
34       os' = prefix: val:
35         optionalString (val != null) (prefix + "${val}");
36       flatten = key: value:
37         "&${key}=${value}";
38     in
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
46     # global options
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"
54     # tcp json rpc
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}" ]
59      # http json rpc
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}\"");
66 in {
67   imports = [
68     (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
69   ];
71   ###### interface
73   options = {
75     services.snapserver = {
77       enable = mkOption {
78         type = types.bool;
79         default = false;
80         description = lib.mdDoc ''
81           Whether to enable snapserver.
82         '';
83       };
85       listenAddress = mkOption {
86         type = types.str;
87         default = "::";
88         example = "0.0.0.0";
89         description = lib.mdDoc ''
90           The address where snapclients can connect.
91         '';
92       };
94       port = mkOption {
95         type = types.port;
96         default = 1704;
97         description = lib.mdDoc ''
98           The port that snapclients can connect to.
99         '';
100       };
102       openFirewall = mkOption {
103         type = types.bool;
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.
106         default = true;
107         description = lib.mdDoc ''
108           Whether to automatically open the specified ports in the firewall.
109         '';
110       };
112       inherit sampleFormat;
113       inherit codec;
115       streamBuffer = mkOption {
116         type = with types; nullOr int;
117         default = null;
118         description = lib.mdDoc ''
119           Stream read (input) buffer in ms.
120         '';
121         example = 20;
122       };
124       buffer = mkOption {
125         type = with types; nullOr int;
126         default = null;
127         description = lib.mdDoc ''
128           Network buffer in ms.
129         '';
130         example = 1000;
131       };
133       sendToMuted = mkOption {
134         type = types.bool;
135         default = false;
136         description = lib.mdDoc ''
137           Send audio to muted clients.
138         '';
139       };
141       tcp.enable = mkOption {
142         type = types.bool;
143         default = true;
144         description = lib.mdDoc ''
145           Whether to enable the JSON-RPC via TCP.
146         '';
147       };
149       tcp.listenAddress = mkOption {
150         type = types.str;
151         default = "::";
152         example = "0.0.0.0";
153         description = lib.mdDoc ''
154           The address where the TCP JSON-RPC listens on.
155         '';
156       };
158       tcp.port = mkOption {
159         type = types.port;
160         default = 1705;
161         description = lib.mdDoc ''
162           The port where the TCP JSON-RPC listens on.
163         '';
164       };
166       http.enable = mkOption {
167         type = types.bool;
168         default = true;
169         description = lib.mdDoc ''
170           Whether to enable the JSON-RPC via HTTP.
171         '';
172       };
174       http.listenAddress = mkOption {
175         type = types.str;
176         default = "::";
177         example = "0.0.0.0";
178         description = lib.mdDoc ''
179           The address where the HTTP JSON-RPC listens on.
180         '';
181       };
183       http.port = mkOption {
184         type = types.port;
185         default = 1780;
186         description = lib.mdDoc ''
187           The port where the HTTP JSON-RPC listens on.
188         '';
189       };
191       http.docRoot = mkOption {
192         type = with types; nullOr path;
193         default = null;
194         description = lib.mdDoc ''
195           Path to serve from the HTTP servers root.
196         '';
197       };
199       streams = mkOption {
200         type = with types; attrsOf (submodule {
201           options = {
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.
210               '';
211               example = literalExpression ''
212                 "/path/to/pipe"
213                 "/path/to/librespot"
214                 "192.168.1.2:4444"
215                 "/MyTCP/Spotify/MyPipe"
216               '';
217             };
218             type = mkOption {
219               type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
220               default = "pipe";
221               description = lib.mdDoc ''
222                 The type of input stream.
223               '';
224             };
225             query = mkOption {
226               type = attrsOf str;
227               default = {};
228               description = lib.mdDoc ''
229                 Key-value pairs that convey additional parameters about a stream.
230               '';
231               example = literalExpression ''
232                 # for type == "pipe":
233                 {
234                   mode = "create";
235                 };
236                 # for type == "process":
237                 {
238                   params = "--param1 --param2";
239                   logStderr = "true";
240                 };
241                 # for type == "tcp":
242                 {
243                   mode = "client";
244                 }
245                 # for type == "alsa":
246                 {
247                   device = "hw:0,0";
248                 }
249               '';
250             };
251             inherit sampleFormat;
252             inherit codec;
253           };
254         });
255         default = { default = {}; };
256         description = lib.mdDoc ''
257           The definition for an input source.
258         '';
259         example = literalExpression ''
260           {
261             mpd = {
262               type = "pipe";
263               location = "/run/snapserver/mpd";
264               sampleFormat = "48000:16:2";
265               codec = "pcm";
266             };
267           };
268         '';
269       };
270     };
271   };
274   ###### implementation
276   config = mkIf cfg.enable {
278     warnings =
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.
287       '';
289     systemd.services.snapserver = {
290       after = [ "network.target" ];
291       description = "Snapserver";
292       wantedBy = [ "multi-user.target" ];
293       before = [ "mpd.service" "mopidy.service" ];
295       serviceConfig = {
296         DynamicUser = true;
297         ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
298         Type = "forking";
299         LimitRTPRIO = 50;
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;
310       };
311     };
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;
317   };
319   meta = {
320     maintainers = with maintainers; [ tobim ];
321   };