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