{ config, lib, options, pkgs, ... }: let gid = config.ids.gids.mediatomb; cfg = config.services.mediatomb; opt = options.services.mediatomb; name = cfg.package.pname; pkg = cfg.package; optionYesNo = option: if option then "yes" else "no"; # configuration on media directory mediaDirectory = { options = { path = lib.mkOption { type = lib.types.str; description = '' Absolute directory path to the media directory to index. ''; }; recursive = lib.mkOption { type = lib.types.bool; default = false; description = "Whether the indexation must take place recursively or not."; }; hidden-files = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to index the hidden files or not."; }; }; }; toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n"; transcodingConfig = if cfg.transcoding then with pkgs; '' <transcoding enabled="yes"> <mimetype-profile-mappings> <transcode mimetype="video/x-flv" using="vlcmpeg" /> <transcode mimetype="application/ogg" using="vlcmpeg" /> <transcode mimetype="audio/ogg" using="ogg2mp3" /> </mimetype-profile-mappings> <profiles> <profile name="ogg2mp3" enabled="no" type="external"> <mimetype>audio/mpeg</mimetype> <accept-url>no</accept-url> <first-resource>yes</first-resource> <accept-ogg-theora>no</accept-ogg-theora> <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" /> <buffer size="1048576" chunk-size="131072" fill-size="262144" /> </profile> <profile name="vlcmpeg" enabled="no" type="external"> <mimetype>video/mpeg</mimetype> <accept-url>yes</accept-url> <first-resource>yes</first-resource> <accept-ogg-theora>yes</accept-ogg-theora> <agent command="${lib.getExe 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" /> <buffer size="14400000" chunk-size="512000" fill-size="120000" /> </profile> </profiles> </transcoding> '' else '' <transcoding enabled="no"> </transcoding> ''; configText = lib.optionalString (! cfg.customCfg) '' <?xml version="1.0" encoding="UTF-8"?> <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"> <server> <ui enabled="yes" show-tooltips="yes"> <accounts enabled="no" session-timeout="30"> <account user="${name}" password="${name}"/> </accounts> </ui> <name>${cfg.serverName}</name> <udn>uuid:${cfg.uuid}</udn> <home>${cfg.dataDir}</home> <interface>${cfg.interface}</interface> <webroot>${pkg}/share/${name}/web</webroot> <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/> <storage> <sqlite3 enabled="yes"> <database-file>${name}.db</database-file> </sqlite3> </storage> <protocolInfo extend="${optionYesNo cfg.ps3Support}"/> ${lib.optionalString cfg.dsmSupport '' <custom-http-headers> <add header="X-User-Agent: redsonic"/> </custom-http-headers> <manufacturerURL>redsonic.com</manufacturerURL> <modelNumber>105</modelNumber> ''} ${lib.optionalString cfg.tg100Support '' <upnp-string-limit>101</upnp-string-limit> ''} <extended-runtime-options> <mark-played-items enabled="yes" suppress-cds-updates="yes"> <string mode="prepend">*</string> <mark> <content>video</content> </mark> </mark-played-items> </extended-runtime-options> </server> <import hidden-files="no"> <autoscan use-inotify="auto"> ${lib.concatMapStrings toMediaDirectory cfg.mediaDirectories} </autoscan> <scripting script-charset="UTF-8"> <common-script>${pkg}/share/${name}/js/common.js</common-script> <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script> <virtual-layout type="builtin"> <import-script>${pkg}/share/${name}/js/import.js</import-script> </virtual-layout> </scripting> <mappings> <extension-mimetype ignore-unknown="no"> <map from="mp3" to="audio/mpeg"/> <map from="ogx" to="application/ogg"/> <map from="ogv" to="video/ogg"/> <map from="oga" to="audio/ogg"/> <map from="ogg" to="audio/ogg"/> <map from="ogm" to="video/ogg"/> <map from="asf" to="video/x-ms-asf"/> <map from="asx" to="video/x-ms-asf"/> <map from="wma" to="audio/x-ms-wma"/> <map from="wax" to="audio/x-ms-wax"/> <map from="wmv" to="video/x-ms-wmv"/> <map from="wvx" to="video/x-ms-wvx"/> <map from="wm" to="video/x-ms-wm"/> <map from="wmx" to="video/x-ms-wmx"/> <map from="m3u" to="audio/x-mpegurl"/> <map from="pls" to="audio/x-scpls"/> <map from="flv" to="video/x-flv"/> <map from="mkv" to="video/x-matroska"/> <map from="mka" to="audio/x-matroska"/> ${lib.optionalString cfg.ps3Support '' <map from="avi" to="video/divx"/> ''} ${lib.optionalString cfg.dsmSupport '' <map from="avi" to="video/avi"/> ''} </extension-mimetype> <mimetype-upnpclass> <map from="audio/*" to="object.item.audioItem.musicTrack"/> <map from="video/*" to="object.item.videoItem"/> <map from="image/*" to="object.item.imageItem"/> </mimetype-upnpclass> <mimetype-contenttype> <treat mimetype="audio/mpeg" as="mp3"/> <treat mimetype="application/ogg" as="ogg"/> <treat mimetype="audio/ogg" as="ogg"/> <treat mimetype="audio/x-flac" as="flac"/> <treat mimetype="audio/x-ms-wma" as="wma"/> <treat mimetype="audio/x-wavpack" as="wv"/> <treat mimetype="image/jpeg" as="jpg"/> <treat mimetype="audio/x-mpegurl" as="playlist"/> <treat mimetype="audio/x-scpls" as="playlist"/> <treat mimetype="audio/x-wav" as="pcm"/> <treat mimetype="audio/L16" as="pcm"/> <treat mimetype="video/x-msvideo" as="avi"/> <treat mimetype="video/mp4" as="mp4"/> <treat mimetype="audio/mp4" as="mp4"/> <treat mimetype="application/x-iso9660" as="dvd"/> <treat mimetype="application/x-iso9660-image" as="dvd"/> </mimetype-contenttype> </mappings> <online-content> <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no"> <favorites user="${name}"/> <standardfeed feed="most_viewed" time-range="today"/> <playlists user="${name}"/> <uploads user="${name}"/> <standardfeed feed="recently_featured" time-range="today"/> </YouTube> </online-content> </import> ${transcodingConfig} </config> ''; defaultFirewallRules = { # udp 1900 port needs to be opened for SSDP (not configurable within # mediatomb/gerbera) cf. # https://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup allowedUDPPorts = [ 1900 cfg.port ]; allowedTCPPorts = [ cfg.port ]; }; in { ###### interface options = { services.mediatomb = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable the Gerbera/Mediatomb DLNA server. ''; }; serverName = lib.mkOption { type = lib.types.str; default = "Gerbera (Mediatomb)"; description = '' How to identify the server on the network. ''; }; package = lib.mkPackageOption pkgs "gerbera" { }; ps3Support = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable ps3 specific tweaks. WARNING: incompatible with DSM 320 support. ''; }; dsmSupport = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable D-Link DSM 320 specific tweaks. WARNING: incompatible with ps3 support. ''; }; tg100Support = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable Telegent TG100 specific tweaks. ''; }; transcoding = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable transcoding. ''; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/${name}"; defaultText = lib.literalExpression ''"/var/lib/''${config.${opt.package}.pname}"''; description = '' The directory where Gerbera/Mediatomb stores its state, data, etc. ''; }; pcDirectoryHide = lib.mkOption { type = lib.types.bool; default = true; description = '' Whether to list the top-level directory or not (from upnp client standpoint). ''; }; user = lib.mkOption { type = lib.types.str; default = "mediatomb"; description = "User account under which the service runs."; }; group = lib.mkOption { type = lib.types.str; default = "mediatomb"; description = "Group account under which the service runs."; }; port = lib.mkOption { type = lib.types.port; default = 49152; description = '' The network port to listen on. ''; }; interface = lib.mkOption { type = lib.types.str; default = ""; description = '' A specific interface to bind to. ''; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = '' If false (the default), this is up to the user to declare the firewall rules. If true, this opens port 1900 (tcp and udp) and the port specified by {option}`sercvices.mediatomb.port`. If the option {option}`services.mediatomb.interface` is set, the firewall rules opened are dedicated to that interface. Otherwise, those rules are opened globally. ''; }; uuid = lib.mkOption { type = lib.types.str; default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687"; description = '' A unique (on your network) to identify the server by. ''; }; mediaDirectories = lib.mkOption { type = with lib.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 = lib.mkOption { type = lib.types.bool; default = false; description = '' Allow the service to create and use its own config file inside the `dataDir` as configured by {option}`services.mediatomb.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 config.xml within the configured `dataDir`. It's up to the user to make a correct configuration file. ''; }; }; }; ###### implementation config = let binaryCommand = "${pkg}/bin/${name}"; interfaceFlag = lib.optionalString ( cfg.interface != "") "--interface ${cfg.interface}"; configFlag = lib.optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}"; in lib.mkIf cfg.enable { systemd.services.mediatomb = { description = "${cfg.serverName} media Server"; # Gerbera might fail if the network interface is not available on startup # https://github.com/gerbera/gerbera/issues/1324 wants = [ "network-online.target" ]; after = [ "network.target" "network-online.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}"; serviceConfig.User = cfg.user; serviceConfig.Group = cfg.group; }; users.groups = lib.optionalAttrs (cfg.group == "mediatomb") { mediatomb.gid = gid; }; users.users = lib.optionalAttrs (cfg.user == "mediatomb") { mediatomb = { isSystemUser = true; group = cfg.group; home = cfg.dataDir; createHome = true; description = "${name} DLNA Server User"; }; }; # Open firewall only if users enable it networking.firewall = lib.mkMerge [ (lib.mkIf (cfg.openFirewall && cfg.interface != "") { interfaces."${cfg.interface}" = defaultFirewallRules; }) (lib.mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules) ]; }; }