Merge pull request #93450 from ardumont/gerbera-service

mediatomb: Improve service + add gerbera support and tests

authored by

Timo Kaufmann and committed by
GitHub
19ac436c b836d199

+288 -68
+41 -1
nixos/doc/manual/release-notes/rl-2009.xml
··· 226 226 <filename>testing-python.nix</filename> respectively. 227 227 </para> 228 228 </listitem> 229 - </itemizedlist> 229 + <listitem> 230 + <para> 231 + The <link linked="opt-services.mediatomb">mediatomb service</link> 232 + declares new options. It also adapts existing options so the 233 + configuration generation is now lazy. The existing option 234 + <literal>customCfg</literal> (defaults to false), when enabled, stops 235 + the service configuration generation completely. It then expects the 236 + users to provide their own correct configuration at the right location 237 + (whereas the configuration was generated and not used at all before). 238 + The new option <literal>transcodingOption</literal> (defaults to no) 239 + allows a generated configuration. It makes the mediatomb service pulls 240 + the necessary runtime dependencies in the nix store (whereas it was 241 + generated with hardcoded values before). The new option 242 + <literal>mediaDirectories</literal> allows the users to declare autoscan 243 + media directories from their nixos configuration: 244 + <programlisting> 245 + services.mediatomb.mediaDirectories = [ 246 + { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; } 247 + { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; } 248 + ]; 249 + </programlisting> 250 + </para> 251 + </listitem> 252 + </itemizedlist> 230 253 </section> 231 254 232 255 <section xmlns="http://docbook.org/ns/docbook" ··· 863 886 </listitem> 864 887 </itemizedlist> 865 888 </para> 889 + <listitem> 890 + <para> 891 + The <link linked="opt-services.mediatomb">mediatomb service</link> is 892 + now using by default the new and maintained fork 893 + <literal>gerbera</literal> package instead of the unmaintained 894 + <literal>mediatomb</literal> package. If you want to keep the old 895 + behavior, you must declare it with: 896 + <programlisting> 897 + services.mediatomb.package = pkgs.mediatomb; 898 + </programlisting> 899 + One new option <literal>openFirewall<literal> has been introduced which 900 + defaults to false. If you relied on the service declaration to add the 901 + firewall rules itself before, you should now declare it with: 902 + <programlisting> 903 + services.mediatomb.openFirewall = true; 904 + </programlisting> 905 + </para> 866 906 </listitem> 867 907 </itemizedlist> 868 908 </section>
+166 -67
nixos/modules/services/misc/mediatomb.nix
··· 6 6 7 7 gid = config.ids.gids.mediatomb; 8 8 cfg = config.services.mediatomb; 9 + name = cfg.package.pname; 10 + pkg = cfg.package; 11 + optionYesNo = option: if option then "yes" else "no"; 12 + # configuration on media directory 13 + mediaDirectory = { 14 + options = { 15 + path = mkOption { 16 + type = types.str; 17 + description = '' 18 + Absolute directory path to the media directory to index. 19 + ''; 20 + }; 21 + recursive = mkOption { 22 + type = types.bool; 23 + default = false; 24 + description = "Whether the indexation must take place recursively or not."; 25 + }; 26 + hidden-files = mkOption { 27 + type = types.bool; 28 + default = true; 29 + description = "Whether to index the hidden files or not."; 30 + }; 31 + }; 32 + }; 33 + toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n"; 9 34 10 - mtConf = pkgs.writeText "config.xml" '' 11 - <?xml version="1.0" encoding="UTF-8"?> 12 - <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"> 35 + transcodingConfig = if cfg.transcoding then with pkgs; '' 36 + <transcoding enabled="yes"> 37 + <mimetype-profile-mappings> 38 + <transcode mimetype="video/x-flv" using="vlcmpeg" /> 39 + <transcode mimetype="application/ogg" using="vlcmpeg" /> 40 + <transcode mimetype="audio/ogg" using="ogg2mp3" /> 41 + <transcode mimetype="audio/x-flac" using="oggflac2raw"/> 42 + </mimetype-profile-mappings> 43 + <profiles> 44 + <profile name="ogg2mp3" enabled="no" type="external"> 45 + <mimetype>audio/mpeg</mimetype> 46 + <accept-url>no</accept-url> 47 + <first-resource>yes</first-resource> 48 + <accept-ogg-theora>no</accept-ogg-theora> 49 + <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" /> 50 + <buffer size="1048576" chunk-size="131072" fill-size="262144" /> 51 + </profile> 52 + <profile name="vlcmpeg" enabled="no" type="external"> 53 + <mimetype>video/mpeg</mimetype> 54 + <accept-url>yes</accept-url> 55 + <first-resource>yes</first-resource> 56 + <accept-ogg-theora>yes</accept-ogg-theora> 57 + <agent command="${libsForQt5.vlc}/bin/vlc" 58 + 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" /> 59 + <buffer size="14400000" chunk-size="512000" fill-size="120000" /> 60 + </profile> 61 + </profiles> 62 + </transcoding> 63 + '' else '' 64 + <transcoding enabled="no"> 65 + </transcoding> 66 + ''; 67 + 68 + configText = optionalString (! cfg.customCfg) '' 69 + <?xml version="1.0" encoding="UTF-8"?> 70 + <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"> 13 71 <server> 14 72 <ui enabled="yes" show-tooltips="yes"> 15 73 <accounts enabled="no" session-timeout="30"> 16 - <account user="mediatomb" password="mediatomb"/> 74 + <account user="${name}" password="${name}"/> 17 75 </accounts> 18 76 </ui> 19 77 <name>${cfg.serverName}</name> 20 78 <udn>uuid:${cfg.uuid}</udn> 21 79 <home>${cfg.dataDir}</home> 22 - <webroot>${pkgs.mediatomb}/share/mediatomb/web</webroot> 80 + <interface>${cfg.interface}</interface> 81 + <webroot>${pkg}/share/${name}/web</webroot> 82 + <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/> 23 83 <storage> 24 84 <sqlite3 enabled="yes"> 25 - <database-file>mediatomb.db</database-file> 85 + <database-file>${name}.db</database-file> 26 86 </sqlite3> 27 87 </storage> 28 - <protocolInfo extend="${if cfg.ps3Support then "yes" else "no"}"/> 29 - ${if cfg.dsmSupport then '' 88 + <protocolInfo extend="${optionYesNo cfg.ps3Support}"/> 89 + ${optionalString cfg.dsmSupport '' 30 90 <custom-http-headers> 31 91 <add header="X-User-Agent: redsonic"/> 32 92 </custom-http-headers> 33 93 34 94 <manufacturerURL>redsonic.com</manufacturerURL> 35 95 <modelNumber>105</modelNumber> 36 - '' else ""} 37 - ${if cfg.tg100Support then '' 96 + ''} 97 + ${optionalString cfg.tg100Support '' 38 98 <upnp-string-limit>101</upnp-string-limit> 39 - '' else ""} 99 + ''} 40 100 <extended-runtime-options> 41 101 <mark-played-items enabled="yes" suppress-cds-updates="yes"> 42 102 <string mode="prepend">*</string> ··· 47 107 </extended-runtime-options> 48 108 </server> 49 109 <import hidden-files="no"> 110 + <autoscan use-inotify="auto"> 111 + ${concatMapStrings toMediaDirectory cfg.mediaDirectories} 112 + </autoscan> 50 113 <scripting script-charset="UTF-8"> 51 - <common-script>${pkgs.mediatomb}/share/mediatomb/js/common.js</common-script> 52 - <playlist-script>${pkgs.mediatomb}/share/mediatomb/js/playlists.js</playlist-script> 114 + <common-script>${pkg}/share/${name}/js/common.js</common-script> 115 + <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script> 53 116 <virtual-layout type="builtin"> 54 - <import-script>${pkgs.mediatomb}/share/mediatomb/js/import.js</import-script> 117 + <import-script>${pkg}/share/${name}/js/import.js</import-script> 55 118 </virtual-layout> 56 119 </scripting> 57 120 <mappings> ··· 75 138 <map from="flv" to="video/x-flv"/> 76 139 <map from="mkv" to="video/x-matroska"/> 77 140 <map from="mka" to="audio/x-matroska"/> 78 - ${if cfg.ps3Support then '' 141 + ${optionalString cfg.ps3Support '' 79 142 <map from="avi" to="video/divx"/> 80 - '' else ""} 81 - ${if cfg.dsmSupport then '' 143 + ''} 144 + ${optionalString cfg.dsmSupport '' 82 145 <map from="avi" to="video/avi"/> 83 - '' else ""} 146 + ''} 84 147 </extension-mimetype> 85 148 <mimetype-upnpclass> 86 149 <map from="audio/*" to="object.item.audioItem.musicTrack"/> ··· 108 171 </mappings> 109 172 <online-content> 110 173 <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no"> 111 - <favorites user="mediatomb"/> 174 + <favorites user="${name}"/> 112 175 <standardfeed feed="most_viewed" time-range="today"/> 113 - <playlists user="mediatomb"/> 114 - <uploads user="mediatomb"/> 176 + <playlists user="${name}"/> 177 + <uploads user="${name}"/> 115 178 <standardfeed feed="recently_featured" time-range="today"/> 116 179 </YouTube> 117 180 </online-content> 118 181 </import> 119 - <transcoding enabled="${if cfg.transcoding then "yes" else "no"}"> 120 - <mimetype-profile-mappings> 121 - <transcode mimetype="video/x-flv" using="vlcmpeg"/> 122 - <transcode mimetype="application/ogg" using="vlcmpeg"/> 123 - <transcode mimetype="application/ogg" using="oggflac2raw"/> 124 - <transcode mimetype="audio/x-flac" using="oggflac2raw"/> 125 - </mimetype-profile-mappings> 126 - <profiles> 127 - <profile name="oggflac2raw" enabled="no" type="external"> 128 - <mimetype>audio/L16</mimetype> 129 - <accept-url>no</accept-url> 130 - <first-resource>yes</first-resource> 131 - <accept-ogg-theora>no</accept-ogg-theora> 132 - <agent command="ogg123" arguments="-d raw -o byteorder:big -f %out %in"/> 133 - <buffer size="1048576" chunk-size="131072" fill-size="262144"/> 134 - </profile> 135 - <profile name="vlcmpeg" enabled="no" type="external"> 136 - <mimetype>video/mpeg</mimetype> 137 - <accept-url>yes</accept-url> 138 - <first-resource>yes</first-resource> 139 - <accept-ogg-theora>yes</accept-ogg-theora> 140 - <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"/> 141 - <buffer size="14400000" chunk-size="512000" fill-size="120000"/> 142 - </profile> 143 - </profiles> 144 - </transcoding> 182 + ${transcodingConfig} 145 183 </config> 146 - ''; 184 + ''; 185 + defaultFirewallRules = { 186 + # udp 1900 port needs to be opened for SSDP (not configurable within 187 + # mediatomb/gerbera) cf. 188 + # http://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup 189 + allowedUDPPorts = [ 1900 cfg.port ]; 190 + allowedTCPPorts = [ cfg.port ]; 191 + }; 147 192 148 193 in { 149 - 150 194 151 195 ###### interface 152 196 ··· 158 202 type = types.bool; 159 203 default = false; 160 204 description = '' 161 - Whether to enable the mediatomb DLNA server. 205 + Whether to enable the Gerbera/Mediatomb DLNA server. 162 206 ''; 163 207 }; 164 208 165 209 serverName = mkOption { 166 210 type = types.str; 167 - default = "mediatomb"; 211 + default = "Gerbera (Mediatomb)"; 168 212 description = '' 169 213 How to identify the server on the network. 170 214 ''; 171 215 }; 172 216 217 + package = mkOption { 218 + type = types.package; 219 + example = literalExample "pkgs.mediatomb"; 220 + default = pkgs.gerbera; 221 + description = '' 222 + Underlying package to be used with the module (default: pkgs.gerbera). 223 + ''; 224 + }; 225 + 173 226 ps3Support = mkOption { 174 227 type = types.bool; 175 228 default = false; ··· 206 259 207 260 dataDir = mkOption { 208 261 type = types.path; 209 - default = "/var/lib/mediatomb"; 262 + default = "/var/lib/${name}"; 263 + description = '' 264 + The directory where ${cfg.serverName} stores its state, data, etc. 265 + ''; 266 + }; 267 + 268 + pcDirectoryHide = mkOption { 269 + type = types.bool; 270 + default = true; 210 271 description = '' 211 - The directory where mediatomb stores its state, data, etc. 272 + Whether to list the top-level directory or not (from upnp client standpoint). 212 273 ''; 213 274 }; 214 275 215 276 user = mkOption { 277 + type = types.str; 216 278 default = "mediatomb"; 217 - description = "User account under which mediatomb runs."; 279 + description = "User account under which ${name} runs."; 218 280 }; 219 281 220 282 group = mkOption { 283 + type = types.str; 221 284 default = "mediatomb"; 222 - description = "Group account under which mediatomb runs."; 285 + description = "Group account under which ${name} runs."; 223 286 }; 224 287 225 288 port = mkOption { 289 + type = types.int; 226 290 default = 49152; 227 291 description = '' 228 292 The network port to listen on. ··· 230 294 }; 231 295 232 296 interface = mkOption { 297 + type = types.str; 233 298 default = ""; 234 299 description = '' 235 300 A specific interface to bind to. 236 301 ''; 237 302 }; 238 303 304 + openFirewall = mkOption { 305 + type = types.bool; 306 + default = false; 307 + description = '' 308 + If false (the default), this is up to the user to declare the firewall rules. 309 + If true, this opens the 1900 (tcp and udp) and ${toString cfg.port} (tcp) ports. 310 + If the option cfg.interface is set, the firewall rules opened are 311 + dedicated to that interface. Otherwise, those rules are opened 312 + globally. 313 + ''; 314 + }; 315 + 239 316 uuid = mkOption { 317 + type = types.str; 240 318 default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687"; 241 319 description = '' 242 320 A unique (on your network) to identify the server by. 243 321 ''; 244 322 }; 245 323 324 + mediaDirectories = mkOption { 325 + type = with types; listOf (submodule mediaDirectory); 326 + default = {}; 327 + description = '' 328 + Declare media directories to index. 329 + ''; 330 + example = [ 331 + { path = "/data/pictures"; recursive = false; hidden-files = false; } 332 + { path = "/data/audio"; recursive = true; hidden-files = false; } 333 + ]; 334 + }; 335 + 246 336 customCfg = mkOption { 247 337 type = types.bool; 248 338 default = false; 249 339 description = '' 250 - Allow mediatomb to create and use its own config file inside ${cfg.dataDir}. 340 + Allow ${name} to create and use its own config file inside ${cfg.dataDir}. 341 + Deactivated by default, the service then runs with the configuration generated from this module. 342 + Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using 343 + ${cfg.dataDir}/config.xml. It's up to the user to make a correct configuration file. 251 344 ''; 252 345 }; 346 + 253 347 }; 254 348 }; 255 349 256 350 257 351 ###### implementation 258 352 259 - config = mkIf cfg.enable { 353 + config = let binaryCommand = "${pkg}/bin/${name}"; 354 + interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}"; 355 + configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}"; 356 + in mkIf cfg.enable { 260 357 systemd.services.mediatomb = { 261 - description = "MediaTomb media Server"; 358 + description = "${cfg.serverName} media Server"; 262 359 after = [ "network.target" ]; 263 360 wantedBy = [ "multi-user.target" ]; 264 - path = [ pkgs.mediatomb ]; 265 - 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}"; 266 - serviceConfig.User = "${cfg.user}"; 361 + serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}"; 362 + serviceConfig.User = cfg.user; 267 363 }; 268 364 269 365 users.groups = optionalAttrs (cfg.group == "mediatomb") { ··· 274 370 mediatomb = { 275 371 isSystemUser = true; 276 372 group = cfg.group; 277 - home = "${cfg.dataDir}"; 373 + home = cfg.dataDir; 278 374 createHome = true; 279 - description = "Mediatomb DLNA Server User"; 375 + description = "${name} DLNA Server User"; 280 376 }; 281 377 }; 282 378 283 - networking.firewall = { 284 - allowedUDPPorts = [ 1900 cfg.port ]; 285 - allowedTCPPorts = [ cfg.port ]; 286 - }; 379 + # Open firewall only if users enable it 380 + networking.firewall = mkMerge [ 381 + (mkIf (cfg.openFirewall && cfg.interface != "") { 382 + interfaces."${cfg.interface}" = defaultFirewallRules; 383 + }) 384 + (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules) 385 + ]; 287 386 }; 288 387 }
+81
nixos/tests/mediatomb.nix
··· 1 + import ./make-test-python.nix ({ pkgs, ... }: 2 + 3 + { 4 + name = "mediatomb"; 5 + 6 + nodes = { 7 + serverGerbera = 8 + { ... }: 9 + let port = 49152; 10 + in { 11 + imports = [ ../modules/profiles/minimal.nix ]; 12 + services.mediatomb = { 13 + enable = true; 14 + serverName = "Gerbera"; 15 + package = pkgs.gerbera; 16 + interface = "eth1"; # accessible from test 17 + openFirewall = true; 18 + mediaDirectories = [ 19 + { path = "/var/lib/gerbera/pictures"; recursive = false; hidden-files = false; } 20 + { path = "/var/lib/gerbera/audio"; recursive = true; hidden-files = false; } 21 + ]; 22 + }; 23 + }; 24 + 25 + serverMediatomb = 26 + { ... }: 27 + let port = 49151; 28 + in { 29 + imports = [ ../modules/profiles/minimal.nix ]; 30 + services.mediatomb = { 31 + enable = true; 32 + serverName = "Mediatomb"; 33 + package = pkgs.mediatomb; 34 + interface = "eth1"; 35 + inherit port; 36 + mediaDirectories = [ 37 + { path = "/var/lib/mediatomb/pictures"; recursive = false; hidden-files = false; } 38 + { path = "/var/lib/mediatomb/audio"; recursive = true; hidden-files = false; } 39 + ]; 40 + }; 41 + networking.firewall.interfaces.eth1 = { 42 + allowedUDPPorts = [ 1900 port ]; 43 + allowedTCPPorts = [ port ]; 44 + }; 45 + }; 46 + 47 + client = { ... }: { }; 48 + }; 49 + 50 + testScript = 51 + '' 52 + start_all() 53 + 54 + port = 49151 55 + serverMediatomb.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}") 56 + serverMediatomb.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb") 57 + serverMediatomb.wait_for_unit("mediatomb") 58 + serverMediatomb.wait_for_open_port(port) 59 + serverMediatomb.succeed(f"curl --fail http://serverMediatomb:{port}/") 60 + page = client.succeed(f"curl --fail http://serverMediatomb:{port}/") 61 + assert "MediaTomb" in page and "Gerbera" not in page 62 + serverMediatomb.shutdown() 63 + 64 + port = 49152 65 + serverGerbera.succeed("mkdir -p /var/lib/mediatomb/{pictures,audio}") 66 + serverGerbera.succeed("chown -R mediatomb:mediatomb /var/lib/mediatomb") 67 + # service running gerbera fails the first time claiming something is already bound 68 + # gerbera[715]: 2020-07-18 23:52:14 info: Please check if another instance of Gerbera or 69 + # gerbera[715]: 2020-07-18 23:52:14 info: another application is running on port TCP 49152 or UDP 1900. 70 + # I did not find anything so here I work around this 71 + serverGerbera.succeed("sleep 2") 72 + serverGerbera.wait_until_succeeds("systemctl restart mediatomb") 73 + serverGerbera.wait_for_unit("mediatomb") 74 + serverGerbera.succeed(f"curl --fail http://serverGerbera:{port}/") 75 + page = client.succeed(f"curl --fail http://serverGerbera:{port}/") 76 + assert "Gerbera" in page and "MediaTomb" not in page 77 + 78 + serverGerbera.shutdown() 79 + client.shutdown() 80 + ''; 81 + })