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