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