1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 name = "mpd";
8
9 uid = config.ids.uids.mpd;
10 gid = config.ids.gids.mpd;
11 cfg = config.services.mpd;
12
13 credentialsPlaceholder = (creds:
14 let
15 placeholders = (imap0
16 (i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
17 creds);
18 in
19 concatStringsSep "\n" placeholders);
20
21 mpdConf = pkgs.writeText "mpd.conf" ''
22 # This file was automatically generated by NixOS. Edit mpd's configuration
23 # via NixOS' configuration.nix, as this file will be rewritten upon mpd's
24 # restart.
25
26 music_directory "${cfg.musicDirectory}"
27 playlist_directory "${cfg.playlistDirectory}"
28 ${lib.optionalString (cfg.dbFile != null) ''
29 db_file "${cfg.dbFile}"
30 ''}
31 state_file "${cfg.dataDir}/state"
32 sticker_file "${cfg.dataDir}/sticker.sql"
33
34 ${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''}
35 ${optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''}
36 ${optionalString (cfg.fluidsynth) ''
37 decoder {
38 plugin "fluidsynth"
39 soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
40 }
41 ''}
42
43 ${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
44
45 ${cfg.extraConfig}
46 '';
47
48in {
49
50 ###### interface
51
52 options = {
53
54 services.mpd = {
55
56 enable = mkOption {
57 type = types.bool;
58 default = false;
59 description = lib.mdDoc ''
60 Whether to enable MPD, the music player daemon.
61 '';
62 };
63
64 startWhenNeeded = mkOption {
65 type = types.bool;
66 default = false;
67 description = lib.mdDoc ''
68 If set, {command}`mpd` is socket-activated; that
69 is, instead of having it permanently running as a daemon,
70 systemd will start it on the first incoming connection.
71 '';
72 };
73
74 musicDirectory = mkOption {
75 type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
76 default = "${cfg.dataDir}/music";
77 defaultText = literalExpression ''"''${dataDir}/music"'';
78 description = lib.mdDoc ''
79 The directory or NFS/SMB network share where MPD reads music from. If left
80 as the default value this directory will automatically be created before
81 the MPD server starts, otherwise the sysadmin is responsible for ensuring
82 the directory exists with appropriate ownership and permissions.
83 '';
84 };
85
86 playlistDirectory = mkOption {
87 type = types.path;
88 default = "${cfg.dataDir}/playlists";
89 defaultText = literalExpression ''"''${dataDir}/playlists"'';
90 description = lib.mdDoc ''
91 The directory where MPD stores playlists. If left as the default value
92 this directory will automatically be created before the MPD server starts,
93 otherwise the sysadmin is responsible for ensuring the directory exists
94 with appropriate ownership and permissions.
95 '';
96 };
97
98 extraConfig = mkOption {
99 type = types.lines;
100 default = "";
101 description = lib.mdDoc ''
102 Extra directives added to to the end of MPD's configuration file,
103 mpd.conf. Basic configuration like file location and uid/gid
104 is added automatically to the beginning of the file. For available
105 options see {manpage}`mpd.conf(5)`.
106 '';
107 };
108
109 dataDir = mkOption {
110 type = types.path;
111 default = "/var/lib/${name}";
112 description = lib.mdDoc ''
113 The directory where MPD stores its state, tag cache, playlists etc. If
114 left as the default value this directory will automatically be created
115 before the MPD server starts, otherwise the sysadmin is responsible for
116 ensuring the directory exists with appropriate ownership and permissions.
117 '';
118 };
119
120 user = mkOption {
121 type = types.str;
122 default = name;
123 description = lib.mdDoc "User account under which MPD runs.";
124 };
125
126 group = mkOption {
127 type = types.str;
128 default = name;
129 description = lib.mdDoc "Group account under which MPD runs.";
130 };
131
132 network = {
133
134 listenAddress = mkOption {
135 type = types.str;
136 default = "127.0.0.1";
137 example = "any";
138 description = lib.mdDoc ''
139 The address for the daemon to listen on.
140 Use `any` to listen on all addresses.
141 '';
142 };
143
144 port = mkOption {
145 type = types.port;
146 default = 6600;
147 description = lib.mdDoc ''
148 This setting is the TCP port that is desired for the daemon to get assigned
149 to.
150 '';
151 };
152
153 };
154
155 dbFile = mkOption {
156 type = types.nullOr types.str;
157 default = "${cfg.dataDir}/tag_cache";
158 defaultText = literalExpression ''"''${dataDir}/tag_cache"'';
159 description = lib.mdDoc ''
160 The path to MPD's database. If set to `null` the
161 parameter is omitted from the configuration.
162 '';
163 };
164
165 credentials = mkOption {
166 type = types.listOf (types.submodule {
167 options = {
168 passwordFile = mkOption {
169 type = types.path;
170 description = lib.mdDoc ''
171 Path to file containing the password.
172 '';
173 };
174 permissions = let
175 perms = ["read" "add" "control" "admin"];
176 in mkOption {
177 type = types.listOf (types.enum perms);
178 default = [ "read" ];
179 description = lib.mdDoc ''
180 List of permissions that are granted with this password.
181 Permissions can be "${concatStringsSep "\", \"" perms}".
182 '';
183 };
184 };
185 });
186 description = lib.mdDoc ''
187 Credentials and permissions for accessing the mpd server.
188 '';
189 default = [];
190 example = [
191 {passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];}
192 {passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];}
193 ];
194 };
195
196 fluidsynth = mkOption {
197 type = types.bool;
198 default = false;
199 description = lib.mdDoc ''
200 If set, add fluidsynth soundfont and configure the plugin.
201 '';
202 };
203 };
204
205 };
206
207
208 ###### implementation
209
210 config = mkIf cfg.enable {
211
212 # install mpd units
213 systemd.packages = [ pkgs.mpd ];
214
215 systemd.sockets.mpd = mkIf cfg.startWhenNeeded {
216 wantedBy = [ "sockets.target" ];
217 listenStreams = [
218 "" # Note: this is needed to override the upstream unit
219 (if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
220 then cfg.network.listenAddress
221 else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
222 ];
223 };
224
225 systemd.services.mpd = {
226 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
227
228 preStart =
229 ''
230 set -euo pipefail
231 install -m 600 ${mpdConf} /run/mpd/mpd.conf
232 '' + optionalString (cfg.credentials != [])
233 (concatStringsSep "\n"
234 (imap0
235 (i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'')
236 cfg.credentials));
237
238 serviceConfig =
239 {
240 User = "${cfg.user}";
241 # Note: the first "" overrides the ExecStart from the upstream unit
242 ExecStart = [ "" "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf" ];
243 RuntimeDirectory = "mpd";
244 StateDirectory = []
245 ++ optionals (cfg.dataDir == "/var/lib/${name}") [ name ]
246 ++ optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ]
247 ++ optionals (cfg.musicDirectory == "/var/lib/${name}/music") [ name "${name}/music" ];
248 };
249 };
250
251 users.users = optionalAttrs (cfg.user == name) {
252 ${name} = {
253 inherit uid;
254 group = cfg.group;
255 extraGroups = [ "audio" ];
256 description = "Music Player Daemon user";
257 home = "${cfg.dataDir}";
258 };
259 };
260
261 users.groups = optionalAttrs (cfg.group == name) {
262 ${name}.gid = gid;
263 };
264 };
265
266}