diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml index c50bc58ca451..6dcf8b6b23b9 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 529f584a201e..ec6ef3d7b53a 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 mediatomb stores its state, data, etc. + The directory where ${cfg.serverName} stores its state, data, etc. + ''; + }; + + pcDirectoryHide = mkOption { + type = types.bool; + default = true; + description = '' + 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 000000000000..b7a126a01ad5 --- /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() + ''; +})