diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index c50bc58ca4513..6dcf8b6b23b9d 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -226,7 +226,30 @@ GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
testing-python.nix respectively.
-
+
+
+ The mediatomb service
+ declares new options. It also adapts existing options so the
+ configuration generation is now lazy. The existing option
+ customCfg (defaults to false), when enabled, stops
+ the service configuration generation completely. It then expects the
+ users to provide their own correct configuration at the right location
+ (whereas the configuration was generated and not used at all before).
+ The new option transcodingOption (defaults to no)
+ allows a generated configuration. It makes the mediatomb service pulls
+ the necessary runtime dependencies in the nix store (whereas it was
+ generated with hardcoded values before). The new option
+ mediaDirectories allows the users to declare autoscan
+ media directories from their nixos configuration:
+
+ services.mediatomb.mediaDirectories = [
+ { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; }
+ { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; }
+ ];
+
+
+
+
+
+
+ The mediatomb service is
+ now using by default the new and maintained fork
+ gerbera package instead of the unmaintained
+ mediatomb package. If you want to keep the old
+ behavior, you must declare it with:
+
+ services.mediatomb.package = pkgs.mediatomb;
+
+ One new option openFirewall has been introduced which
+ defaults to false. If you relied on the service declaration to add the
+ firewall rules itself before, you should now declare it with:
+
+ services.mediatomb.openFirewall = true;
+
+
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index 529f584a201e4..ec6ef3d7b53a7 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -6,37 +6,97 @@ let
gid = config.ids.gids.mediatomb;
cfg = config.services.mediatomb;
+ name = cfg.package.pname;
+ pkg = cfg.package;
+ optionYesNo = option: if option then "yes" else "no";
+ # configuration on media directory
+ mediaDirectory = {
+ options = {
+ path = mkOption {
+ type = types.str;
+ description = ''
+ Absolute directory path to the media directory to index.
+ '';
+ };
+ recursive = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether the indexation must take place recursively or not.";
+ };
+ hidden-files = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to index the hidden files or not.";
+ };
+ };
+ };
+ toMediaDirectory = d: "\n";
- mtConf = pkgs.writeText "config.xml" ''
-
-
+ transcodingConfig = if cfg.transcoding then with pkgs; ''
+
+
+
+
+
+
+
+
+
+ audio/mpeg
+ no
+ yes
+ no
+
+
+
+
+ video/mpeg
+ yes
+ yes
+ yes
+
+
+
+
+
+'' else ''
+
+
+'';
+
+ configText = optionalString (! cfg.customCfg) ''
+
+
-
+
${cfg.serverName}
uuid:${cfg.uuid}
${cfg.dataDir}
- ${pkgs.mediatomb}/share/mediatomb/web
+ ${cfg.interface}
+ ${pkg}/share/${name}/web
+
- mediatomb.db
+ ${name}.db
-
- ${if cfg.dsmSupport then ''
+
+ ${optionalString cfg.dsmSupport ''
redsonic.com
105
- '' else ""}
- ${if cfg.tg100Support then ''
+ ''}
+ ${optionalString cfg.tg100Support ''
101
- '' else ""}
+ ''}
*
@@ -47,11 +107,14 @@ let
+
+ ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
+
- ${pkgs.mediatomb}/share/mediatomb/js/common.js
- ${pkgs.mediatomb}/share/mediatomb/js/playlists.js
+ ${pkg}/share/${name}/js/common.js
+ ${pkg}/share/${name}/js/playlists.js
- ${pkgs.mediatomb}/share/mediatomb/js/import.js
+ ${pkg}/share/${name}/js/import.js
@@ -75,12 +138,12 @@ let
- ${if cfg.ps3Support then ''
+ ${optionalString cfg.ps3Support ''
- '' else ""}
- ${if cfg.dsmSupport then ''
+ ''}
+ ${optionalString cfg.dsmSupport ''
- '' else ""}
+ ''}
@@ -108,46 +171,27 @@ let
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
- audio/L16
- no
- yes
- no
-
-
-
-
- video/mpeg
- yes
- yes
- yes
-
-
-
-
-
+ ${transcodingConfig}
- '';
+'';
+ defaultFirewallRules = {
+ # udp 1900 port needs to be opened for SSDP (not configurable within
+ # mediatomb/gerbera) cf.
+ # http://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
+ allowedUDPPorts = [ 1900 cfg.port ];
+ allowedTCPPorts = [ cfg.port ];
+ };
in {
-
###### interface
options = {
@@ -158,18 +202,27 @@ in {
type = types.bool;
default = false;
description = ''
- Whether to enable the mediatomb DLNA server.
+ Whether to enable the Gerbera/Mediatomb DLNA server.
'';
};
serverName = mkOption {
type = types.str;
- default = "mediatomb";
+ default = "Gerbera (Mediatomb)";
description = ''
How to identify the server on the network.
'';
};
+ package = mkOption {
+ type = types.package;
+ example = literalExample "pkgs.mediatomb";
+ default = pkgs.gerbera;
+ description = ''
+ Underlying package to be used with the module (default: pkgs.gerbera).
+ '';
+ };
+
ps3Support = mkOption {
type = types.bool;
default = false;
@@ -206,23 +259,34 @@ in {
dataDir = mkOption {
type = types.path;
- default = "/var/lib/mediatomb";
+ default = "/var/lib/${name}";
+ description = ''
+ The directory where ${cfg.serverName} stores its state, data, etc.
+ '';
+ };
+
+ pcDirectoryHide = mkOption {
+ type = types.bool;
+ default = true;
description = ''
- The directory where mediatomb stores its state, data, etc.
+ Whether to list the top-level directory or not (from upnp client standpoint).
'';
};
user = mkOption {
+ type = types.str;
default = "mediatomb";
- description = "User account under which mediatomb runs.";
+ description = "User account under which ${name} runs.";
};
group = mkOption {
+ type = types.str;
default = "mediatomb";
- description = "Group account under which mediatomb runs.";
+ description = "Group account under which ${name} runs.";
};
port = mkOption {
+ type = types.int;
default = 49152;
description = ''
The network port to listen on.
@@ -230,40 +294,72 @@ in {
};
interface = mkOption {
+ type = types.str;
default = "";
description = ''
A specific interface to bind to.
'';
};
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ If false (the default), this is up to the user to declare the firewall rules.
+ If true, this opens the 1900 (tcp and udp) and ${toString cfg.port} (tcp) ports.
+ If the option cfg.interface is set, the firewall rules opened are
+ dedicated to that interface. Otherwise, those rules are opened
+ globally.
+ '';
+ };
+
uuid = mkOption {
+ type = types.str;
default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
description = ''
A unique (on your network) to identify the server by.
'';
};
+ mediaDirectories = mkOption {
+ type = with types; listOf (submodule mediaDirectory);
+ default = {};
+ description = ''
+ Declare media directories to index.
+ '';
+ example = [
+ { path = "/data/pictures"; recursive = false; hidden-files = false; }
+ { path = "/data/audio"; recursive = true; hidden-files = false; }
+ ];
+ };
+
customCfg = mkOption {
type = types.bool;
default = false;
description = ''
- Allow mediatomb to create and use its own config file inside ${cfg.dataDir}.
+ Allow ${name} to create and use its own config file inside ${cfg.dataDir}.
+ Deactivated by default, the service then runs with the configuration generated from this module.
+ Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
+ ${cfg.dataDir}/config.xml. It's up to the user to make a correct configuration file.
'';
};
+
};
};
###### implementation
- config = mkIf cfg.enable {
+ config = let binaryCommand = "${pkg}/bin/${name}";
+ interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
+ configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
+ in mkIf cfg.enable {
systemd.services.mediatomb = {
- description = "MediaTomb media Server";
+ description = "${cfg.serverName} media Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
- path = [ pkgs.mediatomb ];
- 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}";
- serviceConfig.User = "${cfg.user}";
+ serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
+ serviceConfig.User = cfg.user;
};
users.groups = optionalAttrs (cfg.group == "mediatomb") {
@@ -274,15 +370,18 @@ in {
mediatomb = {
isSystemUser = true;
group = cfg.group;
- home = "${cfg.dataDir}";
+ home = cfg.dataDir;
createHome = true;
- description = "Mediatomb DLNA Server User";
+ description = "${name} DLNA Server User";
};
};
- networking.firewall = {
- allowedUDPPorts = [ 1900 cfg.port ];
- allowedTCPPorts = [ cfg.port ];
- };
+ # Open firewall only if users enable it
+ networking.firewall = mkMerge [
+ (mkIf (cfg.openFirewall && cfg.interface != "") {
+ interfaces."${cfg.interface}" = defaultFirewallRules;
+ })
+ (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
+ ];
};
}
diff --git a/nixos/tests/mediatomb.nix b/nixos/tests/mediatomb.nix
new file mode 100644
index 0000000000000..b7a126a01ad5c
--- /dev/null
+++ b/nixos/tests/mediatomb.nix
@@ -0,0 +1,81 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+{
+ name = "mediatomb";
+
+ nodes = {
+ serverGerbera =
+ { ... }:
+ let port = 49152;
+ in {
+ imports = [ ../modules/profiles/minimal.nix ];
+ services.mediatomb = {
+ enable = true;
+ serverName = "Gerbera";
+ package = pkgs.gerbera;
+ interface = "eth1"; # accessible from test
+ openFirewall = true;
+ mediaDirectories = [
+ { path = "/var/lib/gerbera/pictures"; recursive = false; hidden-files = false; }
+ { path = "/var/lib/gerbera/audio"; recursive = true; hidden-files = false; }
+ ];
+ };
+ };
+
+ serverMediatomb =
+ { ... }:
+ let port = 49151;
+ in {
+ imports = [ ../modules/profiles/minimal.nix ];
+ services.mediatomb = {
+ enable = true;
+ serverName = "Mediatomb";
+ package = pkgs.mediatomb;
+ interface = "eth1";
+ inherit port;
+ mediaDirectories = [
+ { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; }
+ { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; }
+ ];
+ };
+ networking.firewall.interfaces.eth1 = {
+ allowedUDPPorts = [ 1900 port ];
+ allowedTCPPorts = [ port ];
+ };
+ };
+
+ client = { ... }: { };
+ };
+
+ testScript =
+ ''
+ start_all()
+
+ port = 49151
+ serverMediatomb.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}")
+ serverMediatomb.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb")
+ serverMediatomb.wait_for_unit("mediatomb")
+ serverMediatomb.wait_for_open_port(port)
+ serverMediatomb.succeed(f"curl --fail http://serverMediatomb:{port}/")
+ page = client.succeed(f"curl --fail http://serverMediatomb:{port}/")
+ assert "MediaTomb" in page and "Gerbera" not in page
+ serverMediatomb.shutdown()
+
+ port = 49152
+ serverGerbera.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}")
+ serverGerbera.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb")
+ # service running gerbera fails the first time claiming something is already bound
+ # gerbera[715]: 2020-07-18 23:52:14 info: Please check if another instance of Gerbera or
+ # gerbera[715]: 2020-07-18 23:52:14 info: another application is running on port TCP 49152 or UDP 1900.
+ # I did not find anything so here I work around this
+ serverGerbera.succeed("sleep 2")
+ serverGerbera.wait_until_succeeds("systemctl restart mediatomb")
+ serverGerbera.wait_for_unit("mediatomb")
+ serverGerbera.succeed(f"curl --fail http://serverGerbera:{port}/")
+ page = client.succeed(f"curl --fail http://serverGerbera:{port}/")
+ assert "Gerbera" in page and "MediaTomb" not in page
+
+ serverGerbera.shutdown()
+ client.shutdown()
+ '';
+})