auto-update-daily-20260202
1{
2 libx,
3 globals,
4 lib,
5 pkgs,
6 config,
7 ...
8}:
9let
10 # Service defaults for media/homelab services
11 serviceDefaults = libx.mkServiceDefaults { };
12
13 # Common rsync configuration for rhea backups
14 rheaBackupDefaults = {
15 source = {
16 host = "rhea.sbr.pm";
17 user = "vincent";
18 };
19 destination = "/neo";
20 delete = true; # Mirror mode: delete files in destination that don't exist in source
21 user = "vincent";
22 group = "users";
23 rsyncArgs = [
24 "--exclude=.Trash-*"
25 "--exclude=lost+found"
26 ];
27 sshArgs = [
28 "-o StrictHostKeyChecking=accept-new"
29 ];
30 };
31
32 # Exportarr services configuration (data-driven approach)
33 exportarrServices = {
34 lidarr = {
35 port = 9709;
36 servicePort = 8686;
37 };
38 };
39in
40{
41 imports = [
42 ../common/services/samba.nix
43 ../common/services/homepage.nix
44 ../common/services/prometheus-exporters-node.nix
45 ../../modules/audible-sync
46 ../../modules/beets-auto-import
47 ../../modules/music-playlist-dl
48 ../../modules/harmonia
49 ../../modules/xmpp-research-bot
50 ./xmpp.nix
51 ];
52
53 users.users.vincent.linger = true;
54
55 # Allow navidrome to read music files owned by users group
56 users.users.navidrome.extraGroups = [ "users" ];
57
58 # Age secrets for homepage widgets (API keys for *arr services on rhea)
59 age.secrets = {
60 "exportarr-sonarr-apikey" = {
61 file = ../../secrets/rhea/exportarr-sonarr-apikey.age;
62 mode = "440";
63 group = "homepage";
64 };
65 "exportarr-radarr-apikey" = {
66 file = ../../secrets/rhea/exportarr-radarr-apikey.age;
67 mode = "440";
68 group = "homepage";
69 };
70 "exportarr-lidarr-apikey" = {
71 file = ../../secrets/rhea/exportarr-lidarr-apikey.age;
72 mode = "440";
73 group = "homepage";
74 };
75 "restic-aix-password" = {
76 file = ../../secrets/aion/restic-aix-password.age;
77 mode = "400";
78 owner = "vincent";
79 group = "users";
80 };
81 "ntfy-token" = {
82 file = ../../secrets/sakhalin/ntfy-token.age;
83 mode = "400";
84 owner = "vincent";
85 group = "users";
86 };
87 "harmonia-aion-signing-key" = {
88 file = ../../secrets/harmonia/aion-signing-key.age;
89 mode = "440";
90 owner = "root";
91 group = "root";
92 };
93 # TODO: Uncomment after creating secrets with agenix
94 # "xmpp-research-bot-password" = {
95 # file = ../../secrets/aion/xmpp-research-bot-password.age;
96 # mode = "400";
97 # owner = "vincent";
98 # group = "users";
99 # };
100 # "anthropic-api-key" = {
101 # file = ../../secrets/aion/anthropic-api-key.age;
102 # mode = "400";
103 # owner = "vincent";
104 # group = "users";
105 # };
106 };
107
108 services = {
109 # Paperless storage plan (not enabled here yet).
110 # Target layout: /neo/paperless/{consume,data,media,trash} (or /neo/documents with migration).
111 # Filename format mirrors sakhalin: year/type + rich metadata.
112 # paperless = {
113 # settings = {
114 # PAPERLESS_CONSUMPTION_DIR = "/neo/paperless/consume";
115 # PAPERLESS_DATA_DIR = "/neo/paperless/data";
116 # PAPERLESS_MEDIA_ROOT = "/neo/paperless/media";
117 # PAPERLESS_EMPTY_TRASH_DIR = "/neo/paperless/trash";
118 # PAPERLESS_FILENAME_FORMAT = "{{ created_year }}/{{ document_type }}/{{ created }} - {{ correspondent }} - {{ title }} - {{ asn }} ({{ doc_pk }})";
119 # PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = "true";
120 # };
121 # };
122 # Binary cache server (aarch64-linux)
123 harmonia-cache = {
124 enable = true;
125 signKeyPath = config.age.secrets."harmonia-aion-signing-key".path;
126 port = 5000;
127 workers = 4;
128 priority = 30;
129
130 # Nightly cache pre-population
131 builder = {
132 enable = true;
133 systems = [
134 "aion" # Self
135 "athena" # RPi4
136 "demeter" # RPi4
137 "aix" # RPi4
138 "rhea" # Media server
139 ];
140 schedule = "02:30"; # 2:30 AM daily (offset from aomi)
141 notification = {
142 enable = true;
143 tokenFile = config.age.secrets."ntfy-token".path;
144 };
145 };
146 };
147
148 wireguard = {
149 enable = true;
150 ips = libx.wg-ips globals.machines.aion.net.vpn.ips;
151 endpoint = "${globals.net.vpn.endpoint}";
152 endpointPublicKey = "${globals.machines.kerkouane.net.vpn.pubkey}";
153 };
154
155 audible-sync = {
156 enable = true; # enable one migration dayrs
157 user = "vincent";
158 outputDir = "/neo/audiobooks";
159 tempDir = "/neo/audiobooks/zz_import"; # Keep AAX files for reuse
160 quality = "best";
161 format = "m4b";
162 schedule = "daily"; # Run daily at 3 AM
163 notification = {
164 enable = true;
165 ntfyUrl = "https://ntfy.sbr.pm";
166 topic = "homelab";
167 tokenFile = config.age.secrets."ntfy-token".path;
168 };
169 };
170
171 beets-auto-import = {
172 enable = true;
173 user = "vincent";
174 musicDir = "/neo/music";
175 importDirs = [
176 "library"
177 "soundtrack"
178 "compilation"
179 ];
180 updatePlaylists = true;
181 schedule = "daily"; # Run daily
182 notification = {
183 enable = true;
184 ntfyUrl = "https://ntfy.sbr.pm";
185 topic = "homelab";
186 tokenFile = config.age.secrets."ntfy-token".path;
187 };
188 };
189
190 audiobookshelf = serviceDefaults // {
191 enable = true;
192 port = 13378;
193 host = "0.0.0.0";
194 };
195
196 lidarr = serviceDefaults // {
197 enable = true;
198 settings.server.port = exportarrServices.lidarr.servicePort;
199 };
200
201 rsync-replica = {
202 enable = true;
203 jobs = {
204 rhea-daily = rheaBackupDefaults // {
205 source = rheaBackupDefaults.source // {
206 paths = [
207 "/neo/documents"
208 "/neo/ebooks"
209 ];
210 };
211 schedule = "daily";
212 };
213 rhea-hourly = rheaBackupDefaults // {
214 source = rheaBackupDefaults.source // {
215 paths = [
216 "/neo/pictures"
217 "/neo/videos"
218 ];
219 };
220 schedule = "hourly";
221 };
222 };
223 };
224
225 # Restic backup to aix (off-site backup) - daily for org and documents
226 restic.backups.aix-daily = {
227 user = "vincent";
228 repository = "sftp:vincent@aix.sbr.pm:/data/backup/restic/aion";
229 passwordFile = config.age.secrets."restic-aix-password".path;
230
231 paths = [
232 "/home/vincent/desktop/org" # Org files (<1GB)
233 "/neo/documents" # Personal docs rsynced from rhea (~113GB)
234 ];
235
236 # Daily backup for frequently changing org files
237 timerConfig = {
238 OnCalendar = "daily";
239 Persistent = true;
240 RandomizedDelaySec = "30m";
241 };
242
243 pruneOpts = [
244 "--keep-daily 14" # Last 2 weeks
245 "--keep-weekly 8" # Last 2 months
246 "--keep-monthly 12" # Last year
247 ];
248
249 extraBackupArgs = [
250 "--exclude-caches"
251 "--exclude='.sync-conflict-*'"
252 "--verbose"
253 ];
254 };
255
256 # Restic backup to aix (off-site backup) - weekly for large datasets
257 # Note: Photos are already rsync'd to aix daily via aix's pull job
258 restic.backups.aix-critical = {
259 user = "vincent";
260 repository = "sftp:vincent@aix.sbr.pm:/data/backup/restic/aion";
261
262 # Use password-based encryption
263 passwordFile = config.age.secrets."restic-aix-password".path;
264
265 paths = [
266 "/neo/pictures/photos/backups" # Immich database dumps only (~100MB, versioned)
267 "/var/lib/lidarr" # Lidarr database and config (~4.6GB)
268 "/var/lib/audiobookshelf" # Audiobookshelf database and config (~30MB)
269 ];
270
271 # Backup schedule - weekly for large dataset
272 timerConfig = {
273 OnCalendar = "weekly";
274 Persistent = true;
275 RandomizedDelaySec = "1h"; # Avoid VPN congestion
276 };
277
278 # Retention policy
279 pruneOpts = [
280 "--keep-daily 7" # Last 7 days
281 "--keep-weekly 4" # Last 4 weeks
282 "--keep-monthly 12" # Last 12 months
283 "--keep-yearly 3" # Last 3 years
284 ];
285
286 # Backup options
287 extraBackupArgs = [
288 "--exclude-caches"
289 "--exclude='*.Trash-*'"
290 "--exclude='lost+found'"
291 "--exclude='.sync-conflict-*'" # Syncthing conflicts
292 "--verbose"
293 ];
294
295 # Check repository integrity after backup
296 checkOpts = [
297 "--read-data-subset=5%" # Verify 5% of data each run
298 ];
299
300 # Backup monitoring with ntfy.sh
301 backupPrepareCommand = ''
302 ${pkgs.curl}/bin/curl \
303 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
304 config.age.secrets."ntfy-token".path
305 })" \
306 -H "Title: Restic Backup Starting (aion)" \
307 -d "Starting backup to aix (critical data only)" \
308 https://ntfy.sbr.pm/backups
309 '';
310
311 backupCleanupCommand = ''
312 ${pkgs.curl}/bin/curl \
313 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
314 config.age.secrets."ntfy-token".path
315 })" \
316 -H "Title: Restic Backup Complete (aion)" \
317 -H "Tags: white_check_mark" \
318 -d "Backup to aix completed successfully" \
319 https://ntfy.sbr.pm/backups || \
320 ${pkgs.curl}/bin/curl \
321 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
322 config.age.secrets."ntfy-token".path
323 })" \
324 -H "Title: Restic Backup Failed (aion)" \
325 -H "Tags: x,warning" \
326 -H "Priority: high" \
327 -d "Backup to aix failed! Check logs: journalctl -u restic-backups-aix-critical.service" \
328 https://ntfy.sbr.pm/backups
329 '';
330 };
331
332 # Prometheus exporter for restic backup monitoring
333 # DISABLED: Causes excessive load (restic check every 60s over SFTP)
334 # TODO: Re-enable with local repository or periodic timer-based checks
335 prometheus.exporters.restic = {
336 enable = false;
337 port = 9753;
338 user = "vincent"; # Must run as vincent to access SSH keys for aix
339 group = "users";
340 repository = "sftp:vincent@aix.sbr.pm:/data/backup/restic/aion";
341 passwordFile = config.age.secrets."restic-aix-password".path;
342 };
343
344 music-playlist-dl = {
345 enable = true; # Enable on music migration day
346 user = "vincent";
347 configFile = "/neo/music/music-playlist-dl.yaml";
348 baseDir = "/neo/music/mixes"; # Downloads to /neo/music/mixes/{show}, playlists to /neo/music/playlists
349 schedule = "weekly"; # Run weekly on Sundays at 2 AM
350 notification = {
351 enable = true;
352 ntfyUrl = "https://ntfy.sbr.pm";
353 topic = "homelab";
354 tokenFile = config.age.secrets."ntfy-token".path;
355 };
356 };
357
358 # XMPP Research Bot (disabled until secrets are created)
359 xmpp-research-bot = {
360 enable = false; # TODO: Enable after creating secrets with agenix
361 # jid = "researchbot@xmpp.sbr.pm";
362 # ownerJid = "vincent@xmpp.sbr.pm";
363 # passwordFile = config.age.secrets."xmpp-research-bot-password".path;
364 # apiKeyFile = config.age.secrets."anthropic-api-key".path;
365 # inboxPath = "/home/vincent/desktop/org/inbox.org";
366 # user = "vincent";
367 # group = "users";
368 };
369
370 navidrome = {
371 enable = true;
372 settings = {
373 MusicFolder = "/neo/music";
374 Address = "0.0.0.0";
375 Port = 4533;
376 BaseURL = "https://music.sbr.pm";
377
378 # Paths
379 DataFolder = "/var/lib/navidrome";
380 CacheFolder = "/var/cache/navidrome";
381
382 # Features
383 EnableTranscodingConfig = false; # Disabled for security - transcoding still works, UI editing disabled
384 EnableSubsonic = true;
385
386 # Scanner settings
387 Scanner.PurgeMissing = "full"; # Auto-cleanup missing files after full scans
388
389 # Optional: Scrobbling (can enable later)
390 # LastFM.Enabled = true;
391 };
392 };
393
394 transmission = serviceDefaults // {
395 enable = true; # Enable on music migration day
396 package = pkgs.transmission_4;
397 openRPCPort = true; # Open firewall for RPC (port 9091)
398 home = "/neo/torrents";
399 settings = {
400 # Override default settings
401 incomplete-dir-enabled = true;
402 rpc-bind-address = "0.0.0.0"; # Bind to all interfaces
403 rpc-host-whitelist = "localhost,tm.sbr.pm,transmission-music.sbr.pm,aion.home,aion.vpn,aion.sbr.pm,192.168.1.51,10.100.0.51";
404 rpc-host-whitelist-enabled = true;
405 rpc-whitelist-enabled = true;
406 rpc-whitelist = "127.0.0.1,192.168.1.*,10.100.0.*"; # Allow local network access
407 rpc-username = "transmission";
408 rpc-password = "transmission";
409 download-queue-enabled = true;
410 download-queue-size = 15;
411 queue-stalled-enabled = true;
412 queue-stalled-minutes = 30;
413 ratio-limit = 0.1;
414 ratio-limit-enabled = true;
415 };
416 };
417
418 # Samba shares for music and audiobooks
419 samba.settings = {
420 global."server string" = "Aion";
421 music = libx.mkSambaShare {
422 name = "music";
423 path = "/neo/music";
424 };
425 audiobooks = libx.mkSambaShare {
426 name = "audiobooks";
427 path = "/neo/audiobooks";
428 };
429 };
430
431 # NFS server for music and audiobooks
432 nfs.server = {
433 enable = true;
434 # Fixed ports for firewall configuration
435 lockdPort = 4001;
436 mountdPort = 4002;
437 statdPort = 4000;
438 exports = ''
439 /neo/music 192.168.1.0/24(rw,fsid=0,no_subtree_check) 10.100.0.0/24(rw,fsid=0,no_subtree_check)
440 /neo/audiobooks 192.168.1.0/24(rw,fsid=1,no_subtree_check) 10.100.0.0/24(rw,fsid=1,no_subtree_check)
441 '';
442 };
443 };
444
445 # Override prometheus-restic-exporter service to disable DynamicUser
446 # This is needed so the service runs as vincent and can access SSH keys
447 # DISABLED: Service is currently disabled due to excessive load
448 # systemd.services.prometheus-restic-exporter.serviceConfig = {
449 # DynamicUser = lib.mkForce false;
450 # User = lib.mkForce "vincent";
451 # Group = lib.mkForce "users";
452 # ProtectHome = lib.mkForce false; # Disable home protection to allow SSH control sockets
453 # RestrictAddressFamilies = lib.mkForce [
454 # "AF_UNIX"
455 # "AF_INET"
456 # "AF_INET6"
457 # ]; # Allow all network families for SSH
458 # };
459
460 networking = {
461 useDHCP = lib.mkDefault true;
462 firewall = {
463 allowedTCPPorts = [
464 3001 # Homepage dashboard
465 4533 # Navidrome
466 13378 # Audiobookshelf
467 8686 # Lidarr
468 9000 # Node exporter
469 9709 # Lidarr exportarr (prometheus)
470 # 9753 # Restic exporter (prometheus) - DISABLED
471 9091 # Transmission (music torrents)
472 # NFS ports
473 111 # rpcbind
474 2049 # NFS daemon
475 4000 # statd
476 4001 # lockd
477 4002 # mountd
478 20048 # mountd (NFSv4)
479 ];
480 allowedUDPPorts = [
481 # NFS ports
482 111 # rpcbind
483 2049 # NFS daemon
484 4000 # statd
485 4001 # lockd
486 4002 # mountd
487 20048 # mountd (NFSv4)
488 ];
489 };
490 };
491
492 environment.systemPackages = with pkgs; [
493 lm_sensors
494 gnumake
495 audible-converter
496 audible-cli
497 ffmpeg-full
498 ];
499
500}