at master 15 kB view raw
1{ 2 config, 3 lib, 4 options, 5 pkgs, 6 ... 7}: 8let 9 10 gid = config.ids.gids.mediatomb; 11 cfg = config.services.mediatomb; 12 opt = options.services.mediatomb; 13 name = cfg.package.pname; 14 pkg = cfg.package; 15 optionYesNo = option: if option then "yes" else "no"; 16 # configuration on media directory 17 mediaDirectory = { 18 options = { 19 path = lib.mkOption { 20 type = lib.types.str; 21 description = '' 22 Absolute directory path to the media directory to index. 23 ''; 24 }; 25 recursive = lib.mkOption { 26 type = lib.types.bool; 27 default = false; 28 description = "Whether the indexation must take place recursively or not."; 29 }; 30 hidden-files = lib.mkOption { 31 type = lib.types.bool; 32 default = true; 33 description = "Whether to index the hidden files or not."; 34 }; 35 }; 36 }; 37 toMediaDirectory = 38 d: 39 "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n"; 40 41 transcodingConfig = 42 if cfg.transcoding then 43 with pkgs; 44 '' 45 <transcoding enabled="yes"> 46 <mimetype-profile-mappings> 47 <transcode mimetype="video/x-flv" using="vlcmpeg" /> 48 <transcode mimetype="application/ogg" using="vlcmpeg" /> 49 <transcode mimetype="audio/ogg" using="ogg2mp3" /> 50 </mimetype-profile-mappings> 51 <profiles> 52 <profile name="ogg2mp3" enabled="no" type="external"> 53 <mimetype>audio/mpeg</mimetype> 54 <accept-url>no</accept-url> 55 <first-resource>yes</first-resource> 56 <accept-ogg-theora>no</accept-ogg-theora> 57 <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" /> 58 <buffer size="1048576" chunk-size="131072" fill-size="262144" /> 59 </profile> 60 <profile name="vlcmpeg" enabled="no" type="external"> 61 <mimetype>video/mpeg</mimetype> 62 <accept-url>yes</accept-url> 63 <first-resource>yes</first-resource> 64 <accept-ogg-theora>yes</accept-ogg-theora> 65 <agent command="${lib.getExe vlc}" 66 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" /> 67 <buffer size="14400000" chunk-size="512000" fill-size="120000" /> 68 </profile> 69 </profiles> 70 </transcoding> 71 '' 72 else 73 '' 74 <transcoding enabled="no"> 75 </transcoding> 76 ''; 77 78 configText = lib.optionalString (!cfg.customCfg) '' 79 <?xml version="1.0" encoding="UTF-8"?> 80 <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"> 81 <server> 82 <ui enabled="yes" show-tooltips="yes"> 83 <accounts enabled="no" session-timeout="30"> 84 <account user="${name}" password="${name}"/> 85 </accounts> 86 </ui> 87 <name>${cfg.serverName}</name> 88 <udn>uuid:${cfg.uuid}</udn> 89 <home>${cfg.dataDir}</home> 90 <interface>${cfg.interface}</interface> 91 <webroot>${pkg}/share/${name}/web</webroot> 92 <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/> 93 <storage> 94 <sqlite3 enabled="yes"> 95 <database-file>${name}.db</database-file> 96 </sqlite3> 97 </storage> 98 <protocolInfo extend="${optionYesNo cfg.ps3Support}"/> 99 ${lib.optionalString cfg.dsmSupport '' 100 <custom-http-headers> 101 <add header="X-User-Agent: redsonic"/> 102 </custom-http-headers> 103 104 <manufacturerURL>redsonic.com</manufacturerURL> 105 <modelNumber>105</modelNumber> 106 ''} 107 ${lib.optionalString cfg.tg100Support '' 108 <upnp-string-limit>101</upnp-string-limit> 109 ''} 110 <extended-runtime-options> 111 <mark-played-items enabled="yes" suppress-cds-updates="yes"> 112 <string mode="prepend">*</string> 113 <mark> 114 <content>video</content> 115 </mark> 116 </mark-played-items> 117 </extended-runtime-options> 118 </server> 119 <import hidden-files="no"> 120 <autoscan use-inotify="auto"> 121 ${lib.concatMapStrings toMediaDirectory cfg.mediaDirectories} 122 </autoscan> 123 <scripting script-charset="UTF-8"> 124 <common-script>${pkg}/share/${name}/js/common.js</common-script> 125 <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script> 126 <virtual-layout type="builtin"> 127 <import-script>${pkg}/share/${name}/js/import.js</import-script> 128 </virtual-layout> 129 </scripting> 130 <mappings> 131 <extension-mimetype ignore-unknown="no"> 132 <map from="mp3" to="audio/mpeg"/> 133 <map from="ogx" to="application/ogg"/> 134 <map from="ogv" to="video/ogg"/> 135 <map from="oga" to="audio/ogg"/> 136 <map from="ogg" to="audio/ogg"/> 137 <map from="ogm" to="video/ogg"/> 138 <map from="asf" to="video/x-ms-asf"/> 139 <map from="asx" to="video/x-ms-asf"/> 140 <map from="wma" to="audio/x-ms-wma"/> 141 <map from="wax" to="audio/x-ms-wax"/> 142 <map from="wmv" to="video/x-ms-wmv"/> 143 <map from="wvx" to="video/x-ms-wvx"/> 144 <map from="wm" to="video/x-ms-wm"/> 145 <map from="wmx" to="video/x-ms-wmx"/> 146 <map from="m3u" to="audio/x-mpegurl"/> 147 <map from="pls" to="audio/x-scpls"/> 148 <map from="flv" to="video/x-flv"/> 149 <map from="mkv" to="video/x-matroska"/> 150 <map from="mka" to="audio/x-matroska"/> 151 ${lib.optionalString cfg.ps3Support '' 152 <map from="avi" to="video/divx"/> 153 ''} 154 ${lib.optionalString cfg.dsmSupport '' 155 <map from="avi" to="video/avi"/> 156 ''} 157 </extension-mimetype> 158 <mimetype-upnpclass> 159 <map from="audio/*" to="object.item.audioItem.musicTrack"/> 160 <map from="video/*" to="object.item.videoItem"/> 161 <map from="image/*" to="object.item.imageItem"/> 162 </mimetype-upnpclass> 163 <mimetype-contenttype> 164 <treat mimetype="audio/mpeg" as="mp3"/> 165 <treat mimetype="application/ogg" as="ogg"/> 166 <treat mimetype="audio/ogg" as="ogg"/> 167 <treat mimetype="audio/x-flac" as="flac"/> 168 <treat mimetype="audio/x-ms-wma" as="wma"/> 169 <treat mimetype="audio/x-wavpack" as="wv"/> 170 <treat mimetype="image/jpeg" as="jpg"/> 171 <treat mimetype="audio/x-mpegurl" as="playlist"/> 172 <treat mimetype="audio/x-scpls" as="playlist"/> 173 <treat mimetype="audio/x-wav" as="pcm"/> 174 <treat mimetype="audio/L16" as="pcm"/> 175 <treat mimetype="video/x-msvideo" as="avi"/> 176 <treat mimetype="video/mp4" as="mp4"/> 177 <treat mimetype="audio/mp4" as="mp4"/> 178 <treat mimetype="application/x-iso9660" as="dvd"/> 179 <treat mimetype="application/x-iso9660-image" as="dvd"/> 180 </mimetype-contenttype> 181 </mappings> 182 <online-content> 183 <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no"> 184 <favorites user="${name}"/> 185 <standardfeed feed="most_viewed" time-range="today"/> 186 <playlists user="${name}"/> 187 <uploads user="${name}"/> 188 <standardfeed feed="recently_featured" time-range="today"/> 189 </YouTube> 190 </online-content> 191 </import> 192 ${transcodingConfig} 193 </config> 194 ''; 195 defaultFirewallRules = { 196 # udp 1900 port needs to be opened for SSDP (not configurable within 197 # mediatomb/gerbera) cf. 198 # https://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup 199 allowedUDPPorts = [ 200 1900 201 cfg.port 202 ]; 203 allowedTCPPorts = [ cfg.port ]; 204 }; 205 206in 207{ 208 209 ###### interface 210 211 options = { 212 213 services.mediatomb = { 214 215 enable = lib.mkOption { 216 type = lib.types.bool; 217 default = false; 218 description = '' 219 Whether to enable the Gerbera/Mediatomb DLNA server. 220 ''; 221 }; 222 223 serverName = lib.mkOption { 224 type = lib.types.str; 225 default = "Gerbera (Mediatomb)"; 226 description = '' 227 How to identify the server on the network. 228 ''; 229 }; 230 231 package = lib.mkPackageOption pkgs "gerbera" { }; 232 233 ps3Support = lib.mkOption { 234 type = lib.types.bool; 235 default = false; 236 description = '' 237 Whether to enable ps3 specific tweaks. 238 WARNING: incompatible with DSM 320 support. 239 ''; 240 }; 241 242 dsmSupport = lib.mkOption { 243 type = lib.types.bool; 244 default = false; 245 description = '' 246 Whether to enable D-Link DSM 320 specific tweaks. 247 WARNING: incompatible with ps3 support. 248 ''; 249 }; 250 251 tg100Support = lib.mkOption { 252 type = lib.types.bool; 253 default = false; 254 description = '' 255 Whether to enable Telegent TG100 specific tweaks. 256 ''; 257 }; 258 259 transcoding = lib.mkOption { 260 type = lib.types.bool; 261 default = false; 262 description = '' 263 Whether to enable transcoding. 264 ''; 265 }; 266 267 dataDir = lib.mkOption { 268 type = lib.types.path; 269 default = "/var/lib/${name}"; 270 defaultText = lib.literalExpression ''"/var/lib/''${config.${opt.package}.pname}"''; 271 description = '' 272 The directory where Gerbera/Mediatomb stores its state, data, etc. 273 ''; 274 }; 275 276 pcDirectoryHide = lib.mkOption { 277 type = lib.types.bool; 278 default = true; 279 description = '' 280 Whether to list the top-level directory or not (from upnp client standpoint). 281 ''; 282 }; 283 284 user = lib.mkOption { 285 type = lib.types.str; 286 default = "mediatomb"; 287 description = "User account under which the service runs."; 288 }; 289 290 group = lib.mkOption { 291 type = lib.types.str; 292 default = "mediatomb"; 293 description = "Group account under which the service runs."; 294 }; 295 296 port = lib.mkOption { 297 type = lib.types.port; 298 default = 49152; 299 description = '' 300 The network port to listen on. 301 ''; 302 }; 303 304 interface = lib.mkOption { 305 type = lib.types.str; 306 default = ""; 307 description = '' 308 A specific interface to bind to. 309 ''; 310 }; 311 312 openFirewall = lib.mkOption { 313 type = lib.types.bool; 314 default = false; 315 description = '' 316 If false (the default), this is up to the user to declare the firewall rules. 317 If true, this opens port 1900 (tcp and udp) and the port specified by 318 {option}`sercvices.mediatomb.port`. 319 320 If the option {option}`services.mediatomb.interface` is set, 321 the firewall rules opened are dedicated to that interface. Otherwise, 322 those rules are opened globally. 323 ''; 324 }; 325 326 uuid = lib.mkOption { 327 type = lib.types.str; 328 default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687"; 329 description = '' 330 A unique (on your network) to identify the server by. 331 ''; 332 }; 333 334 mediaDirectories = lib.mkOption { 335 type = with lib.types; listOf (submodule mediaDirectory); 336 default = [ ]; 337 description = '' 338 Declare media directories to index. 339 ''; 340 example = [ 341 { 342 path = "/data/pictures"; 343 recursive = false; 344 hidden-files = false; 345 } 346 { 347 path = "/data/audio"; 348 recursive = true; 349 hidden-files = false; 350 } 351 ]; 352 }; 353 354 customCfg = lib.mkOption { 355 type = lib.types.bool; 356 default = false; 357 description = '' 358 Allow the service to create and use its own config file inside the `dataDir` as 359 configured by {option}`services.mediatomb.dataDir`. 360 Deactivated by default, the service then runs with the configuration generated from this module. 361 Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using 362 config.xml within the configured `dataDir`. It's up to the user to make a correct 363 configuration file. 364 ''; 365 }; 366 367 }; 368 }; 369 370 ###### implementation 371 372 config = 373 let 374 binaryCommand = "${pkg}/bin/${name}"; 375 interfaceFlag = lib.optionalString (cfg.interface != "") "--interface ${cfg.interface}"; 376 configFlag = lib.optionalString ( 377 !cfg.customCfg 378 ) "--config ${pkgs.writeText "config.xml" configText}"; 379 in 380 lib.mkIf cfg.enable { 381 systemd.services.mediatomb = { 382 description = "${cfg.serverName} media Server"; 383 # Gerbera might fail if the network interface is not available on startup 384 # https://github.com/gerbera/gerbera/issues/1324 385 wants = [ "network-online.target" ]; 386 after = [ 387 "network.target" 388 "network-online.target" 389 ]; 390 wantedBy = [ "multi-user.target" ]; 391 serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}"; 392 serviceConfig.User = cfg.user; 393 serviceConfig.Group = cfg.group; 394 }; 395 396 users.groups = lib.optionalAttrs (cfg.group == "mediatomb") { 397 mediatomb.gid = gid; 398 }; 399 400 users.users = lib.optionalAttrs (cfg.user == "mediatomb") { 401 mediatomb = { 402 isSystemUser = true; 403 group = cfg.group; 404 home = cfg.dataDir; 405 createHome = true; 406 description = "${name} DLNA Server User"; 407 }; 408 }; 409 410 # Open firewall only if users enable it 411 networking.firewall = lib.mkMerge [ 412 (lib.mkIf (cfg.openFirewall && cfg.interface != "") { 413 interfaces."${cfg.interface}" = defaultFirewallRules; 414 }) 415 (lib.mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules) 416 ]; 417 }; 418}