1 { config, lib, options, pkgs, ... }:
4 gid = config.ids.gids.mediatomb;
5 cfg = config.services.mediatomb;
6 opt = options.services.mediatomb;
7 name = cfg.package.pname;
9 optionYesNo = option: if option then "yes" else "no";
10 # configuration on media directory
16 Absolute directory path to the media directory to index.
19 recursive = lib.mkOption {
20 type = lib.types.bool;
22 description = "Whether the indexation must take place recursively or not.";
24 hidden-files = lib.mkOption {
25 type = lib.types.bool;
27 description = "Whether to index the hidden files or not.";
31 toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
33 transcodingConfig = if cfg.transcoding then with pkgs; ''
34 <transcoding enabled="yes">
35 <mimetype-profile-mappings>
36 <transcode mimetype="video/x-flv" using="vlcmpeg" />
37 <transcode mimetype="application/ogg" using="vlcmpeg" />
38 <transcode mimetype="audio/ogg" using="ogg2mp3" />
39 </mimetype-profile-mappings>
41 <profile name="ogg2mp3" enabled="no" type="external">
42 <mimetype>audio/mpeg</mimetype>
43 <accept-url>no</accept-url>
44 <first-resource>yes</first-resource>
45 <accept-ogg-theora>no</accept-ogg-theora>
46 <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
47 <buffer size="1048576" chunk-size="131072" fill-size="262144" />
49 <profile name="vlcmpeg" enabled="no" type="external">
50 <mimetype>video/mpeg</mimetype>
51 <accept-url>yes</accept-url>
52 <first-resource>yes</first-resource>
53 <accept-ogg-theora>yes</accept-ogg-theora>
54 <agent command="${lib.getExe vlc}"
55 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" />
56 <buffer size="14400000" chunk-size="512000" fill-size="120000" />
61 <transcoding enabled="no">
65 configText = lib.optionalString (! cfg.customCfg) ''
66 <?xml version="1.0" encoding="UTF-8"?>
67 <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">
69 <ui enabled="yes" show-tooltips="yes">
70 <accounts enabled="no" session-timeout="30">
71 <account user="${name}" password="${name}"/>
74 <name>${cfg.serverName}</name>
75 <udn>uuid:${cfg.uuid}</udn>
76 <home>${cfg.dataDir}</home>
77 <interface>${cfg.interface}</interface>
78 <webroot>${pkg}/share/${name}/web</webroot>
79 <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
81 <sqlite3 enabled="yes">
82 <database-file>${name}.db</database-file>
85 <protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
86 ${lib.optionalString cfg.dsmSupport ''
88 <add header="X-User-Agent: redsonic"/>
89 </custom-http-headers>
91 <manufacturerURL>redsonic.com</manufacturerURL>
92 <modelNumber>105</modelNumber>
94 ${lib.optionalString cfg.tg100Support ''
95 <upnp-string-limit>101</upnp-string-limit>
97 <extended-runtime-options>
98 <mark-played-items enabled="yes" suppress-cds-updates="yes">
99 <string mode="prepend">*</string>
101 <content>video</content>
104 </extended-runtime-options>
106 <import hidden-files="no">
107 <autoscan use-inotify="auto">
108 ${lib.concatMapStrings toMediaDirectory cfg.mediaDirectories}
110 <scripting script-charset="UTF-8">
111 <common-script>${pkg}/share/${name}/js/common.js</common-script>
112 <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
113 <virtual-layout type="builtin">
114 <import-script>${pkg}/share/${name}/js/import.js</import-script>
118 <extension-mimetype ignore-unknown="no">
119 <map from="mp3" to="audio/mpeg"/>
120 <map from="ogx" to="application/ogg"/>
121 <map from="ogv" to="video/ogg"/>
122 <map from="oga" to="audio/ogg"/>
123 <map from="ogg" to="audio/ogg"/>
124 <map from="ogm" to="video/ogg"/>
125 <map from="asf" to="video/x-ms-asf"/>
126 <map from="asx" to="video/x-ms-asf"/>
127 <map from="wma" to="audio/x-ms-wma"/>
128 <map from="wax" to="audio/x-ms-wax"/>
129 <map from="wmv" to="video/x-ms-wmv"/>
130 <map from="wvx" to="video/x-ms-wvx"/>
131 <map from="wm" to="video/x-ms-wm"/>
132 <map from="wmx" to="video/x-ms-wmx"/>
133 <map from="m3u" to="audio/x-mpegurl"/>
134 <map from="pls" to="audio/x-scpls"/>
135 <map from="flv" to="video/x-flv"/>
136 <map from="mkv" to="video/x-matroska"/>
137 <map from="mka" to="audio/x-matroska"/>
138 ${lib.optionalString cfg.ps3Support ''
139 <map from="avi" to="video/divx"/>
141 ${lib.optionalString cfg.dsmSupport ''
142 <map from="avi" to="video/avi"/>
144 </extension-mimetype>
146 <map from="audio/*" to="object.item.audioItem.musicTrack"/>
147 <map from="video/*" to="object.item.videoItem"/>
148 <map from="image/*" to="object.item.imageItem"/>
149 </mimetype-upnpclass>
150 <mimetype-contenttype>
151 <treat mimetype="audio/mpeg" as="mp3"/>
152 <treat mimetype="application/ogg" as="ogg"/>
153 <treat mimetype="audio/ogg" as="ogg"/>
154 <treat mimetype="audio/x-flac" as="flac"/>
155 <treat mimetype="audio/x-ms-wma" as="wma"/>
156 <treat mimetype="audio/x-wavpack" as="wv"/>
157 <treat mimetype="image/jpeg" as="jpg"/>
158 <treat mimetype="audio/x-mpegurl" as="playlist"/>
159 <treat mimetype="audio/x-scpls" as="playlist"/>
160 <treat mimetype="audio/x-wav" as="pcm"/>
161 <treat mimetype="audio/L16" as="pcm"/>
162 <treat mimetype="video/x-msvideo" as="avi"/>
163 <treat mimetype="video/mp4" as="mp4"/>
164 <treat mimetype="audio/mp4" as="mp4"/>
165 <treat mimetype="application/x-iso9660" as="dvd"/>
166 <treat mimetype="application/x-iso9660-image" as="dvd"/>
167 </mimetype-contenttype>
170 <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
171 <favorites user="${name}"/>
172 <standardfeed feed="most_viewed" time-range="today"/>
173 <playlists user="${name}"/>
174 <uploads user="${name}"/>
175 <standardfeed feed="recently_featured" time-range="today"/>
182 defaultFirewallRules = {
183 # udp 1900 port needs to be opened for SSDP (not configurable within
184 # mediatomb/gerbera) cf.
185 # https://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
186 allowedUDPPorts = [ 1900 cfg.port ];
187 allowedTCPPorts = [ cfg.port ];
196 services.mediatomb = {
198 enable = lib.mkOption {
199 type = lib.types.bool;
202 Whether to enable the Gerbera/Mediatomb DLNA server.
206 serverName = lib.mkOption {
207 type = lib.types.str;
208 default = "Gerbera (Mediatomb)";
210 How to identify the server on the network.
214 package = lib.mkPackageOption pkgs "gerbera" { };
216 ps3Support = lib.mkOption {
217 type = lib.types.bool;
220 Whether to enable ps3 specific tweaks.
221 WARNING: incompatible with DSM 320 support.
225 dsmSupport = lib.mkOption {
226 type = lib.types.bool;
229 Whether to enable D-Link DSM 320 specific tweaks.
230 WARNING: incompatible with ps3 support.
234 tg100Support = lib.mkOption {
235 type = lib.types.bool;
238 Whether to enable Telegent TG100 specific tweaks.
242 transcoding = lib.mkOption {
243 type = lib.types.bool;
246 Whether to enable transcoding.
250 dataDir = lib.mkOption {
251 type = lib.types.path;
252 default = "/var/lib/${name}";
253 defaultText = lib.literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
255 The directory where Gerbera/Mediatomb stores its state, data, etc.
259 pcDirectoryHide = lib.mkOption {
260 type = lib.types.bool;
263 Whether to list the top-level directory or not (from upnp client standpoint).
267 user = lib.mkOption {
268 type = lib.types.str;
269 default = "mediatomb";
270 description = "User account under which the service runs.";
273 group = lib.mkOption {
274 type = lib.types.str;
275 default = "mediatomb";
276 description = "Group account under which the service runs.";
279 port = lib.mkOption {
280 type = lib.types.port;
283 The network port to listen on.
287 interface = lib.mkOption {
288 type = lib.types.str;
291 A specific interface to bind to.
295 openFirewall = lib.mkOption {
296 type = lib.types.bool;
299 If false (the default), this is up to the user to declare the firewall rules.
300 If true, this opens port 1900 (tcp and udp) and the port specified by
301 {option}`sercvices.mediatomb.port`.
303 If the option {option}`services.mediatomb.interface` is set,
304 the firewall rules opened are dedicated to that interface. Otherwise,
305 those rules are opened globally.
309 uuid = lib.mkOption {
310 type = lib.types.str;
311 default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
313 A unique (on your network) to identify the server by.
317 mediaDirectories = lib.mkOption {
318 type = with lib.types; listOf (submodule mediaDirectory);
321 Declare media directories to index.
324 { path = "/data/pictures"; recursive = false; hidden-files = false; }
325 { path = "/data/audio"; recursive = true; hidden-files = false; }
329 customCfg = lib.mkOption {
330 type = lib.types.bool;
333 Allow the service to create and use its own config file inside the `dataDir` as
334 configured by {option}`services.mediatomb.dataDir`.
335 Deactivated by default, the service then runs with the configuration generated from this module.
336 Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
337 config.xml within the configured `dataDir`. It's up to the user to make a correct
346 ###### implementation
348 config = let binaryCommand = "${pkg}/bin/${name}";
349 interfaceFlag = lib.optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
350 configFlag = lib.optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
351 in lib.mkIf cfg.enable {
352 systemd.services.mediatomb = {
353 description = "${cfg.serverName} media Server";
354 # Gerbera might fail if the network interface is not available on startup
355 # https://github.com/gerbera/gerbera/issues/1324
356 wants = [ "network-online.target" ];
357 after = [ "network.target" "network-online.target" ];
358 wantedBy = [ "multi-user.target" ];
359 serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
360 serviceConfig.User = cfg.user;
361 serviceConfig.Group = cfg.group;
364 users.groups = lib.optionalAttrs (cfg.group == "mediatomb") {
368 users.users = lib.optionalAttrs (cfg.user == "mediatomb") {
374 description = "${name} DLNA Server User";
378 # Open firewall only if users enable it
379 networking.firewall = lib.mkMerge [
380 (lib.mkIf (cfg.openFirewall && cfg.interface != "") {
381 interfaces."${cfg.interface}" = defaultFirewallRules;
383 (lib.mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)