···226226 <filename>testing-python.nix</filename> respectively.
227227 </para>
228228 </listitem>
229229- </itemizedlist>
229229+ <listitem>
230230+ <para>
231231+ The <link linked="opt-services.mediatomb">mediatomb service</link>
232232+ declares new options. It also adapts existing options so the
233233+ configuration generation is now lazy. The existing option
234234+ <literal>customCfg</literal> (defaults to false), when enabled, stops
235235+ the service configuration generation completely. It then expects the
236236+ users to provide their own correct configuration at the right location
237237+ (whereas the configuration was generated and not used at all before).
238238+ The new option <literal>transcodingOption</literal> (defaults to no)
239239+ allows a generated configuration. It makes the mediatomb service pulls
240240+ the necessary runtime dependencies in the nix store (whereas it was
241241+ generated with hardcoded values before). The new option
242242+ <literal>mediaDirectories</literal> allows the users to declare autoscan
243243+ media directories from their nixos configuration:
244244+ <programlisting>
245245+ services.mediatomb.mediaDirectories = [
246246+ { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; }
247247+ { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; }
248248+ ];
249249+ </programlisting>
250250+ </para>
251251+ </listitem>
252252+ </itemizedlist>
230253 </section>
231254232255 <section xmlns="http://docbook.org/ns/docbook"
···863886 </listitem>
864887 </itemizedlist>
865888 </para>
889889+ <listitem>
890890+ <para>
891891+ The <link linked="opt-services.mediatomb">mediatomb service</link> is
892892+ now using by default the new and maintained fork
893893+ <literal>gerbera</literal> package instead of the unmaintained
894894+ <literal>mediatomb</literal> package. If you want to keep the old
895895+ behavior, you must declare it with:
896896+ <programlisting>
897897+ services.mediatomb.package = pkgs.mediatomb;
898898+ </programlisting>
899899+ One new option <literal>openFirewall<literal> has been introduced which
900900+ defaults to false. If you relied on the service declaration to add the
901901+ firewall rules itself before, you should now declare it with:
902902+ <programlisting>
903903+ services.mediatomb.openFirewall = true;
904904+ </programlisting>
905905+ </para>
866906 </listitem>
867907 </itemizedlist>
868908 </section>
+166-67
nixos/modules/services/misc/mediatomb.nix
···6677 gid = config.ids.gids.mediatomb;
88 cfg = config.services.mediatomb;
99+ name = cfg.package.pname;
1010+ pkg = cfg.package;
1111+ optionYesNo = option: if option then "yes" else "no";
1212+ # configuration on media directory
1313+ mediaDirectory = {
1414+ options = {
1515+ path = mkOption {
1616+ type = types.str;
1717+ description = ''
1818+ Absolute directory path to the media directory to index.
1919+ '';
2020+ };
2121+ recursive = mkOption {
2222+ type = types.bool;
2323+ default = false;
2424+ description = "Whether the indexation must take place recursively or not.";
2525+ };
2626+ hidden-files = mkOption {
2727+ type = types.bool;
2828+ default = true;
2929+ description = "Whether to index the hidden files or not.";
3030+ };
3131+ };
3232+ };
3333+ toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
9341010- mtConf = pkgs.writeText "config.xml" ''
1111- <?xml version="1.0" encoding="UTF-8"?>
1212- <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">
3535+ transcodingConfig = if cfg.transcoding then with pkgs; ''
3636+ <transcoding enabled="yes">
3737+ <mimetype-profile-mappings>
3838+ <transcode mimetype="video/x-flv" using="vlcmpeg" />
3939+ <transcode mimetype="application/ogg" using="vlcmpeg" />
4040+ <transcode mimetype="audio/ogg" using="ogg2mp3" />
4141+ <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
4242+ </mimetype-profile-mappings>
4343+ <profiles>
4444+ <profile name="ogg2mp3" enabled="no" type="external">
4545+ <mimetype>audio/mpeg</mimetype>
4646+ <accept-url>no</accept-url>
4747+ <first-resource>yes</first-resource>
4848+ <accept-ogg-theora>no</accept-ogg-theora>
4949+ <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
5050+ <buffer size="1048576" chunk-size="131072" fill-size="262144" />
5151+ </profile>
5252+ <profile name="vlcmpeg" enabled="no" type="external">
5353+ <mimetype>video/mpeg</mimetype>
5454+ <accept-url>yes</accept-url>
5555+ <first-resource>yes</first-resource>
5656+ <accept-ogg-theora>yes</accept-ogg-theora>
5757+ <agent command="${libsForQt5.vlc}/bin/vlc"
5858+ 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" />
5959+ <buffer size="14400000" chunk-size="512000" fill-size="120000" />
6060+ </profile>
6161+ </profiles>
6262+ </transcoding>
6363+'' else ''
6464+ <transcoding enabled="no">
6565+ </transcoding>
6666+'';
6767+6868+ configText = optionalString (! cfg.customCfg) ''
6969+<?xml version="1.0" encoding="UTF-8"?>
7070+<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">
1371 <server>
1472 <ui enabled="yes" show-tooltips="yes">
1573 <accounts enabled="no" session-timeout="30">
1616- <account user="mediatomb" password="mediatomb"/>
7474+ <account user="${name}" password="${name}"/>
1775 </accounts>
1876 </ui>
1977 <name>${cfg.serverName}</name>
2078 <udn>uuid:${cfg.uuid}</udn>
2179 <home>${cfg.dataDir}</home>
2222- <webroot>${pkgs.mediatomb}/share/mediatomb/web</webroot>
8080+ <interface>${cfg.interface}</interface>
8181+ <webroot>${pkg}/share/${name}/web</webroot>
8282+ <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
2383 <storage>
2484 <sqlite3 enabled="yes">
2525- <database-file>mediatomb.db</database-file>
8585+ <database-file>${name}.db</database-file>
2686 </sqlite3>
2787 </storage>
2828- <protocolInfo extend="${if cfg.ps3Support then "yes" else "no"}"/>
2929- ${if cfg.dsmSupport then ''
8888+ <protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
8989+ ${optionalString cfg.dsmSupport ''
3090 <custom-http-headers>
3191 <add header="X-User-Agent: redsonic"/>
3292 </custom-http-headers>
33933494 <manufacturerURL>redsonic.com</manufacturerURL>
3595 <modelNumber>105</modelNumber>
3636- '' else ""}
3737- ${if cfg.tg100Support then ''
9696+ ''}
9797+ ${optionalString cfg.tg100Support ''
3898 <upnp-string-limit>101</upnp-string-limit>
3939- '' else ""}
9999+ ''}
40100 <extended-runtime-options>
41101 <mark-played-items enabled="yes" suppress-cds-updates="yes">
42102 <string mode="prepend">*</string>
···47107 </extended-runtime-options>
48108 </server>
49109 <import hidden-files="no">
110110+ <autoscan use-inotify="auto">
111111+ ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
112112+ </autoscan>
50113 <scripting script-charset="UTF-8">
5151- <common-script>${pkgs.mediatomb}/share/mediatomb/js/common.js</common-script>
5252- <playlist-script>${pkgs.mediatomb}/share/mediatomb/js/playlists.js</playlist-script>
114114+ <common-script>${pkg}/share/${name}/js/common.js</common-script>
115115+ <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
53116 <virtual-layout type="builtin">
5454- <import-script>${pkgs.mediatomb}/share/mediatomb/js/import.js</import-script>
117117+ <import-script>${pkg}/share/${name}/js/import.js</import-script>
55118 </virtual-layout>
56119 </scripting>
57120 <mappings>
···75138 <map from="flv" to="video/x-flv"/>
76139 <map from="mkv" to="video/x-matroska"/>
77140 <map from="mka" to="audio/x-matroska"/>
7878- ${if cfg.ps3Support then ''
141141+ ${optionalString cfg.ps3Support ''
79142 <map from="avi" to="video/divx"/>
8080- '' else ""}
8181- ${if cfg.dsmSupport then ''
143143+ ''}
144144+ ${optionalString cfg.dsmSupport ''
82145 <map from="avi" to="video/avi"/>
8383- '' else ""}
146146+ ''}
84147 </extension-mimetype>
85148 <mimetype-upnpclass>
86149 <map from="audio/*" to="object.item.audioItem.musicTrack"/>
···108171 </mappings>
109172 <online-content>
110173 <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
111111- <favorites user="mediatomb"/>
174174+ <favorites user="${name}"/>
112175 <standardfeed feed="most_viewed" time-range="today"/>
113113- <playlists user="mediatomb"/>
114114- <uploads user="mediatomb"/>
176176+ <playlists user="${name}"/>
177177+ <uploads user="${name}"/>
115178 <standardfeed feed="recently_featured" time-range="today"/>
116179 </YouTube>
117180 </online-content>
118181 </import>
119119- <transcoding enabled="${if cfg.transcoding then "yes" else "no"}">
120120- <mimetype-profile-mappings>
121121- <transcode mimetype="video/x-flv" using="vlcmpeg"/>
122122- <transcode mimetype="application/ogg" using="vlcmpeg"/>
123123- <transcode mimetype="application/ogg" using="oggflac2raw"/>
124124- <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
125125- </mimetype-profile-mappings>
126126- <profiles>
127127- <profile name="oggflac2raw" enabled="no" type="external">
128128- <mimetype>audio/L16</mimetype>
129129- <accept-url>no</accept-url>
130130- <first-resource>yes</first-resource>
131131- <accept-ogg-theora>no</accept-ogg-theora>
132132- <agent command="ogg123" arguments="-d raw -o byteorder:big -f %out %in"/>
133133- <buffer size="1048576" chunk-size="131072" fill-size="262144"/>
134134- </profile>
135135- <profile name="vlcmpeg" enabled="no" type="external">
136136- <mimetype>video/mpeg</mimetype>
137137- <accept-url>yes</accept-url>
138138- <first-resource>yes</first-resource>
139139- <accept-ogg-theora>yes</accept-ogg-theora>
140140- <agent command="vlc" 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"/>
141141- <buffer size="14400000" chunk-size="512000" fill-size="120000"/>
142142- </profile>
143143- </profiles>
144144- </transcoding>
182182+ ${transcodingConfig}
145183 </config>
146146- '';
184184+'';
185185+ defaultFirewallRules = {
186186+ # udp 1900 port needs to be opened for SSDP (not configurable within
187187+ # mediatomb/gerbera) cf.
188188+ # http://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
189189+ allowedUDPPorts = [ 1900 cfg.port ];
190190+ allowedTCPPorts = [ cfg.port ];
191191+ };
147192148193in {
149149-150194151195 ###### interface
152196···158202 type = types.bool;
159203 default = false;
160204 description = ''
161161- Whether to enable the mediatomb DLNA server.
205205+ Whether to enable the Gerbera/Mediatomb DLNA server.
162206 '';
163207 };
164208165209 serverName = mkOption {
166210 type = types.str;
167167- default = "mediatomb";
211211+ default = "Gerbera (Mediatomb)";
168212 description = ''
169213 How to identify the server on the network.
170214 '';
171215 };
172216217217+ package = mkOption {
218218+ type = types.package;
219219+ example = literalExample "pkgs.mediatomb";
220220+ default = pkgs.gerbera;
221221+ description = ''
222222+ Underlying package to be used with the module (default: pkgs.gerbera).
223223+ '';
224224+ };
225225+173226 ps3Support = mkOption {
174227 type = types.bool;
175228 default = false;
···206259207260 dataDir = mkOption {
208261 type = types.path;
209209- default = "/var/lib/mediatomb";
262262+ default = "/var/lib/${name}";
263263+ description = ''
264264+ The directory where ${cfg.serverName} stores its state, data, etc.
265265+ '';
266266+ };
267267+268268+ pcDirectoryHide = mkOption {
269269+ type = types.bool;
270270+ default = true;
210271 description = ''
211211- The directory where mediatomb stores its state, data, etc.
272272+ Whether to list the top-level directory or not (from upnp client standpoint).
212273 '';
213274 };
214275215276 user = mkOption {
277277+ type = types.str;
216278 default = "mediatomb";
217217- description = "User account under which mediatomb runs.";
279279+ description = "User account under which ${name} runs.";
218280 };
219281220282 group = mkOption {
283283+ type = types.str;
221284 default = "mediatomb";
222222- description = "Group account under which mediatomb runs.";
285285+ description = "Group account under which ${name} runs.";
223286 };
224287225288 port = mkOption {
289289+ type = types.int;
226290 default = 49152;
227291 description = ''
228292 The network port to listen on.
···230294 };
231295232296 interface = mkOption {
297297+ type = types.str;
233298 default = "";
234299 description = ''
235300 A specific interface to bind to.
236301 '';
237302 };
238303304304+ openFirewall = mkOption {
305305+ type = types.bool;
306306+ default = false;
307307+ description = ''
308308+ If false (the default), this is up to the user to declare the firewall rules.
309309+ If true, this opens the 1900 (tcp and udp) and ${toString cfg.port} (tcp) ports.
310310+ If the option cfg.interface is set, the firewall rules opened are
311311+ dedicated to that interface. Otherwise, those rules are opened
312312+ globally.
313313+ '';
314314+ };
315315+239316 uuid = mkOption {
317317+ type = types.str;
240318 default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
241319 description = ''
242320 A unique (on your network) to identify the server by.
243321 '';
244322 };
245323324324+ mediaDirectories = mkOption {
325325+ type = with types; listOf (submodule mediaDirectory);
326326+ default = {};
327327+ description = ''
328328+ Declare media directories to index.
329329+ '';
330330+ example = [
331331+ { path = "/data/pictures"; recursive = false; hidden-files = false; }
332332+ { path = "/data/audio"; recursive = true; hidden-files = false; }
333333+ ];
334334+ };
335335+246336 customCfg = mkOption {
247337 type = types.bool;
248338 default = false;
249339 description = ''
250250- Allow mediatomb to create and use its own config file inside ${cfg.dataDir}.
340340+ Allow ${name} to create and use its own config file inside ${cfg.dataDir}.
341341+ Deactivated by default, the service then runs with the configuration generated from this module.
342342+ Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
343343+ ${cfg.dataDir}/config.xml. It's up to the user to make a correct configuration file.
251344 '';
252345 };
346346+253347 };
254348 };
255349256350257351 ###### implementation
258352259259- config = mkIf cfg.enable {
353353+ config = let binaryCommand = "${pkg}/bin/${name}";
354354+ interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
355355+ configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
356356+ in mkIf cfg.enable {
260357 systemd.services.mediatomb = {
261261- description = "MediaTomb media Server";
358358+ description = "${cfg.serverName} media Server";
262359 after = [ "network.target" ];
263360 wantedBy = [ "multi-user.target" ];
264264- path = [ pkgs.mediatomb ];
265265- serviceConfig.ExecStart = "${pkgs.mediatomb}/bin/mediatomb -p ${toString cfg.port} ${if cfg.interface!="" then "-e ${cfg.interface}" else ""} ${if cfg.customCfg then "" else "-c ${mtConf}"} -m ${cfg.dataDir}";
266266- serviceConfig.User = "${cfg.user}";
361361+ serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
362362+ serviceConfig.User = cfg.user;
267363 };
268364269365 users.groups = optionalAttrs (cfg.group == "mediatomb") {
···274370 mediatomb = {
275371 isSystemUser = true;
276372 group = cfg.group;
277277- home = "${cfg.dataDir}";
373373+ home = cfg.dataDir;
278374 createHome = true;
279279- description = "Mediatomb DLNA Server User";
375375+ description = "${name} DLNA Server User";
280376 };
281377 };
282378283283- networking.firewall = {
284284- allowedUDPPorts = [ 1900 cfg.port ];
285285- allowedTCPPorts = [ cfg.port ];
286286- };
379379+ # Open firewall only if users enable it
380380+ networking.firewall = mkMerge [
381381+ (mkIf (cfg.openFirewall && cfg.interface != "") {
382382+ interfaces."${cfg.interface}" = defaultFirewallRules;
383383+ })
384384+ (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
385385+ ];
287386 };
288387}
+81
nixos/tests/mediatomb.nix
···11+import ./make-test-python.nix ({ pkgs, ... }:
22+33+{
44+ name = "mediatomb";
55+66+ nodes = {
77+ serverGerbera =
88+ { ... }:
99+ let port = 49152;
1010+ in {
1111+ imports = [ ../modules/profiles/minimal.nix ];
1212+ services.mediatomb = {
1313+ enable = true;
1414+ serverName = "Gerbera";
1515+ package = pkgs.gerbera;
1616+ interface = "eth1"; # accessible from test
1717+ openFirewall = true;
1818+ mediaDirectories = [
1919+ { path = "/var/lib/gerbera/pictures"; recursive = false; hidden-files = false; }
2020+ { path = "/var/lib/gerbera/audio"; recursive = true; hidden-files = false; }
2121+ ];
2222+ };
2323+ };
2424+2525+ serverMediatomb =
2626+ { ... }:
2727+ let port = 49151;
2828+ in {
2929+ imports = [ ../modules/profiles/minimal.nix ];
3030+ services.mediatomb = {
3131+ enable = true;
3232+ serverName = "Mediatomb";
3333+ package = pkgs.mediatomb;
3434+ interface = "eth1";
3535+ inherit port;
3636+ mediaDirectories = [
3737+ { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; }
3838+ { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; }
3939+ ];
4040+ };
4141+ networking.firewall.interfaces.eth1 = {
4242+ allowedUDPPorts = [ 1900 port ];
4343+ allowedTCPPorts = [ port ];
4444+ };
4545+ };
4646+4747+ client = { ... }: { };
4848+ };
4949+5050+ testScript =
5151+ ''
5252+ start_all()
5353+5454+ port = 49151
5555+ serverMediatomb.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}")
5656+ serverMediatomb.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb")
5757+ serverMediatomb.wait_for_unit("mediatomb")
5858+ serverMediatomb.wait_for_open_port(port)
5959+ serverMediatomb.succeed(f"curl --fail http://serverMediatomb:{port}/")
6060+ page = client.succeed(f"curl --fail http://serverMediatomb:{port}/")
6161+ assert "MediaTomb" in page and "Gerbera" not in page
6262+ serverMediatomb.shutdown()
6363+6464+ port = 49152
6565+ serverGerbera.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}")
6666+ serverGerbera.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb")
6767+ # service running gerbera fails the first time claiming something is already bound
6868+ # gerbera[715]: 2020-07-18 23:52:14 info: Please check if another instance of Gerbera or
6969+ # gerbera[715]: 2020-07-18 23:52:14 info: another application is running on port TCP 49152 or UDP 1900.
7070+ # I did not find anything so here I work around this
7171+ serverGerbera.succeed("sleep 2")
7272+ serverGerbera.wait_until_succeeds("systemctl restart mediatomb")
7373+ serverGerbera.wait_for_unit("mediatomb")
7474+ serverGerbera.succeed(f"curl --fail http://serverGerbera:{port}/")
7575+ page = client.succeed(f"curl --fail http://serverGerbera:{port}/")
7676+ assert "Gerbera" in page and "MediaTomb" not in page
7777+7878+ serverGerbera.shutdown()
7979+ client.shutdown()
8080+ '';
8181+})