croc: 10.1.1 -> 10.1.3 (#364662)
[NixPkgs.git] / nixos / modules / services / video / epgstation / default.nix
blob99190297eb758646fd8488d1384fa0f53480b36d
2   config,
3   lib,
4   options,
5   pkgs,
6   ...
7 }:
9 let
10   cfg = config.services.epgstation;
11   opt = options.services.epgstation;
13   description = "EPGStation: DVR system for Mirakurun-managed TV tuners";
15   username = config.users.users.epgstation.name;
16   groupname = config.users.users.epgstation.group;
17   mirakurun = {
18     sock = config.services.mirakurun.unixSocket;
19     option = options.services.mirakurun.unixSocket;
20   };
22   yaml = pkgs.formats.yaml { };
23   settingsTemplate = yaml.generate "config.yml" cfg.settings;
24   preStartScript = pkgs.writeScript "epgstation-prestart" ''
25     #!${pkgs.runtimeShell}
27     DB_PASSWORD_FILE=${lib.escapeShellArg cfg.database.passwordFile}
29     if [[ ! -f "$DB_PASSWORD_FILE" ]]; then
30       printf "[FATAL] File containing the DB password was not found in '%s'. Double check the NixOS option '%s'." \
31         "$DB_PASSWORD_FILE" ${lib.escapeShellArg opt.database.passwordFile} >&2
32       exit 1
33     fi
35     DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})"
37     # setup configuration
38     touch /etc/epgstation/config.yml
39     chmod 640 /etc/epgstation/config.yml
40     sed \
41       -e "s,@dbPassword@,$DB_PASSWORD,g" \
42       ${settingsTemplate} > /etc/epgstation/config.yml
43     chown "${username}:${groupname}" /etc/epgstation/config.yml
45     # NOTE: Use password authentication, since mysqljs does not yet support auth_socket
46     if [ ! -e /var/lib/epgstation/db-created ]; then
47       ${pkgs.mariadb}/bin/mysql -e \
48         "GRANT ALL ON \`${cfg.database.name}\`.* TO '${username}'@'localhost' IDENTIFIED by '$DB_PASSWORD';"
49       touch /var/lib/epgstation/db-created
50     fi
51   '';
53   streamingConfig = lib.importJSON ./streaming.json;
54   logConfig = yaml.generate "logConfig.yml" {
55     appenders.stdout.type = "stdout";
56     categories = {
57       default = {
58         appenders = [ "stdout" ];
59         level = "info";
60       };
61       system = {
62         appenders = [ "stdout" ];
63         level = "info";
64       };
65       access = {
66         appenders = [ "stdout" ];
67         level = "info";
68       };
69       stream = {
70         appenders = [ "stdout" ];
71         level = "info";
72       };
73     };
74   };
76   # Deprecate top level options that are redundant.
77   deprecateTopLevelOption =
78     config:
79     lib.mkRenamedOptionModule
80       (
81         [
82           "services"
83           "epgstation"
84         ]
85         ++ config
86       )
87       (
88         [
89           "services"
90           "epgstation"
91           "settings"
92         ]
93         ++ config
94       );
96   removeOption =
97     config: instruction:
98     lib.mkRemovedOptionModule (
99       [
100         "services"
101         "epgstation"
102       ]
103       ++ config
104     ) instruction;
107   meta.maintainers = with lib.maintainers; [ midchildan ];
109   imports = [
110     (deprecateTopLevelOption [ "port" ])
111     (deprecateTopLevelOption [ "socketioPort" ])
112     (deprecateTopLevelOption [ "clientSocketioPort" ])
113     (removeOption [ "basicAuth" ] "Use a TLS-terminated reverse proxy with authentication instead.")
114   ];
116   options.services.epgstation = {
117     enable = lib.mkEnableOption description;
119     package = lib.mkPackageOption pkgs "epgstation" { };
121     ffmpeg = lib.mkPackageOption pkgs "ffmpeg" {
122       default = "ffmpeg-headless";
123       example = "ffmpeg-full";
124     };
126     usePreconfiguredStreaming = lib.mkOption {
127       type = lib.types.bool;
128       default = true;
129       description = ''
130         Use preconfigured default streaming options.
132         Upstream defaults:
133         <https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template>
134       '';
135     };
137     openFirewall = lib.mkOption {
138       type = lib.types.bool;
139       default = false;
140       description = ''
141         Open ports in the firewall for the EPGStation web interface.
143         ::: {.warning}
144         Exposing EPGStation to the open internet is generally advised
145         against. Only use it inside a trusted local network, or consider
146         putting it behind a VPN if you want remote access.
147         :::
148       '';
149     };
151     database = {
152       name = lib.mkOption {
153         type = lib.types.str;
154         default = "epgstation";
155         description = ''
156           Name of the MySQL database that holds EPGStation's data.
157         '';
158       };
160       passwordFile = lib.mkOption {
161         type = lib.types.path;
162         example = "/run/keys/epgstation-db-password";
163         description = ''
164           A file containing the password for the database named
165           {option}`database.name`.
166         '';
167       };
168     };
170     # The defaults for some options come from the upstream template
171     # configuration, which is the one that users would get if they follow the
172     # upstream instructions. This is, in some cases, different from the
173     # application defaults. Some options like encodeProcessNum and
174     # concurrentEncodeNum doesn't have an optimal default value that works for
175     # all hardware setups and/or performance requirements. For those kind of
176     # options, the application default wouldn't always result in the expected
177     # out-of-the-box behavior because it's the responsibility of the user to
178     # configure them according to their needs. In these cases, the value in the
179     # upstream template configuration should serve as a "good enough" default.
180     settings = lib.mkOption {
181       description = ''
182         Options to add to config.yml.
184         Documentation:
185         <https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md>
186       '';
188       default = { };
189       example = {
190         recPriority = 20;
191         conflictPriority = 10;
192       };
194       type = lib.types.submodule {
195         freeformType = yaml.type;
197         options.port = lib.mkOption {
198           type = lib.types.port;
199           default = 20772;
200           description = ''
201             HTTP port for EPGStation to listen on.
202           '';
203         };
205         options.socketioPort = lib.mkOption {
206           type = lib.types.port;
207           default = cfg.settings.port + 1;
208           defaultText = lib.literalExpression "config.${opt.settings}.port + 1";
209           description = ''
210             Socket.io port for EPGStation to listen on. It is valid to share
211             ports with {option}`${opt.settings}.port`.
212           '';
213         };
215         options.clientSocketioPort = lib.mkOption {
216           type = lib.types.port;
217           default = cfg.settings.socketioPort;
218           defaultText = lib.literalExpression "config.${opt.settings}.socketioPort";
219           description = ''
220             Socket.io port that the web client is going to connect to. This may
221             be different from {option}`${opt.settings}.socketioPort` if
222             EPGStation is hidden behind a reverse proxy.
223           '';
224         };
226         options.mirakurunPath =
227           with mirakurun;
228           lib.mkOption {
229             type = lib.types.str;
230             default = "http+unix://${lib.replaceStrings [ "/" ] [ "%2F" ] sock}";
231             defaultText = lib.literalExpression ''
232               "http+unix://''${lib.replaceStrings ["/"] ["%2F"] config.${option}}"
233             '';
234             example = "http://localhost:40772";
235             description = "URL to connect to Mirakurun.";
236           };
238         options.encodeProcessNum = lib.mkOption {
239           type = lib.types.ints.positive;
240           default = 4;
241           description = ''
242             The maximum number of processes that EPGStation would allow to run
243             at the same time for encoding or streaming videos.
244           '';
245         };
247         options.concurrentEncodeNum = lib.mkOption {
248           type = lib.types.ints.positive;
249           default = 1;
250           description = ''
251             The maximum number of encoding jobs that EPGStation would run at the
252             same time.
253           '';
254         };
256         options.encode = lib.mkOption {
257           type = with lib.types; listOf attrs;
258           description = "Encoding presets for recorded videos.";
259           default = [
260             {
261               name = "H.264";
262               cmd = "%NODE% ${cfg.package}/libexec/enc.js";
263               suffix = ".mp4";
264             }
265           ];
266           defaultText = lib.literalExpression ''
267             [
268               {
269                 name = "H.264";
270                 cmd = "%NODE% config.${opt.package}/libexec/enc.js";
271                 suffix = ".mp4";
272               }
273             ]
274           '';
275         };
276       };
277     };
278   };
280   config = lib.mkIf cfg.enable {
281     assertions = [
282       {
283         assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings);
284         message = ''
285           The option config.${opt.settings}.readOnlyOnce can no longer be used
286           since it's been removed. No replacements are available.
287         '';
288       }
289     ];
291     environment.etc = {
292       "epgstation/epgUpdaterLogConfig.yml".source = logConfig;
293       "epgstation/operatorLogConfig.yml".source = logConfig;
294       "epgstation/serviceLogConfig.yml".source = logConfig;
295     };
297     networking.firewall = lib.mkIf cfg.openFirewall {
298       allowedTCPPorts = with cfg.settings; [
299         port
300         socketioPort
301       ];
302     };
304     users.users.epgstation = {
305       description = "EPGStation user";
306       group = config.users.groups.epgstation.name;
307       isSystemUser = true;
309       # NPM insists on creating ~/.npm
310       home = "/var/cache/epgstation";
311     };
313     users.groups.epgstation = { };
315     services.mirakurun.enable = lib.mkDefault true;
317     services.mysql = {
318       enable = lib.mkDefault true;
319       package = lib.mkDefault pkgs.mariadb;
320       ensureDatabases = [ cfg.database.name ];
321       # FIXME: enable once mysqljs supports auth_socket
322       # https://github.com/mysqljs/mysql/issues/1507
323       #
324       # ensureUsers = [ {
325       #   name = username;
326       #   ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
327       # } ];
328     };
330     services.epgstation.settings =
331       let
332         defaultSettings = {
333           dbtype = lib.mkDefault "mysql";
334           mysql = {
335             socketPath = lib.mkDefault "/run/mysqld/mysqld.sock";
336             user = username;
337             password = lib.mkDefault "@dbPassword@";
338             database = cfg.database.name;
339           };
341           ffmpeg = lib.mkDefault "${cfg.ffmpeg}/bin/ffmpeg";
342           ffprobe = lib.mkDefault "${cfg.ffmpeg}/bin/ffprobe";
344           # for disambiguation with TypeScript files
345           recordedFileExtension = lib.mkDefault ".m2ts";
346         };
347       in
348       lib.mkMerge [
349         defaultSettings
350         (lib.mkIf cfg.usePreconfiguredStreaming streamingConfig)
351       ];
353     systemd.tmpfiles.settings."10-epgstation" = lib.listToAttrs (
354       map
355         (
356           dir:
357           lib.nameValuePair dir {
358             d = {
359               user = username;
360               group = groupname;
361             };
362           }
363         )
364         [
365           "/var/lib/epgstation/key"
366           "/var/lib/epgstation/streamfiles"
367           "/var/lib/epgstation/drop"
368           "/var/lib/epgstation/recorded"
369           "/var/lib/epgstation/thumbnail"
370           "/var/lib/epgstation/db/subscribers"
371           "/var/lib/epgstation/db/migrations/mysql"
372           "/var/lib/epgstation/db/migrations/postgres"
373           "/var/lib/epgstation/db/migrations/sqlite"
374         ]
375     );
377     systemd.services.epgstation = {
378       inherit description;
380       wantedBy = [ "multi-user.target" ];
381       after =
382         [ "network.target" ]
383         ++ lib.optional config.services.mirakurun.enable "mirakurun.service"
384         ++ lib.optional config.services.mysql.enable "mysql.service";
386       environment.NODE_ENV = "production";
388       serviceConfig = {
389         ExecStart = "${cfg.package}/bin/epgstation start";
390         ExecStartPre = "+${preStartScript}";
391         User = username;
392         Group = groupname;
393         CacheDirectory = "epgstation";
394         StateDirectory = "epgstation";
395         LogsDirectory = "epgstation";
396         ConfigurationDirectory = "epgstation";
397       };
398     };
399   };