at 23.05-pre 9.7 kB view raw
1{ config, options, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 name = "snapserver"; 8 9 cfg = config.services.snapserver; 10 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 }; 20 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 }; 29 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) + "\""; 41 42 optionalNull = val: ret: 43 optional (val != null) ret; 44 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}\""); 65 66in { 67 imports = [ 68 (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ]) 69 ]; 70 71 ###### interface 72 73 options = { 74 75 services.snapserver = { 76 77 enable = mkOption { 78 type = types.bool; 79 default = false; 80 description = lib.mdDoc '' 81 Whether to enable snapserver. 82 ''; 83 }; 84 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 }; 93 94 port = mkOption { 95 type = types.port; 96 default = 1704; 97 description = lib.mdDoc '' 98 The port that snapclients can connect to. 99 ''; 100 }; 101 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 }; 111 112 inherit sampleFormat; 113 inherit codec; 114 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 }; 123 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 }; 132 133 sendToMuted = mkOption { 134 type = types.bool; 135 default = false; 136 description = lib.mdDoc '' 137 Send audio to muted clients. 138 ''; 139 }; 140 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 }; 148 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 }; 157 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 }; 165 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 }; 173 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 }; 182 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 }; 190 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 }; 198 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 }; 272 273 274 ###### implementation 275 276 config = mkIf cfg.enable { 277 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 ''; 288 289 systemd.services.snapserver = { 290 after = [ "network.target" ]; 291 description = "Snapserver"; 292 wantedBy = [ "multi-user.target" ]; 293 before = [ "mpd.service" "mopidy.service" ]; 294 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 }; 312 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 }; 318 319 meta = { 320 maintainers = with maintainers; [ tobim ]; 321 }; 322 323}