1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10 cfg = config.services.ytdl-sub;
11
12 settingsFormat = pkgs.formats.yaml { };
13in
14{
15 meta.maintainers = with lib.maintainers; [ defelo ];
16
17 options.services.ytdl-sub = {
18 package = lib.mkPackageOption pkgs "ytdl-sub" { };
19
20 user = lib.mkOption {
21 type = lib.types.str;
22 default = "ytdl-sub";
23 description = "User account under which ytdl-sub runs.";
24 };
25
26 group = lib.mkOption {
27 type = lib.types.str;
28 default = "ytdl-sub";
29 description = "Group under which ytdl-sub runs.";
30 };
31
32 instances = lib.mkOption {
33 default = { };
34 description = "Configuration for ytdl-sub instances.";
35 type = lib.types.attrsOf (
36 lib.types.submodule (
37 { name, ... }:
38 {
39 options = {
40 enable = lib.mkEnableOption "ytdl-sub instance";
41
42 schedule = lib.mkOption {
43 type = lib.types.nullOr lib.types.str;
44 description = "How often to run ytdl-sub. See {manpage}`systemd.time(7)` for the format.";
45 default = null;
46 example = "0/6:0";
47 };
48
49 config = lib.mkOption {
50 type = settingsFormat.type;
51 description = "Configuration for ytdl-sub. See <https://ytdl-sub.readthedocs.io/en/latest/config_reference/config_yaml.html> for more information.";
52 default = { };
53 example = {
54 presets."YouTube Playlist" = {
55 download = "{subscription_value}";
56 output_options = {
57 output_directory = "YouTube";
58 file_name = "{channel}/{playlist_title}/{playlist_index_padded}_{title}.{ext}";
59 maintain_download_archive = true;
60 };
61 };
62 };
63 };
64
65 subscriptions = lib.mkOption {
66 type = settingsFormat.type;
67 description = "Subscriptions for ytdl-sub. See <https://ytdl-sub.readthedocs.io/en/latest/config_reference/subscription_yaml.html> for more information.";
68 default = { };
69 example = {
70 "YouTube Playlist" = {
71 "Some Playlist" = "https://www.youtube.com/playlist?list=...";
72 };
73 };
74 };
75 };
76
77 config = {
78 config.configuration.working_directory = "/run/ytdl-sub/${utils.escapeSystemdPath name}";
79 };
80 }
81 )
82 );
83 };
84 };
85
86 config = lib.mkIf (cfg.instances != { }) {
87 systemd.services =
88 let
89 mkService =
90 name: instance:
91 let
92 configFile = settingsFormat.generate "config.yaml" instance.config;
93 subscriptionsFile = settingsFormat.generate "subscriptions.yaml" instance.subscriptions;
94 in
95 lib.nameValuePair "ytdl-sub-${utils.escapeSystemdPath name}" {
96 inherit (instance) enable;
97
98 wants = [ "network-online.target" ];
99 after = [ "network-online.target" ];
100
101 startAt = lib.optional (instance.schedule != null) instance.schedule;
102
103 serviceConfig = {
104 User = cfg.user;
105 Group = cfg.group;
106
107 RuntimeDirectory = "ytdl-sub/${utils.escapeSystemdPath name}";
108 StateDirectory = "ytdl-sub/${utils.escapeSystemdPath name}";
109 WorkingDirectory = "/var/lib/ytdl-sub/${utils.escapeSystemdPath name}";
110
111 ExecStart = "${lib.getExe cfg.package} --config ${configFile} sub ${subscriptionsFile}";
112
113 # Hardening
114 CapabilityBoundingSet = [ "" ];
115 DeviceAllow = [ "" ];
116 LockPersonality = true;
117 PrivateDevices = true;
118 PrivateTmp = true;
119 PrivateUsers = true;
120 ProcSubset = "pid";
121 ProtectClock = true;
122 ProtectControlGroups = true;
123 ProtectHome = true;
124 ProtectHostname = true;
125 ProtectKernelLogs = true;
126 ProtectKernelModules = true;
127 ProtectKernelTunables = true;
128 ProtectProc = "invisible";
129 ProtectSystem = "strict";
130 RestrictAddressFamilies = [
131 "AF_INET"
132 "AF_INET6"
133 "AF_UNIX"
134 ];
135 RestrictNamespaces = true;
136 RestrictRealtime = true;
137 RestrictSUIDSGID = true;
138 SystemCallArchitectures = "native";
139 };
140 };
141 in
142 lib.mapAttrs' mkService cfg.instances;
143
144 users.users = lib.mkIf (cfg.user == "ytdl-sub") {
145 ytdl-sub = {
146 isSystemUser = true;
147 group = cfg.group;
148 };
149 };
150
151 users.groups = lib.mkIf (cfg.group == "ytdl-sub") {
152 ytdl-sub = { };
153 };
154 };
155}