at 25.11-pre 10 kB view raw
1{ 2 config, 3 options, 4 lib, 5 pkgs, 6 ... 7}: 8let 9 10 name = "snapserver"; 11 12 cfg = config.services.snapserver; 13 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 }; 23 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 }; 32 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 + "\""; 48 49 optionalNull = val: ret: lib.optional (val != null) ret; 50 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 ); 75 76in 77{ 78 imports = [ 79 (lib.mkRenamedOptionModule 80 [ "services" "snapserver" "controlPort" ] 81 [ "services" "snapserver" "tcp" "port" ] 82 ) 83 ]; 84 85 ###### interface 86 87 options = { 88 89 services.snapserver = { 90 91 enable = lib.mkOption { 92 type = lib.types.bool; 93 default = false; 94 description = '' 95 Whether to enable snapserver. 96 ''; 97 }; 98 99 package = lib.options.mkPackageOption pkgs "snapcast" { }; 100 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 }; 109 110 port = lib.mkOption { 111 type = lib.types.port; 112 default = 1704; 113 description = '' 114 The port that snapclients can connect to. 115 ''; 116 }; 117 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 }; 125 126 inherit sampleFormat; 127 inherit codec; 128 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 }; 137 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 }; 146 147 sendToMuted = lib.mkOption { 148 type = lib.types.bool; 149 default = false; 150 description = '' 151 Send audio to muted clients. 152 ''; 153 }; 154 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 }; 162 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 }; 171 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 }; 179 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 }; 187 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 }; 196 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 }; 204 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 }; 213 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 }; 304 305 ###### implementation 306 307 config = lib.mkIf cfg.enable { 308 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 ); 319 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 ]; 331 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 }; 350 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 }; 356 357 meta = { 358 maintainers = with lib.maintainers; [ tobim ]; 359 }; 360 361}