flake-update-20260201
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.beets-auto-import;
12
13 # Convert schedule shortcuts to systemd OnCalendar format
14 scheduleToCalendar =
15 schedule:
16 if schedule == "hourly" then
17 "hourly"
18 else if schedule == "daily" then
19 "daily"
20 else if schedule == "weekly" then
21 "weekly"
22 else if schedule == "monthly" then
23 "monthly"
24 else
25 schedule;
26in
27{
28 options.services.beets-auto-import = {
29 enable = mkEnableOption "Beets auto-import and playlist update service";
30
31 package = mkOption {
32 type = types.package;
33 default = pkgs.beets;
34 defaultText = literalExpression "pkgs.beets";
35 description = "Beets package to use (can include custom plugins)";
36 };
37
38 user = mkOption {
39 type = types.str;
40 default = "vincent";
41 description = "User to run the beets service as";
42 };
43
44 group = mkOption {
45 type = types.str;
46 default = "users";
47 description = "Group to run the beets service as";
48 };
49
50 musicDir = mkOption {
51 type = types.str;
52 default = "/neo/music";
53 description = "Base music directory";
54 };
55
56 importDirs = mkOption {
57 type = types.listOf types.str;
58 default = [
59 "library"
60 "soundtrack"
61 "compilation"
62 ];
63 description = "Subdirectories under musicDir to import (relative paths)";
64 };
65
66 updatePlaylists = mkOption {
67 type = types.bool;
68 default = true;
69 description = "Run beet splupdate after import to regenerate smart playlists";
70 };
71
72 schedule = mkOption {
73 type = types.str;
74 default = "daily";
75 description = ''
76 When to run the import. Can be "hourly", "daily", "weekly", "monthly", or a systemd OnCalendar format.
77 See systemd.time(7) for OnCalendar format details.
78 '';
79 example = "weekly";
80 };
81
82 notification = {
83 enable = mkEnableOption "notifications via ntfy";
84
85 ntfyUrl = mkOption {
86 type = types.str;
87 default = "https://ntfy.sbr.pm";
88 description = "URL of ntfy server";
89 };
90
91 topic = mkOption {
92 type = types.str;
93 default = "homelab";
94 description = "ntfy topic for notifications";
95 };
96
97 tokenFile = mkOption {
98 type = types.nullOr types.path;
99 default = null;
100 description = "Path to file containing ntfy authentication token (optional)";
101 };
102 };
103 };
104
105 config =
106 let
107 # Build the import paths
108 importPaths = map (dir: "${cfg.musicDir}/${dir}") cfg.importDirs;
109
110 # Build the beet import command
111 beetsImportScript = pkgs.writeShellScript "beets-auto-import-run" ''
112 set -euo pipefail
113
114 echo "Starting beets auto-import..."
115 echo "Import directories: ${concatStringsSep " " importPaths}"
116
117 # Import each subdirectory separately for better error isolation
118 for dir in ${concatStringsSep " " importPaths}; do
119 if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then
120 echo "Importing subdirectories of: $dir"
121 for subdir in "$dir"/*; do
122 if [ -d "$subdir" ]; then
123 echo " Importing: $subdir"
124 ${cfg.package}/bin/beet import -q "$subdir" || true
125 fi
126 done
127 else
128 echo "Skipping empty or missing directory: $dir"
129 fi
130 done
131
132 ${optionalString cfg.updatePlaylists ''
133 echo "Updating smart playlists..."
134 ${cfg.package}/bin/beet splupdate
135 ''}
136
137 echo "Beets auto-import complete!"
138 '';
139 in
140 mkIf cfg.enable {
141 # Systemd timer for scheduled import
142 systemd.timers.beets-auto-import = {
143 wantedBy = [ "timers.target" ];
144 timerConfig = {
145 OnCalendar = scheduleToCalendar cfg.schedule;
146 Persistent = true;
147 RandomizedDelaySec = "15m";
148 };
149 };
150
151 # Systemd service to run beets
152 systemd.services.beets-auto-import = {
153 description = "Auto-import music to beets and update playlists";
154 after = [ "network-online.target" ];
155 wants = [ "network-online.target" ];
156
157 environment = {
158 HOME = "/home/${cfg.user}";
159 };
160
161 serviceConfig = {
162 Type = "oneshot";
163 User = cfg.user;
164 Group = cfg.group;
165
166 # Run the import script
167 ExecStart = beetsImportScript;
168
169 # Notifications on success (if enabled)
170 ExecStartPost = mkIf cfg.notification.enable (
171 pkgs.writeShellScript "beets-auto-import-notify-success" ''
172 ${
173 if cfg.notification.tokenFile != null then
174 ''
175 ${pkgs.curl}/bin/curl \
176 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.notification.tokenFile})" \
177 -H "Title: Beets Import Complete" \
178 -H "Tags: musical_note,white_check_mark" \
179 -d "Successfully imported music and updated playlists" \
180 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
181 ''
182 else
183 ''
184 ${pkgs.curl}/bin/curl \
185 -H "Title: Beets Import Complete" \
186 -H "Tags: musical_note,white_check_mark" \
187 -d "Successfully imported music and updated playlists" \
188 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
189 ''
190 }
191 ''
192 );
193
194 # Resource limits (low priority)
195 Nice = 15;
196 IOSchedulingClass = "idle";
197 CPUSchedulingPolicy = "idle";
198
199 # Security hardening
200 PrivateTmp = true;
201 NoNewPrivileges = true;
202 ProtectSystem = "strict";
203 ProtectHome = "read-only";
204 ReadWritePaths = [
205 cfg.musicDir
206 "/home/${cfg.user}/.config/beets" # beets config/db
207 ];
208 };
209
210 # Notify on failure (if enabled)
211 onFailure = mkIf cfg.notification.enable [ "beets-auto-import-failure.service" ];
212 };
213
214 # Failure notification service
215 systemd.services.beets-auto-import-failure = mkIf cfg.notification.enable {
216 description = "Notify on beets auto-import failure";
217 serviceConfig = {
218 Type = "oneshot";
219 ExecStart = pkgs.writeShellScript "beets-auto-import-notify-failure" ''
220 ${
221 if cfg.notification.tokenFile != null then
222 ''
223 ${pkgs.curl}/bin/curl \
224 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.notification.tokenFile})" \
225 -H "Title: Beets Import Failed" \
226 -H "Priority: high" \
227 -H "Tags: warning,musical_note" \
228 -d "Beets auto-import failed. Check logs: journalctl -u beets-auto-import" \
229 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
230 ''
231 else
232 ''
233 ${pkgs.curl}/bin/curl \
234 -H "Title: Beets Import Failed" \
235 -H "Priority: high" \
236 -H "Tags: warning,musical_note" \
237 -d "Beets auto-import failed. Check logs: journalctl -u beets-auto-import" \
238 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
239 ''
240 }
241 '';
242 };
243 };
244 };
245}