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