1{ config, lib, options, pkgs, ... }:
2
3with lib;
4
5let
6
7 gid = config.ids.gids.mediatomb;
8 cfg = config.services.mediatomb;
9 opt = options.services.mediatomb;
10 name = cfg.package.pname;
11 pkg = cfg.package;
12 optionYesNo = option: if option then "yes" else "no";
13 # configuration on media directory
14 mediaDirectory = {
15 options = {
16 path = mkOption {
17 type = types.str;
18 description = ''
19 Absolute directory path to the media directory to index.
20 '';
21 };
22 recursive = mkOption {
23 type = types.bool;
24 default = false;
25 description = "Whether the indexation must take place recursively or not.";
26 };
27 hidden-files = mkOption {
28 type = types.bool;
29 default = true;
30 description = "Whether to index the hidden files or not.";
31 };
32 };
33 };
34 toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
35
36 transcodingConfig = if cfg.transcoding then with pkgs; ''
37 <transcoding enabled="yes">
38 <mimetype-profile-mappings>
39 <transcode mimetype="video/x-flv" using="vlcmpeg" />
40 <transcode mimetype="application/ogg" using="vlcmpeg" />
41 <transcode mimetype="audio/ogg" using="ogg2mp3" />
42 <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
43 </mimetype-profile-mappings>
44 <profiles>
45 <profile name="ogg2mp3" enabled="no" type="external">
46 <mimetype>audio/mpeg</mimetype>
47 <accept-url>no</accept-url>
48 <first-resource>yes</first-resource>
49 <accept-ogg-theora>no</accept-ogg-theora>
50 <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
51 <buffer size="1048576" chunk-size="131072" fill-size="262144" />
52 </profile>
53 <profile name="vlcmpeg" enabled="no" type="external">
54 <mimetype>video/mpeg</mimetype>
55 <accept-url>yes</accept-url>
56 <first-resource>yes</first-resource>
57 <accept-ogg-theora>yes</accept-ogg-theora>
58 <agent command="${libsForQt5.vlc}/bin/vlc"
59 arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit" />
60 <buffer size="14400000" chunk-size="512000" fill-size="120000" />
61 </profile>
62 </profiles>
63 </transcoding>
64'' else ''
65 <transcoding enabled="no">
66 </transcoding>
67'';
68
69 configText = optionalString (! cfg.customCfg) ''
70<?xml version="1.0" encoding="UTF-8"?>
71<config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
72 <server>
73 <ui enabled="yes" show-tooltips="yes">
74 <accounts enabled="no" session-timeout="30">
75 <account user="${name}" password="${name}"/>
76 </accounts>
77 </ui>
78 <name>${cfg.serverName}</name>
79 <udn>uuid:${cfg.uuid}</udn>
80 <home>${cfg.dataDir}</home>
81 <interface>${cfg.interface}</interface>
82 <webroot>${pkg}/share/${name}/web</webroot>
83 <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
84 <storage>
85 <sqlite3 enabled="yes">
86 <database-file>${name}.db</database-file>
87 </sqlite3>
88 </storage>
89 <protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
90 ${optionalString cfg.dsmSupport ''
91 <custom-http-headers>
92 <add header="X-User-Agent: redsonic"/>
93 </custom-http-headers>
94
95 <manufacturerURL>redsonic.com</manufacturerURL>
96 <modelNumber>105</modelNumber>
97 ''}
98 ${optionalString cfg.tg100Support ''
99 <upnp-string-limit>101</upnp-string-limit>
100 ''}
101 <extended-runtime-options>
102 <mark-played-items enabled="yes" suppress-cds-updates="yes">
103 <string mode="prepend">*</string>
104 <mark>
105 <content>video</content>
106 </mark>
107 </mark-played-items>
108 </extended-runtime-options>
109 </server>
110 <import hidden-files="no">
111 <autoscan use-inotify="auto">
112 ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
113 </autoscan>
114 <scripting script-charset="UTF-8">
115 <common-script>${pkg}/share/${name}/js/common.js</common-script>
116 <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
117 <virtual-layout type="builtin">
118 <import-script>${pkg}/share/${name}/js/import.js</import-script>
119 </virtual-layout>
120 </scripting>
121 <mappings>
122 <extension-mimetype ignore-unknown="no">
123 <map from="mp3" to="audio/mpeg"/>
124 <map from="ogx" to="application/ogg"/>
125 <map from="ogv" to="video/ogg"/>
126 <map from="oga" to="audio/ogg"/>
127 <map from="ogg" to="audio/ogg"/>
128 <map from="ogm" to="video/ogg"/>
129 <map from="asf" to="video/x-ms-asf"/>
130 <map from="asx" to="video/x-ms-asf"/>
131 <map from="wma" to="audio/x-ms-wma"/>
132 <map from="wax" to="audio/x-ms-wax"/>
133 <map from="wmv" to="video/x-ms-wmv"/>
134 <map from="wvx" to="video/x-ms-wvx"/>
135 <map from="wm" to="video/x-ms-wm"/>
136 <map from="wmx" to="video/x-ms-wmx"/>
137 <map from="m3u" to="audio/x-mpegurl"/>
138 <map from="pls" to="audio/x-scpls"/>
139 <map from="flv" to="video/x-flv"/>
140 <map from="mkv" to="video/x-matroska"/>
141 <map from="mka" to="audio/x-matroska"/>
142 ${optionalString cfg.ps3Support ''
143 <map from="avi" to="video/divx"/>
144 ''}
145 ${optionalString cfg.dsmSupport ''
146 <map from="avi" to="video/avi"/>
147 ''}
148 </extension-mimetype>
149 <mimetype-upnpclass>
150 <map from="audio/*" to="object.item.audioItem.musicTrack"/>
151 <map from="video/*" to="object.item.videoItem"/>
152 <map from="image/*" to="object.item.imageItem"/>
153 </mimetype-upnpclass>
154 <mimetype-contenttype>
155 <treat mimetype="audio/mpeg" as="mp3"/>
156 <treat mimetype="application/ogg" as="ogg"/>
157 <treat mimetype="audio/ogg" as="ogg"/>
158 <treat mimetype="audio/x-flac" as="flac"/>
159 <treat mimetype="audio/x-ms-wma" as="wma"/>
160 <treat mimetype="audio/x-wavpack" as="wv"/>
161 <treat mimetype="image/jpeg" as="jpg"/>
162 <treat mimetype="audio/x-mpegurl" as="playlist"/>
163 <treat mimetype="audio/x-scpls" as="playlist"/>
164 <treat mimetype="audio/x-wav" as="pcm"/>
165 <treat mimetype="audio/L16" as="pcm"/>
166 <treat mimetype="video/x-msvideo" as="avi"/>
167 <treat mimetype="video/mp4" as="mp4"/>
168 <treat mimetype="audio/mp4" as="mp4"/>
169 <treat mimetype="application/x-iso9660" as="dvd"/>
170 <treat mimetype="application/x-iso9660-image" as="dvd"/>
171 </mimetype-contenttype>
172 </mappings>
173 <online-content>
174 <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
175 <favorites user="${name}"/>
176 <standardfeed feed="most_viewed" time-range="today"/>
177 <playlists user="${name}"/>
178 <uploads user="${name}"/>
179 <standardfeed feed="recently_featured" time-range="today"/>
180 </YouTube>
181 </online-content>
182 </import>
183 ${transcodingConfig}
184 </config>
185'';
186 defaultFirewallRules = {
187 # udp 1900 port needs to be opened for SSDP (not configurable within
188 # mediatomb/gerbera) cf.
189 # https://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
190 allowedUDPPorts = [ 1900 cfg.port ];
191 allowedTCPPorts = [ cfg.port ];
192 };
193
194in {
195
196 ###### interface
197
198 options = {
199
200 services.mediatomb = {
201
202 enable = mkOption {
203 type = types.bool;
204 default = false;
205 description = ''
206 Whether to enable the Gerbera/Mediatomb DLNA server.
207 '';
208 };
209
210 serverName = mkOption {
211 type = types.str;
212 default = "Gerbera (Mediatomb)";
213 description = ''
214 How to identify the server on the network.
215 '';
216 };
217
218 package = mkPackageOption pkgs "gerbera" { };
219
220 ps3Support = mkOption {
221 type = types.bool;
222 default = false;
223 description = ''
224 Whether to enable ps3 specific tweaks.
225 WARNING: incompatible with DSM 320 support.
226 '';
227 };
228
229 dsmSupport = mkOption {
230 type = types.bool;
231 default = false;
232 description = ''
233 Whether to enable D-Link DSM 320 specific tweaks.
234 WARNING: incompatible with ps3 support.
235 '';
236 };
237
238 tg100Support = mkOption {
239 type = types.bool;
240 default = false;
241 description = ''
242 Whether to enable Telegent TG100 specific tweaks.
243 '';
244 };
245
246 transcoding = mkOption {
247 type = types.bool;
248 default = false;
249 description = ''
250 Whether to enable transcoding.
251 '';
252 };
253
254 dataDir = mkOption {
255 type = types.path;
256 default = "/var/lib/${name}";
257 defaultText = literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
258 description = ''
259 The directory where Gerbera/Mediatomb stores its state, data, etc.
260 '';
261 };
262
263 pcDirectoryHide = mkOption {
264 type = types.bool;
265 default = true;
266 description = ''
267 Whether to list the top-level directory or not (from upnp client standpoint).
268 '';
269 };
270
271 user = mkOption {
272 type = types.str;
273 default = "mediatomb";
274 description = "User account under which the service runs.";
275 };
276
277 group = mkOption {
278 type = types.str;
279 default = "mediatomb";
280 description = "Group account under which the service runs.";
281 };
282
283 port = mkOption {
284 type = types.port;
285 default = 49152;
286 description = ''
287 The network port to listen on.
288 '';
289 };
290
291 interface = mkOption {
292 type = types.str;
293 default = "";
294 description = ''
295 A specific interface to bind to.
296 '';
297 };
298
299 openFirewall = mkOption {
300 type = types.bool;
301 default = false;
302 description = ''
303 If false (the default), this is up to the user to declare the firewall rules.
304 If true, this opens port 1900 (tcp and udp) and the port specified by
305 {option}`sercvices.mediatomb.port`.
306
307 If the option {option}`services.mediatomb.interface` is set,
308 the firewall rules opened are dedicated to that interface. Otherwise,
309 those rules are opened globally.
310 '';
311 };
312
313 uuid = mkOption {
314 type = types.str;
315 default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
316 description = ''
317 A unique (on your network) to identify the server by.
318 '';
319 };
320
321 mediaDirectories = mkOption {
322 type = with types; listOf (submodule mediaDirectory);
323 default = [];
324 description = ''
325 Declare media directories to index.
326 '';
327 example = [
328 { path = "/data/pictures"; recursive = false; hidden-files = false; }
329 { path = "/data/audio"; recursive = true; hidden-files = false; }
330 ];
331 };
332
333 customCfg = mkOption {
334 type = types.bool;
335 default = false;
336 description = ''
337 Allow the service to create and use its own config file inside the `dataDir` as
338 configured by {option}`services.mediatomb.dataDir`.
339 Deactivated by default, the service then runs with the configuration generated from this module.
340 Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
341 config.xml within the configured `dataDir`. It's up to the user to make a correct
342 configuration file.
343 '';
344 };
345
346 };
347 };
348
349
350 ###### implementation
351
352 config = let binaryCommand = "${pkg}/bin/${name}";
353 interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
354 configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
355 in mkIf cfg.enable {
356 systemd.services.mediatomb = {
357 description = "${cfg.serverName} media Server";
358 # Gerbera might fail if the network interface is not available on startup
359 # https://github.com/gerbera/gerbera/issues/1324
360 wants = [ "network-online.target" ];
361 after = [ "network.target" "network-online.target" ];
362 wantedBy = [ "multi-user.target" ];
363 serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
364 serviceConfig.User = cfg.user;
365 serviceConfig.Group = cfg.group;
366 };
367
368 users.groups = optionalAttrs (cfg.group == "mediatomb") {
369 mediatomb.gid = gid;
370 };
371
372 users.users = optionalAttrs (cfg.user == "mediatomb") {
373 mediatomb = {
374 isSystemUser = true;
375 group = cfg.group;
376 home = cfg.dataDir;
377 createHome = true;
378 description = "${name} DLNA Server User";
379 };
380 };
381
382 # Open firewall only if users enable it
383 networking.firewall = mkMerge [
384 (mkIf (cfg.openFirewall && cfg.interface != "") {
385 interfaces."${cfg.interface}" = defaultFirewallRules;
386 })
387 (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
388 ];
389 };
390}