1 { config, lib, options, pkgs, ... }:
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;
12 sock = config.services.mirakurun.unixSocket;
13 option = options.services.mirakurun.unixSocket;
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
29 DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})"
32 touch /etc/epgstation/config.yml
33 chmod 640 /etc/epgstation/config.yml
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
47 streamingConfig = lib.importJSON ./streaming.json;
48 logConfig = yaml.generate "logConfig.yml" {
49 appenders.stdout.type = "stdout";
51 default = { appenders = [ "stdout" ]; level = "info"; };
52 system = { appenders = [ "stdout" ]; level = "info"; };
53 access = { appenders = [ "stdout" ]; level = "info"; };
54 stream = { appenders = [ "stdout" ]; level = "info"; };
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)
70 meta.maintainers = with lib.maintainers; [ midchildan ];
73 (deprecateTopLevelOption [ "port" ])
74 (deprecateTopLevelOption [ "socketioPort" ])
75 (deprecateTopLevelOption [ "clientSocketioPort" ])
76 (removeOption [ "basicAuth" ]
77 "Use a TLS-terminated reverse proxy with authentication instead.")
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";
90 usePreconfiguredStreaming = lib.mkOption {
91 type = lib.types.bool;
94 Use preconfigured default streaming options.
97 <https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template>
101 openFirewall = lib.mkOption {
102 type = lib.types.bool;
105 Open ports in the firewall for the EPGStation web interface.
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.
116 name = lib.mkOption {
117 type = lib.types.str;
118 default = "epgstation";
120 Name of the MySQL database that holds EPGStation's data.
124 passwordFile = lib.mkOption {
125 type = lib.types.path;
126 example = "/run/keys/epgstation-db-password";
128 A file containing the password for the database named
129 {option}`database.name`.
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 {
146 Options to add to config.yml.
149 <https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md>
155 conflictPriority = 10;
158 type = lib.types.submodule {
159 freeformType = yaml.type;
161 options.port = lib.mkOption {
162 type = lib.types.port;
165 HTTP port for EPGStation to listen on.
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";
174 Socket.io port for EPGStation to listen on. It is valid to share
175 ports with {option}`${opt.settings}.port`.
179 options.clientSocketioPort = lib.mkOption {
180 type = lib.types.port;
181 default = cfg.settings.socketioPort;
182 defaultText = lib.literalExpression "config.${opt.settings}.socketioPort";
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.
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}}"
196 example = "http://localhost:40772";
197 description = "URL to connect to Mirakurun.";
200 options.encodeProcessNum = lib.mkOption {
201 type = lib.types.ints.positive;
204 The maximum number of processes that EPGStation would allow to run
205 at the same time for encoding or streaming videos.
209 options.concurrentEncodeNum = lib.mkOption {
210 type = lib.types.ints.positive;
213 The maximum number of encoding jobs that EPGStation would run at the
218 options.encode = lib.mkOption {
219 type = with lib.types; listOf attrs;
220 description = "Encoding presets for recorded videos.";
224 cmd = "%NODE% ${cfg.package}/libexec/enc.js";
228 defaultText = lib.literalExpression ''
232 cmd = "%NODE% config.${opt.package}/libexec/enc.js";
242 config = lib.mkIf cfg.enable {
245 assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings);
247 The option config.${opt.settings}.readOnlyOnce can no longer be used
248 since it's been removed. No replacements are available.
254 "epgstation/epgUpdaterLogConfig.yml".source = logConfig;
255 "epgstation/operatorLogConfig.yml".source = logConfig;
256 "epgstation/serviceLogConfig.yml".source = logConfig;
259 networking.firewall = lib.mkIf cfg.openFirewall {
260 allowedTCPPorts = with cfg.settings; [ port socketioPort ];
263 users.users.epgstation = {
264 description = "EPGStation user";
265 group = config.users.groups.epgstation.name;
268 # NPM insists on creating ~/.npm
269 home = "/var/cache/epgstation";
272 users.groups.epgstation = { };
274 services.mirakurun.enable = lib.mkDefault true;
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
285 # ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
289 services.epgstation.settings =
292 dbtype = lib.mkDefault "mysql";
294 socketPath = lib.mkDefault "/run/mysqld/mysqld.sock";
296 password = lib.mkDefault "@dbPassword@";
297 database = cfg.database.name;
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";
309 (lib.mkIf cfg.usePreconfiguredStreaming streamingConfig)
312 systemd.tmpfiles.settings."10-epgstation" =
314 (map (dir: lib.nameValuePair dir {
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"
332 systemd.services.epgstation = {
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";
343 ExecStart = "${cfg.package}/bin/epgstation start";
344 ExecStartPre = "+${preStartScript}";
347 CacheDirectory = "epgstation";
348 StateDirectory = "epgstation";
349 LogsDirectory = "epgstation";
350 ConfigurationDirectory = "epgstation";