at 23.11-pre 9.2 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 default = false; 105 description = lib.mdDoc '' 106 Whether to automatically open the specified ports in the firewall. 107 ''; 108 }; 109 110 inherit sampleFormat; 111 inherit codec; 112 113 streamBuffer = mkOption { 114 type = with types; nullOr int; 115 default = null; 116 description = lib.mdDoc '' 117 Stream read (input) buffer in ms. 118 ''; 119 example = 20; 120 }; 121 122 buffer = mkOption { 123 type = with types; nullOr int; 124 default = null; 125 description = lib.mdDoc '' 126 Network buffer in ms. 127 ''; 128 example = 1000; 129 }; 130 131 sendToMuted = mkOption { 132 type = types.bool; 133 default = false; 134 description = lib.mdDoc '' 135 Send audio to muted clients. 136 ''; 137 }; 138 139 tcp.enable = mkOption { 140 type = types.bool; 141 default = true; 142 description = lib.mdDoc '' 143 Whether to enable the JSON-RPC via TCP. 144 ''; 145 }; 146 147 tcp.listenAddress = mkOption { 148 type = types.str; 149 default = "::"; 150 example = "0.0.0.0"; 151 description = lib.mdDoc '' 152 The address where the TCP JSON-RPC listens on. 153 ''; 154 }; 155 156 tcp.port = mkOption { 157 type = types.port; 158 default = 1705; 159 description = lib.mdDoc '' 160 The port where the TCP JSON-RPC listens on. 161 ''; 162 }; 163 164 http.enable = mkOption { 165 type = types.bool; 166 default = true; 167 description = lib.mdDoc '' 168 Whether to enable the JSON-RPC via HTTP. 169 ''; 170 }; 171 172 http.listenAddress = mkOption { 173 type = types.str; 174 default = "::"; 175 example = "0.0.0.0"; 176 description = lib.mdDoc '' 177 The address where the HTTP JSON-RPC listens on. 178 ''; 179 }; 180 181 http.port = mkOption { 182 type = types.port; 183 default = 1780; 184 description = lib.mdDoc '' 185 The port where the HTTP JSON-RPC listens on. 186 ''; 187 }; 188 189 http.docRoot = mkOption { 190 type = with types; nullOr path; 191 default = null; 192 description = lib.mdDoc '' 193 Path to serve from the HTTP servers root. 194 ''; 195 }; 196 197 streams = mkOption { 198 type = with types; attrsOf (submodule { 199 options = { 200 location = mkOption { 201 type = types.oneOf [ types.path types.str ]; 202 description = lib.mdDoc '' 203 For type `pipe` or `file`, the path to the pipe or file. 204 For type `librespot`, `airplay` or `process`, the path to the corresponding binary. 205 For type `tcp`, the `host:port` address to connect to or listen on. 206 For type `meta`, a list of stream names in the form `/one/two/...`. Don't forget the leading slash. 207 For type `alsa`, use an empty string. 208 ''; 209 example = literalExpression '' 210 "/path/to/pipe" 211 "/path/to/librespot" 212 "192.168.1.2:4444" 213 "/MyTCP/Spotify/MyPipe" 214 ''; 215 }; 216 type = mkOption { 217 type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ]; 218 default = "pipe"; 219 description = lib.mdDoc '' 220 The type of input stream. 221 ''; 222 }; 223 query = mkOption { 224 type = attrsOf str; 225 default = {}; 226 description = lib.mdDoc '' 227 Key-value pairs that convey additional parameters about a stream. 228 ''; 229 example = literalExpression '' 230 # for type == "pipe": 231 { 232 mode = "create"; 233 }; 234 # for type == "process": 235 { 236 params = "--param1 --param2"; 237 logStderr = "true"; 238 }; 239 # for type == "tcp": 240 { 241 mode = "client"; 242 } 243 # for type == "alsa": 244 { 245 device = "hw:0,0"; 246 } 247 ''; 248 }; 249 inherit sampleFormat; 250 inherit codec; 251 }; 252 }); 253 default = { default = {}; }; 254 description = lib.mdDoc '' 255 The definition for an input source. 256 ''; 257 example = literalExpression '' 258 { 259 mpd = { 260 type = "pipe"; 261 location = "/run/snapserver/mpd"; 262 sampleFormat = "48000:16:2"; 263 codec = "pcm"; 264 }; 265 }; 266 ''; 267 }; 268 }; 269 }; 270 271 272 ###### implementation 273 274 config = mkIf cfg.enable { 275 276 warnings = 277 # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85 278 filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") '' 279 services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead. 280 '') cfg.streams); 281 282 systemd.services.snapserver = { 283 after = [ "network.target" ]; 284 description = "Snapserver"; 285 wantedBy = [ "multi-user.target" ]; 286 before = [ "mpd.service" "mopidy.service" ]; 287 288 serviceConfig = { 289 DynamicUser = true; 290 ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}"; 291 Type = "forking"; 292 LimitRTPRIO = 50; 293 LimitRTTIME = "infinity"; 294 NoNewPrivileges = true; 295 PIDFile = "/run/${name}/pid"; 296 ProtectKernelTunables = true; 297 ProtectControlGroups = true; 298 ProtectKernelModules = true; 299 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; 300 RestrictNamespaces = true; 301 RuntimeDirectory = name; 302 StateDirectory = name; 303 }; 304 }; 305 306 networking.firewall.allowedTCPPorts = 307 optionals cfg.openFirewall [ cfg.port ] 308 ++ optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port 309 ++ optional (cfg.openFirewall && cfg.http.enable) cfg.http.port; 310 }; 311 312 meta = { 313 maintainers = with maintainers; [ tobim ]; 314 }; 315 316}