diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 039478a1d489..c3f0c8de7e54 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -162,6 +162,8 @@ - The `yeahwm` package and `services.xserver.windowManager.yeahwm` module were removed due to the package being broken and unmaintained upstream. +- The `services.snapserver` module has been migrated to use the settings option and render a configuration file instead of passing every option over the command line. + - The `services.postgresql` module now sets up a systemd unit `postgresql.target`. Depending on `postgresql.target` guarantees that postgres is in read-write mode and initial/ensure scripts were executed. Depending on `postgresql.service` only guarantees a read-only connection. - The `services.siproxd` module has been removed as `siproxd` is unmaintained and broken with libosip 5.x. diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix index 86ba7ec7183d..9fcaa2f2c158 100644 --- a/nixos/modules/services/audio/snapserver.nix +++ b/nixos/modules/services/audio/snapserver.nix @@ -1,6 +1,5 @@ { config, - options, lib, pkgs, ... @@ -9,77 +8,96 @@ let name = "snapserver"; + inherit (lib) + literalExpression + mkEnableOption + mkOption + mkPackageOption + mkRemovedOptionModule + mkRenamedOptionModule + types + ; + cfg = config.services.snapserver; - # Using types.nullOr to inherit upstream defaults. - sampleFormat = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - Default sample format. - ''; - example = "48000:16:2"; + format = pkgs.formats.ini { + listsAsDuplicateKeys = true; }; - codec = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - Default audio compression method. - ''; - example = "flac"; - }; - - streamToOption = - name: opt: - let - os = val: lib.optionalString (val != null) "${val}"; - os' = prefix: val: lib.optionalString (val != null) (prefix + "${val}"); - toQueryString = key: value: "&${key}=${value}"; - in - "--stream.stream=\"${opt.type}://" - + os opt.location - + "?" - + os' "name=" name - + os' "&sampleformat=" opt.sampleFormat - + os' "&codec=" opt.codec - + lib.concatStrings (lib.mapAttrsToList toQueryString opt.query) - + "\""; - - optionalNull = val: ret: lib.optional (val != null) ret; - - optionString = lib.concatStringsSep " " ( - lib.mapAttrsToList streamToOption cfg.streams - # 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=${toString cfg.streamBuffer}" - ++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}" - ++ lib.optional cfg.sendToMuted "--stream.send_to_muted" - # tcp json rpc - ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ] - ++ lib.optionals cfg.tcp.enable [ - "--tcp.bind_to_address=${cfg.tcp.listenAddress}" - "--tcp.port=${toString cfg.tcp.port}" - ] - # http json rpc - ++ [ "--http.enabled=${toString cfg.http.enable}" ] - ++ lib.optionals cfg.http.enable [ - "--http.bind_to_address=${cfg.http.listenAddress}" - "--http.port=${toString cfg.http.port}" - ] - ++ lib.optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"" - ); + configFile = format.generate "snapserver.conf" cfg.settings; in { imports = [ - (lib.mkRenamedOptionModule + (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ] ) + + (mkRenamedOptionModule + [ "services" "snapserver" "listenAddress" ] + [ "services" "snapserver" "settings" "stream" "bind_to_address" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "port" ] + [ "services" "snapserver" "settings" "stream" "port" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "sampleFormat" ] + [ "services" "snapserver" "settings" "stream" "sampleformat" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "codec" ] + [ "services" "snapserver" "settings" "stream" "codec" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "streamBuffer" ] + [ "services" "snapserver" "settings" "stream" "chunk_ms" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "buffer" ] + [ "services" "snapserver" "settings" "stream" "buffer" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "send" ] + [ "services" "snapserver" "settings" "stream" "chunk_ms" ] + ) + + (mkRenamedOptionModule + [ "services" "snapserver" "tcp" "enable" ] + [ "services" "snapserver" "settings" "tcp" "enabled" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "tcp" "listenAddress" ] + [ "services" "snapserver" "settings" "tcp" "bind_to_address" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "tcp" "port" ] + [ "services" "snapserver" "settings" "tcp" "port" ] + ) + + (mkRenamedOptionModule + [ "services" "snapserver" "http" "enable" ] + [ "services" "snapserver" "settings" "http" "enabled" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "http" "listenAddress" ] + [ "services" "snapserver" "settings" "http" "bind_to_address" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "http" "port" ] + [ "services" "snapserver" "settings" "http" "port" ] + ) + (mkRenamedOptionModule + [ "services" "snapserver" "http" "docRoot" ] + [ "services" "snapserver" "settings" "http" "doc_root" ] + ) + + (mkRemovedOptionModule [ + "services" + "snapserver" + "streams" + ] "Configure `services.snapserver.settings.stream.source` instead") ]; ###### interface @@ -88,31 +106,93 @@ in services.snapserver = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to enable snapserver. - ''; - }; + enable = mkEnableOption "snapserver"; - package = lib.options.mkPackageOption pkgs "snapcast" { }; + package = mkPackageOption pkgs "snapcast" { }; - listenAddress = lib.mkOption { - type = lib.types.str; - default = "::"; - example = "0.0.0.0"; + settings = mkOption { + default = { }; description = '' - The address where snapclients can connect. - ''; - }; + Snapserver configuration. - port = lib.mkOption { - type = lib.types.port; - default = 1704; - description = '' - The port that snapclients can connect to. + Refer to the [example configuration](https://github.com/badaix/snapcast/blob/develop/server/etc/snapserver.conf) for possible options. ''; + type = types.submodule { + freeformType = format.type; + options = { + stream = { + bind_to_address = mkOption { + default = "::"; + description = '' + Address to listen on for snapclient connections. + ''; + }; + + port = mkOption { + type = types.port; + default = 1704; + description = '' + Port to listen on for snapclient connections. + ''; + }; + + source = mkOption { + type = with types; either str (listOf str); + example = "pipe:///tmp/snapfifo?name=default"; + description = '' + One or multiple URIs to PCM inpuit streams. + ''; + }; + }; + + tcp = { + enabled = mkEnableOption "the TCP JSON-RPC"; + + bind_to_address = mkOption { + default = "::"; + description = '' + Address to listen on for snapclient connections. + ''; + }; + + port = mkOption { + type = types.port; + default = 1705; + description = '' + Port to listen on for snapclient connections. + ''; + }; + }; + + http = { + enabled = mkEnableOption "the HTTP JSON-RPC"; + + bind_to_address = mkOption { + default = "::"; + description = '' + Address to listen on for snapclient connections. + ''; + }; + + port = mkOption { + type = types.port; + default = 1705; + description = '' + Port to listen on for snapclient connections. + ''; + }; + + doc_root = lib.mkOption { + type = with lib.types; nullOr path; + default = pkgs.snapweb; + defaultText = literalExpression "pkgs.snapweb"; + description = '' + Path to serve from the HTTP servers root. + ''; + }; + }; + }; + }; }; openFirewall = lib.mkOption { @@ -122,200 +202,13 @@ in Whether to automatically open the specified ports in the firewall. ''; }; - - inherit sampleFormat; - inherit codec; - - streamBuffer = lib.mkOption { - type = with lib.types; nullOr int; - default = null; - description = '' - Stream read (input) buffer in ms. - ''; - example = 20; - }; - - buffer = lib.mkOption { - type = with lib.types; nullOr int; - default = null; - description = '' - Network buffer in ms. - ''; - example = 1000; - }; - - sendToMuted = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Send audio to muted clients. - ''; - }; - - tcp.enable = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether to enable the JSON-RPC via TCP. - ''; - }; - - tcp.listenAddress = lib.mkOption { - type = lib.types.str; - default = "::"; - example = "0.0.0.0"; - description = '' - The address where the TCP JSON-RPC listens on. - ''; - }; - - tcp.port = lib.mkOption { - type = lib.types.port; - default = 1705; - description = '' - The port where the TCP JSON-RPC listens on. - ''; - }; - - http.enable = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether to enable the JSON-RPC via HTTP. - ''; - }; - - http.listenAddress = lib.mkOption { - type = lib.types.str; - default = "::"; - example = "0.0.0.0"; - description = '' - The address where the HTTP JSON-RPC listens on. - ''; - }; - - http.port = lib.mkOption { - type = lib.types.port; - default = 1780; - description = '' - The port where the HTTP JSON-RPC listens on. - ''; - }; - - http.docRoot = lib.mkOption { - type = with lib.types; nullOr path; - default = pkgs.snapweb; - defaultText = lib.literalExpression "pkgs.snapweb"; - description = '' - Path to serve from the HTTP servers root. - ''; - }; - - streams = lib.mkOption { - type = - with lib.types; - attrsOf (submodule { - options = { - location = lib.mkOption { - type = lib.types.oneOf [ - lib.types.path - lib.types.str - ]; - description = '' - For type `pipe` or `file`, the path to the pipe or file. - For type `librespot`, `airplay` or `process`, the path to the corresponding binary. - For type `tcp`, the `host:port` address to connect to or listen on. - For type `meta`, a list of stream names in the form `/one/two/...`. Don't forget the leading slash. - For type `alsa`, use an empty string. - ''; - example = lib.literalExpression '' - "/path/to/pipe" - "/path/to/librespot" - "192.168.1.2:4444" - "/MyTCP/Spotify/MyPipe" - ''; - }; - type = lib.mkOption { - type = lib.types.enum [ - "pipe" - "librespot" - "airplay" - "file" - "process" - "tcp" - "alsa" - "spotify" - "meta" - ]; - default = "pipe"; - description = '' - The type of input stream. - ''; - }; - query = lib.mkOption { - type = attrsOf str; - default = { }; - description = '' - Key-value pairs that convey additional parameters about a stream. - ''; - example = lib.literalExpression '' - # for type == "pipe": - { - mode = "create"; - }; - # for type == "process": - { - params = "--param1 --param2"; - logStderr = "true"; - }; - # for type == "tcp": - { - mode = "client"; - } - # for type == "alsa": - { - device = "hw:0,0"; - } - ''; - }; - inherit sampleFormat; - inherit codec; - }; - }); - default = { - default = { }; - }; - description = '' - The definition for an input source. - ''; - example = lib.literalExpression '' - { - mpd = { - type = "pipe"; - location = "/run/snapserver/mpd"; - sampleFormat = "48000:16:2"; - codec = "pcm"; - }; - }; - ''; - }; }; }; ###### implementation config = lib.mkIf cfg.enable { - - warnings = - # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85 - lib.filter (w: w != "") ( - lib.mapAttrsToList ( - k: v: - lib.optionalString (v.type == "spotify") '' - services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead. - '' - ) cfg.streams - ); + environment.etc."snapserver.conf".source = configFile; systemd.services.snapserver = { after = [ @@ -328,10 +221,13 @@ in "mpd.service" "mopidy.service" ]; - + restartTriggers = [ configFile ]; serviceConfig = { DynamicUser = true; - ExecStart = "${cfg.package}/bin/snapserver --daemon ${optionString}"; + ExecStart = toString [ + (lib.getExe' cfg.package "snapserver") + "--daemon" + ]; Type = "forking"; LimitRTPRIO = 50; LimitRTTIME = "infinity"; @@ -349,9 +245,9 @@ in }; networking.firewall.allowedTCPPorts = - lib.optionals cfg.openFirewall [ cfg.port ] - ++ lib.optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port - ++ lib.optional (cfg.openFirewall && cfg.http.enable) cfg.http.port; + lib.optionals cfg.openFirewall [ cfg.settings.stream.port ] + ++ lib.optional (cfg.openFirewall && cfg.settings.tcp.enabled) cfg.settings.tcp.port + ++ lib.optional (cfg.openFirewall && cfg.settings.http.enabled) cfg.settings.http.port; }; meta = { diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix index 2a3bf114ee70..2856fc5515e5 100644 --- a/nixos/tests/snapcast.nix +++ b/nixos/tests/snapcast.nix @@ -1,4 +1,5 @@ { + lib, pkgs, ... }: @@ -12,7 +13,7 @@ let in { name = "snapcast"; - meta = with pkgs.lib.maintainers; { + meta = with lib.maintainers; { maintainers = [ hexa ]; }; @@ -20,30 +21,27 @@ in server = { services.snapserver = { enable = true; - port = port; - tcp.port = tcpPort; - http.port = httpPort; - openFirewall = true; - buffer = bufferSize; - streams = { - mpd = { - type = "pipe"; - location = "/run/snapserver/mpd"; - query.mode = "create"; - }; - bluetooth = { - type = "pipe"; - location = "/run/snapserver/bluetooth"; + settings = { + stream = { + port = port; + source = [ + "pipe:///run/snapserver/mpd?name=mpd&mode=create" + "pipe:///run/snapserver/bluetooth?name=bluetooth" + "tcp://127.0.0.1:${toString tcpStreamPort}?name=tcp" + "meta:///mpd/bluetooth/tcp?name=meta" + ]; + buffer = bufferSize; }; tcp = { - type = "tcp"; - location = "127.0.0.1:${toString tcpStreamPort}"; + enabled = true; + port = tcpPort; }; - meta = { - type = "meta"; - location = "/mpd/bluetooth/tcp"; + http = { + enabled = true; + port = httpPort; }; }; + openFirewall = true; }; environment.systemPackages = [ pkgs.snapcast ]; }; diff --git a/pkgs/applications/audio/snapcast/default.nix b/pkgs/applications/audio/snapcast/default.nix index 9ba36c4b6b5a..67733286cc53 100644 --- a/pkgs/applications/audio/snapcast/default.nix +++ b/pkgs/applications/audio/snapcast/default.nix @@ -8,6 +8,7 @@ asio, avahi, boost, + expat, flac, libogg, libvorbis, @@ -23,13 +24,13 @@ stdenv.mkDerivation rec { pname = "snapcast"; - version = "0.30.0"; + version = "0.32.3"; src = fetchFromGitHub { owner = "badaix"; repo = "snapcast"; rev = "v${version}"; - hash = "sha256-EJgpZz4PnXfge0rkVH1F7cah+i9AvDJVSUVqL7qChDM="; + hash = "sha256-pGON2Nh7GgcGvMUNI3nWstm5Q9R+VW9eEi4IE6KkFBo="; }; nativeBuildInputs = [ @@ -42,6 +43,7 @@ stdenv.mkDerivation rec { boost asio avahi + expat flac libogg libvorbis