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