mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-03-08 01:24:09 +01:00
458 lines
13 KiB
Nix
458 lines
13 KiB
Nix
{
|
|
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;
|
|
nonBlockSettings = lib.filterAttrs (n: v: !(builtins.isAttrs v || builtins.isList v)) cfg.settings;
|
|
pureBlockSettings = removeAttrs cfg.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
|
|
}
|
|
}
|
|
'';
|
|
passAsFile = [
|
|
"expectScript"
|
|
];
|
|
};
|
|
checkPhase = ''
|
|
${lib.getExe pkgs.buildPackages.expect} -f "$expectScriptPath"
|
|
'';
|
|
};
|
|
|
|
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.bool;
|
|
default = false;
|
|
description = "Open ports in the firewall for mpd.";
|
|
};
|
|
|
|
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.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"
|
|
"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"
|
|
"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)
|
|
)
|
|
&& !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.";
|
|
|
|
# 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 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;
|
|
};
|
|
};
|
|
|
|
}
|