1{ config, lib, options, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.mpdscribble;
7 mpdCfg = config.services.mpd;
8 mpdOpt = options.services.mpd;
9
10 endpointUrls = {
11 "last.fm" = "http://post.audioscrobbler.com";
12 "libre.fm" = "http://turtle.libre.fm";
13 "jamendo" = "http://postaudioscrobbler.jamendo.com";
14 "listenbrainz" = "http://proxy.listenbrainz.org";
15 };
16
17 mkSection = secname: secCfg: ''
18 [${secname}]
19 url = ${secCfg.url}
20 username = ${secCfg.username}
21 password = {{${secname}_PASSWORD}}
22 journal = /var/lib/mpdscribble/${secname}.journal
23 '';
24
25 endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints);
26 cfgTemplate = pkgs.writeText "mpdscribble.conf" ''
27 ## This file was automatically genenrated by NixOS and will be overwritten.
28 ## Do not edit. Edit your NixOS configuration instead.
29
30 ## mpdscribble - an audioscrobbler for the Music Player Daemon.
31 ## http://mpd.wikia.com/wiki/Client:mpdscribble
32
33 # HTTP proxy URL.
34 ${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"}
35
36 # The location of the mpdscribble log file. The special value
37 # "syslog" makes mpdscribble use the local syslog daemon. On most
38 # systems, log messages will appear in /var/log/daemon.log then.
39 # "-" means log to stderr (the current terminal).
40 log = -
41
42 # How verbose mpdscribble's logging should be. Default is 1.
43 verbose = ${toString cfg.verbose}
44
45 # How often should mpdscribble save the journal file? [seconds]
46 journal_interval = ${toString cfg.journalInterval}
47
48 # The host running MPD, possibly protected by a password
49 # ([PASSWORD@]HOSTNAME).
50 host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host}
51
52 # The port that the MPD listens on and mpdscribble should try to
53 # connect to.
54 port = ${toString cfg.port}
55
56 ${endpoints}
57 '';
58
59 cfgFile = "/run/mpdscribble/mpdscribble.conf";
60
61 replaceSecret = secretFile: placeholder: targetFile:
62 optionalString (secretFile != null) ''
63 ${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '';
64
65 preStart = pkgs.writeShellScript "mpdscribble-pre-start" ''
66 cp -f "${cfgTemplate}" "${cfgFile}"
67 ${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile}
68 ${concatStringsSep "\n" (mapAttrsToList (secname: cfg:
69 replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile)
70 cfg.endpoints)}
71 '';
72
73 localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1");
74
75in {
76 ###### interface
77
78 options.services.mpdscribble = {
79
80 enable = mkEnableOption "mpdscribble, an MPD client which submits info about tracks being played to Last.fm (formerly AudioScrobbler)";
81
82 proxy = mkOption {
83 default = null;
84 type = types.nullOr types.str;
85 description = ''
86 HTTP proxy URL.
87 '';
88 };
89
90 verbose = mkOption {
91 default = 1;
92 type = types.int;
93 description = ''
94 Log level for the mpdscribble daemon.
95 '';
96 };
97
98 journalInterval = mkOption {
99 default = 600;
100 example = 60;
101 type = types.int;
102 description = ''
103 How often should mpdscribble save the journal file? [seconds]
104 '';
105 };
106
107 host = mkOption {
108 default = (if mpdCfg.network.listenAddress != "any" then
109 mpdCfg.network.listenAddress
110 else
111 "localhost");
112 defaultText = literalExpression ''
113 if config.${mpdOpt.network.listenAddress} != "any"
114 then config.${mpdOpt.network.listenAddress}
115 else "localhost"
116 '';
117 type = types.str;
118 description = ''
119 Host for the mpdscribble daemon to search for a mpd daemon on.
120 '';
121 };
122
123 passwordFile = mkOption {
124 default = if localMpd then
125 (findFirst
126 (c: any (x: x == "read") c.permissions)
127 { passwordFile = null; }
128 mpdCfg.credentials).passwordFile
129 else
130 null;
131 defaultText = literalMD ''
132 The first password file with read access configured for MPD when using a local instance,
133 otherwise `null`.
134 '';
135 type = types.nullOr types.str;
136 description = ''
137 File containing the password for the mpd daemon.
138 If there is a local mpd configured using {option}`services.mpd.credentials`
139 the default is automatically set to a matching passwordFile of the local mpd.
140 '';
141 };
142
143 port = mkOption {
144 default = mpdCfg.network.port;
145 defaultText = literalExpression "config.${mpdOpt.network.port}";
146 type = types.port;
147 description = ''
148 Port for the mpdscribble daemon to search for a mpd daemon on.
149 '';
150 };
151
152 endpoints = mkOption {
153 type = (let
154 endpoint = { name, ... }: {
155 options = {
156 url = mkOption {
157 type = types.str;
158 default = endpointUrls.${name} or "";
159 description = "The url endpoint where the scrobble API is listening.";
160 };
161 username = mkOption {
162 type = types.str;
163 description = ''
164 Username for the scrobble service.
165 '';
166 };
167 passwordFile = mkOption {
168 type = types.nullOr types.str;
169 description = "File containing the password, either as MD5SUM or cleartext.";
170 };
171 };
172 };
173 in types.attrsOf (types.submodule endpoint));
174 default = { };
175 example = {
176 "last.fm" = {
177 username = "foo";
178 passwordFile = "/run/secrets/lastfm_password";
179 };
180 };
181 description = ''
182 Endpoints to scrobble to.
183 If the endpoint is one of "${
184 concatStringsSep "\", \"" (attrNames endpointUrls)
185 }" the url is set automatically.
186 '';
187 };
188
189 };
190
191 ###### implementation
192
193 config = mkIf cfg.enable {
194 systemd.services.mpdscribble = {
195 after = [ "network.target" ] ++ (optional localMpd "mpd.service");
196 description = "mpdscribble mpd scrobble client";
197 wantedBy = [ "multi-user.target" ];
198 serviceConfig = {
199 DynamicUser = true;
200 StateDirectory = "mpdscribble";
201 RuntimeDirectory = "mpdscribble";
202 RuntimeDirectoryMode = "700";
203 # TODO use LoadCredential= instead of running preStart with full privileges?
204 ExecStartPre = "+${preStart}";
205 ExecStart =
206 "${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}";
207 };
208 };
209 };
210
211}