{ config, lib, pkgs, ... }: let name = "mpd"; uid = config.ids.uids.mpd; gid = config.ids.gids.mpd; cfg = config.services.mpd; mkKeyValue = a: lib.mapAttrsToList ( k: v: k + " " + ( if builtins.isBool v then # Mainly for https://mpd.readthedocs.io/en/stable/user.html#zeroconf "\"" + (lib.boolToYesNo v) + "\"" else "\"" + (toString v) + "\"" ) ) a; settings = lib.filterAttrs (n: v: !(isNull v)) cfg.settings; nonBlockSettings = lib.filterAttrs (n: v: !(builtins.isAttrs v || builtins.isList v)) settings; pureBlockSettings = removeAttrs settings (builtins.attrNames nonBlockSettings); blocks = pureBlockSettings // lib.optionalAttrs cfg.fluidsynth { decoder = (pureBlockSettings.decoder or [ ]) ++ [ { plugin = "fluidsynth"; soundfont = "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"; } ]; }; processSingleBlock = n: v: [ (n + " {") ] # Add indentation, for better readability ++ (map (l: " " + l) (mkKeyValue v)) ++ [ "}" ]; mpdConf = pkgs.writeTextFile { name = "mpd.conf"; text = '' # This file was automatically generated by NixOS. Edit mpd's configuration # via NixOS' configuration.nix, as this file will be rewritten upon mpd's # restart. '' + lib.concatStringsSep "\n" ( mkKeyValue ( { state_file = "${cfg.dataDir}/state"; sticker_file = "${cfg.dataDir}/sticker.sql"; } // nonBlockSettings ) ++ lib.flatten ( lib.mapAttrsToList ( n: v: if builtins.isList v then (map (b: processSingleBlock n b) v) else (processSingleBlock n v) ) blocks ) ++ lib.imap0 ( i: a: "password \"{{password-${toString i}}}@${lib.concatStringsSep "," a.permissions}\"" ) cfg.credentials ); derivationArgs = { expectScript = '' spawn ${lib.getExe pkgs.buildPackages.mpd} --no-daemon "$env(out)" expect { "exception: Error in \"$env(out)\"" { puts "Config file invalid\n" exit 1 } "exception:" { exit 0 } } ''; }; checkPhase = '' printf "%s" "$expectScript" | ${lib.getExe pkgs.buildPackages.expect} -f - ''; }; in { ###### interface options = { services.mpd = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable MPD, the music player daemon. ''; }; startWhenNeeded = lib.mkOption { type = lib.types.bool; default = false; description = '' If set, {command}`mpd` is socket-activated; that is, instead of having it permanently running as a daemon, systemd will start it on the first incoming connection. ''; }; user = lib.mkOption { type = lib.types.str; default = name; description = "User account under which MPD runs."; }; group = lib.mkOption { type = lib.types.str; default = name; description = "Group account under which MPD runs."; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/${name}"; description = '' The directory where MPD stores its state, tag cache, playlists etc. If left as the default value this directory will automatically be created before the MPD server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions. ''; }; openFirewall = lib.mkOption { type = lib.types.nullOr lib.types.bool; default = null; description = '' Open ports in the firewall for mpd. If `null` (default), you might get a warning asking you to set it explicitly to `true` or `false`, depending upon the value of `services.mpd.settings.bind_to_address`. ''; }; settings = lib.mkOption { type = lib.types.submodule { freeformType = let inherit (lib.types) oneOf attrsOf listOf str int bool path ; atomType = oneOf [ str int bool path ]; in attrsOf (oneOf [ atomType (listOf (attrsOf atomType)) ]); options = { music_directory = lib.mkOption { type = with lib.types; either path (strMatching "([a-z]+)://.+"); default = "${cfg.dataDir}/music"; defaultText = lib.literalExpression ''"''${dataDir}/music"''; description = '' The directory or URI where MPD reads music from. If left as the default value this directory will automatically be created before the MPD server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions. ''; }; playlist_directory = lib.mkOption { type = lib.types.path; default = "${cfg.dataDir}/playlists"; defaultText = lib.literalExpression ''"''${dataDir}/playlists"''; description = '' The directory where MPD stores playlists. If left as the default value this directory will automatically be created before the MPD server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions. ''; }; bind_to_address = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; example = "any"; description = '' The address for the daemon to listen on. Use `any` to listen on all addresses. ''; }; port = lib.mkOption { type = lib.types.port; default = 6600; description = '' This setting is the TCP port that is desired for the daemon to get assigned to. ''; }; db_file = lib.mkOption { type = lib.types.nullOr lib.types.path; default = "${cfg.dataDir}/tag_cache"; defaultText = lib.literalExpression ''"''${dataDir}/tag_cache"''; description = '' The path to MPD's database. ''; }; }; }; default = { }; description = '' Configuration for MPD. MPD supports key-value like blocks for settings like `audio_output` and `neighbor`. Some of these blocks can be specified multiple times, so the following configuration: ```txt audio_output { device "iec958:CARD=Intel,DEV=0" mixer_control "PCM" name "My specific ALSA output" type "alsa" } audio_output { mixer_type "null" name "ALSA Null" type "alsa" } audio_output { name "The Pulse" type "pulse" } ``` Can be inserted with: ```nix audio_output = [ { type = "alsa"; name = "My specific ALSA output"; device = "iec958:CARD=Intel,DEV=0"; mixer_control = "PCM"; } { type = "alsa"; name = "ALSA Null"; mixer_type = "null"; } { type = "pulse"; name = "The Pulse"; } ]; ``` ''; }; credentials = lib.mkOption { type = lib.types.listOf ( lib.types.submodule { options = { passwordFile = lib.mkOption { type = lib.types.path; description = '' Path to file containing the password. ''; }; permissions = let perms = [ "read" "add" "player" "control" "admin" ]; in lib.mkOption { type = lib.types.listOf (lib.types.enum perms); default = [ "read" ]; description = '' List of permissions that are granted with this password. Permissions can be "${lib.concatStringsSep "\", \"" perms}". ''; }; }; } ); description = '' Credentials and permissions for accessing the mpd server. ''; default = [ ]; example = [ { passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ]; } { passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = [ "read" "add" "player" "control" "admin" ]; } ]; }; fluidsynth = lib.mkOption { type = lib.types.bool; default = false; description = '' If set, add fluidsynth soundfont `decoder` block. ''; }; }; }; ###### implementation imports = [ (lib.mkRenamedOptionModule [ "services" "mpd" "musicDirectory" ] [ "services" "mpd" "settings" "music_directory" ] ) (lib.mkRenamedOptionModule [ "services" "mpd" "playlistDirectory" ] [ "services" "mpd" "settings" "playlist_directory" ] ) (lib.mkRenamedOptionModule [ "services" "mpd" "dbFile" ] [ "services" "mpd" "settings" "db_file" ]) (lib.mkRenamedOptionModule [ "services" "mpd" "network" "listenAddress" ] [ "services" "mpd" "settings" "bind_to_address" ] ) (lib.mkRenamedOptionModule [ "services" "mpd" "network" "port" ] [ "services" "mpd" "settings" "port" ] ) (lib.mkRemovedOptionModule [ "services" "mpd" "extraConfig" ] "services.mpd.extraConfig was replaced by the declarative services.mpd.settings option, per RFC42." ) ]; config = lib.mkIf cfg.enable { warnings = lib.optional ( !( (builtins.elem cfg.settings.bind_to_address [ "localhost" "127.0.0.1" ]) || (lib.hasPrefix "/" cfg.settings.bind_to_address) ) && (isNull cfg.openFirewall) ) "Using '${cfg.settings.bind_to_address}' as services.mpd.settings.bind_to_address without enabling services.mpd.openFirewall, might prevent you from accessing MPD from other clients. To suppress this warning, set services.mpd.openFirewall explicitly to `false`"; # install mpd units systemd.packages = [ pkgs.mpd ]; systemd.sockets.mpd = lib.mkIf cfg.startWhenNeeded { wantedBy = [ "sockets.target" ]; listenStreams = [ "" # Note: this is needed to override the upstream unit ( if pkgs.lib.hasPrefix "/" cfg.settings.bind_to_address then cfg.settings.bind_to_address else "${ lib.optionalString (cfg.settings.bind_to_address != "any") "${cfg.settings.bind_to_address}:" }${toString cfg.settings.port}" ) ]; }; systemd.services.mpd = { wantedBy = lib.optional (!cfg.startWhenNeeded) "multi-user.target"; preStart = '' set -euo pipefail install -m 600 ${mpdConf} /run/mpd/mpd.conf '' + lib.optionalString (cfg.credentials != [ ]) ( lib.concatStringsSep "\n" ( lib.imap0 ( i: c: "${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf" ) cfg.credentials ) ); serviceConfig = { User = "${cfg.user}"; # Note: the first "" overrides the ExecStart from the upstream unit ExecStart = [ "" "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf" ]; RuntimeDirectory = "mpd"; StateDirectory = [ ] ++ lib.optionals (cfg.dataDir == "/var/lib/${name}") [ name ] ++ lib.optionals (cfg.settings.playlist_directory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ] ++ lib.optionals (cfg.settings.music_directory == "/var/lib/${name}/music") [ name "${name}/music" ]; }; }; networking.firewall.allowedTCPPorts = lib.optionals ( builtins.isBool cfg.openFirewall && cfg.openFirewall ) [ cfg.settings.port ]; users.users = lib.optionalAttrs (cfg.user == name) { ${name} = { inherit uid; group = cfg.group; extraGroups = [ "audio" ]; description = "Music Player Daemon user"; home = "${cfg.dataDir}"; }; }; users.groups = lib.optionalAttrs (cfg.group == name) { ${name}.gid = gid; }; }; }