Unverified Commit a1b4ee54 authored by Franz Pletz's avatar Franz Pletz Committed by GitHub
Browse files

nixos/snapserver: migrate to settings option (#441245)

parents 7385a8e6 b0f63e91
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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.
+158 −262
Original line number Diff line number Diff line
{
  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,215 +106,100 @@ 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.
        '';
      };

      openFirewall = lib.mkOption {
        type = lib.types.bool;
        default = false;
        type = types.submodule {
          freeformType = format.type;
          options = {
            stream = {
              bind_to_address = mkOption {
                default = "::";
                description = ''
          Whether to automatically open the specified ports in the firewall.
                  Address to listen on for snapclient connections.
                '';
              };

      inherit sampleFormat;
      inherit codec;

      streamBuffer = lib.mkOption {
        type = with lib.types; nullOr int;
        default = null;
              port = mkOption {
                type = types.port;
                default = 1704;
                description = ''
          Stream read (input) buffer in ms.
                  Port to listen on for snapclient connections.
                '';
        example = 20;
              };

      buffer = lib.mkOption {
        type = with lib.types; nullOr int;
        default = null;
              source = mkOption {
                type = with types; either str (listOf str);
                example = "pipe:///tmp/snapfifo?name=default";
                description = ''
          Network buffer in ms.
                  One or multiple URIs to PCM inpuit streams.
                '';
        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 = {
              enabled = mkEnableOption "the TCP JSON-RPC";

      tcp.listenAddress = lib.mkOption {
        type = lib.types.str;
              bind_to_address = mkOption {
                default = "::";
        example = "0.0.0.0";
                description = ''
          The address where the TCP JSON-RPC listens on.
                  Address to listen on for snapclient connections.
                '';
              };

      tcp.port = lib.mkOption {
        type = lib.types.port;
              port = mkOption {
                type = types.port;
                default = 1705;
                description = ''
          The port where the TCP JSON-RPC listens on.
                  Port to listen on for snapclient connections.
                '';
              };

      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;
            http = {
              enabled = mkEnableOption "the HTTP JSON-RPC";

              bind_to_address = mkOption {
                default = "::";
        example = "0.0.0.0";
                description = ''
          The address where the HTTP JSON-RPC listens on.
                  Address to listen on for snapclient connections.
                '';
              };

      http.port = lib.mkOption {
        type = lib.types.port;
        default = 1780;
              port = mkOption {
                type = types.port;
                default = 1705;
                description = ''
          The port where the HTTP JSON-RPC listens on.
                  Port to listen on for snapclient connections.
                '';
              };

      http.docRoot = lib.mkOption {
              doc_root = lib.mkOption {
                type = with lib.types; nullOr path;
                default = pkgs.snapweb;
        defaultText = lib.literalExpression "pkgs.snapweb";
                defaultText = 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 = { };
      };

      openFirewall = lib.mkOption {
        type = lib.types.bool;
        default = false;
        description = ''
          The definition for an input source.
        '';
        example = lib.literalExpression ''
          {
            mpd = {
              type = "pipe";
              location = "/run/snapserver/mpd";
              sampleFormat = "48000:16:2";
              codec = "pcm";
            };
          };
          Whether to automatically open the specified ports in the firewall.
        '';
      };
    };
@@ -305,17 +208,7 @@ in
  ###### 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 = {
+18 −20
Original line number Diff line number Diff line
{
  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;
        settings = {
          stream = {
            port = port;
        tcp.port = tcpPort;
        http.port = httpPort;
        openFirewall = true;
            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;
        streams = {
          mpd = {
            type = "pipe";
            location = "/run/snapserver/mpd";
            query.mode = "create";
          };
          bluetooth = {
            type = "pipe";
            location = "/run/snapserver/bluetooth";
          };
          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 ];
    };
+4 −2
Original line number Diff line number Diff line
@@ -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