diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix index b0b9264e816662b..f614f0ba3e10c19 100644 --- a/nixos/modules/services/audio/snapserver.nix +++ b/nixos/modules/services/audio/snapserver.nix @@ -31,27 +31,42 @@ let let os = val: optionalString (val != null) "${val}"; - os' = prefixx: val: - optionalString (val != null) (prefixx + "${val}"); + os' = prefix: val: + optionalString (val != null) (prefix + "${val}"); flatten = key: value: "&${key}=${value}"; in - "-s ${opt.type}://" + os opt.location + "?" + os' "name=" name - + concatStrings (mapAttrsToList flatten opt.query); + "--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name + + concatStrings (mapAttrsToList flatten opt.query) + "\""; optionalNull = val: ret: optional (val != null) ret; optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams - ++ ["-p ${toString cfg.port}"] - ++ ["--controlPort ${toString cfg.controlPort}"] - ++ optionalNull cfg.sampleFormat "--sampleFormat ${cfg.sampleFormat}" - ++ optionalNull cfg.codec "-c ${cfg.codec}" - ++ optionalNull cfg.streamBuffer "--streamBuffer ${cfg.streamBuffer}" - ++ optionalNull cfg.buffer "-b ${cfg.buffer}" - ++ optional cfg.sendToMuted "--sendToMuted"); + # global options + ++ [ "--stream.bind_to_address ${cfg.listenAddress}" ] + ++ [ "--stream.port ${toString cfg.port}" ] + ++ optionalNull cfg.sampleFormat "--stream.sampleformat ${cfg.sampleFormat}" + ++ optionalNull cfg.codec "--stream.codec ${cfg.codec}" + ++ optionalNull cfg.streamBuffer "--stream.stream_buffer ${cfg.streamBuffer}" + ++ optionalNull cfg.buffer "--stream.buffer ${cfg.buffer}" + ++ optional cfg.sendToMuted "--stream.send_to_muted" + # tcp json rpc + ++ [ "--tcp.enabled ${toString cfg.tcp.enable}" ] + ++ optionals cfg.tcp.enable [ + "--tcp.address ${cfg.tcp.listenAddress}" + "--tcp.port ${toString cfg.tcp.port}" ] + # http json rpc + ++ [ "--http.enabled ${toString cfg.http.enable}" ] + ++ optionals cfg.http.enable [ + "--http.address ${cfg.http.listenAddress}" + "--http.port ${toString cfg.http.port}" + ] ++ optional (cfg.http.docRoot != null) "--http.doc_root \"${toString cfg.http.docRoot}\""); in { + imports = [ + (mkRenamedOptionModule [ "services" "snapserver" "controlPort"] [ "services" "snapserver" "tcp" "port" ]) + ]; ###### interface @@ -67,6 +82,15 @@ in { ''; }; + listenAddress = mkOption { + type = types.str; + default = "::"; + example = "0.0.0.0"; + description = '' + The address where snapclients can connect. + ''; + }; + port = mkOption { type = types.port; default = 1704; @@ -75,24 +99,100 @@ in { ''; }; - controlPort = mkOption { + openFirewall = mkOption { + type = types.bool; + default = true; + description = '' + Whether to automatically open the specified ports in the firewall. + ''; + }; + + inherit sampleFormat; + inherit codec; + + streamBuffer = mkOption { + type = with types; nullOr int; + default = null; + description = '' + Stream read (input) buffer in ms. + ''; + example = 20; + }; + + buffer = mkOption { + type = with types; nullOr int; + default = null; + description = '' + Network buffer in ms. + ''; + example = 1000; + }; + + sendToMuted = mkOption { + type = types.bool; + default = false; + description = '' + Send audio to muted clients. + ''; + }; + + tcp.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable the JSON-RPC via TCP. + ''; + }; + + tcp.listenAddress = mkOption { + type = types.str; + default = "::"; + example = "0.0.0.0"; + description = '' + The address where the TCP JSON-RPC listens on. + ''; + }; + + tcp.port = mkOption { type = types.port; default = 1705; description = '' - The port for control connections (JSON-RPC). + The port where the TCP JSON-RPC listens on. ''; }; - openFirewall = mkOption { + http.enable = mkOption { type = types.bool; default = true; description = '' - Whether to automatically open the specified ports in the firewall. + Whether to enable the JSON-RPC via HTTP. ''; }; - inherit sampleFormat; - inherit codec; + http.listenAddress = mkOption { + type = types.str; + default = "::"; + example = "0.0.0.0"; + description = '' + The address where the HTTP JSON-RPC listens on. + ''; + }; + + http.port = mkOption { + type = types.port; + default = 1780; + description = '' + The port where the HTTP JSON-RPC listens on. + ''; + }; + + http.docRoot = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to serve from the HTTP servers root. + ''; + }; streams = mkOption { type = with types; attrsOf (submodule { @@ -147,34 +247,7 @@ in { }; ''; }; - - streamBuffer = mkOption { - type = with types; nullOr int; - default = null; - description = '' - Stream read (input) buffer in ms. - ''; - example = 20; - }; - - buffer = mkOption { - type = with types; nullOr int; - default = null; - description = '' - Network buffer in ms. - ''; - example = 1000; - }; - - sendToMuted = mkOption { - type = types.bool; - default = false; - description = '' - Send audio to muted clients. - ''; - }; }; - }; @@ -206,7 +279,10 @@ in { }; }; - networking.firewall.allowedTCPPorts = optionals cfg.openFirewall [ cfg.port cfg.controlPort ]; + networking.firewall.allowedTCPPorts = + optionals cfg.openFirewall [ cfg.port ] + ++ optional cfg.tcp.enable cfg.tcp.port + ++ optional cfg.http.enable cfg.http.port; }; meta = { diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 19397427b2abab3..c030faf15cd29ad 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -310,6 +310,7 @@ in simple = handleTest ./simple.nix {}; slurm = handleTest ./slurm.nix {}; smokeping = handleTest ./smokeping.nix {}; + snapcast = handleTest ./snapcast.nix {}; snapper = handleTest ./snapper.nix {}; sogo = handleTest ./sogo.nix {}; solr = handleTest ./solr.nix {}; diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix new file mode 100644 index 000000000000000..92534f102819007 --- /dev/null +++ b/nixos/tests/snapcast.nix @@ -0,0 +1,58 @@ +import ./make-test-python.nix ({ pkgs, ...} : + +let + port = 10004; + tcpPort = 10005; + httpPort = 10080; +in { + name = "snapcast"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ hexa ]; + }; + + nodes = { + server = { + services.snapserver = { + enable = true; + port = port; + tcp.port = tcpPort; + http.port = httpPort; + streams = { + mpd = { + type = "pipe"; + location = "/run/snapserver/mpd"; + }; + bluetooth = { + type = "pipe"; + location = "/run/snapserver/bluetooth"; + }; + }; + }; + }; + }; + + testScript = '' + import json + + get_rpc_version = {"id": "1", "jsonrpc": "2.0", "method": "Server.GetRPCVersion"} + + start_all() + + server.wait_for_unit("snapserver.service") + server.wait_until_succeeds("ss -ntl | grep -q ${toString port}") + server.wait_until_succeeds("ss -ntl | grep -q ${toString tcpPort}") + server.wait_until_succeeds("ss -ntl | grep -q ${toString httpPort}") + + with subtest("check that pipes are created"): + server.succeed("test -p /run/snapserver/mpd") + server.succeed("test -p /run/snapserver/bluetooth") + + with subtest("test tcp json-rpc"): + server.succeed(f"echo '{json.dumps(get_rpc_version)}' | nc -w 1 localhost ${toString tcpPort}") + + with subtest("test http json-rpc"): + server.succeed( + "curl --fail http://localhost:${toString httpPort}/jsonrpc -d '{json.dumps(get_rpc_version)}'" + ) + ''; +}) diff --git a/pkgs/applications/audio/snapcast/default.nix b/pkgs/applications/audio/snapcast/default.nix index 25b8f9456a07297..5eff8f82e89d057 100644 --- a/pkgs/applications/audio/snapcast/default.nix +++ b/pkgs/applications/audio/snapcast/default.nix @@ -1,5 +1,6 @@ { stdenv, lib, fetchFromGitHub, cmake, pkgconfig -, alsaLib, asio, avahi, boost170, flac, libogg, libvorbis, soxr }: +, alsaLib, asio, avahi, boost170, flac, libogg, libvorbis, soxr +, nixosTests }: let @@ -57,6 +58,8 @@ stdenv.mkDerivation rec { cp -r ../doc/* ../*.md $out/share/doc/snapcast ''; + passthru.tests.snapcast = nixosTests.snapcast; + meta = with lib; { description = "Synchronous multi-room audio player"; homepage = "https://github.com/badaix/snapcast";