main
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.music-playlist-dl;
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.music-playlist-dl = {
29 enable = mkEnableOption "Music playlist downloader service";
30
31 user = mkOption {
32 type = types.str;
33 default = "vincent";
34 description = "User to run the downloader service as";
35 };
36
37 group = mkOption {
38 type = types.str;
39 default = "users";
40 description = "Group to run the downloader service as";
41 };
42
43 configFile = mkOption {
44 type = types.path;
45 default = "/neo/music/music-playlist-dl.yaml";
46 description = "Path to YAML configuration file";
47 };
48
49 baseDir = mkOption {
50 type = types.str;
51 default = "/neo/music";
52 description = "Base directory for downloads (shows and playlists subdirectories)";
53 };
54
55 schedule = mkOption {
56 type = types.str;
57 default = "weekly";
58 description = ''
59 When to run the downloader. Can be "hourly", "daily", "weekly", "monthly", or a systemd OnCalendar format.
60 See systemd.time(7) for OnCalendar format details.
61 '';
62 example = "weekly";
63 };
64
65 notification = {
66 enable = mkEnableOption "notifications via ntfy";
67
68 ntfyUrl = mkOption {
69 type = types.str;
70 default = "https://ntfy.sbr.pm";
71 description = "URL of ntfy server";
72 };
73
74 topic = mkOption {
75 type = types.str;
76 default = "homelab";
77 description = "ntfy topic for notifications";
78 };
79
80 tokenFile = mkOption {
81 type = types.nullOr types.path;
82 default = null;
83 description = "Path to file containing ntfy authentication token (optional)";
84 };
85 };
86 };
87
88 config =
89 let
90 # The script writes playlists to parent_dir(baseDir)/playlists
91 # e.g., if baseDir is /neo/music/mixes, playlists go to /neo/music/playlists
92 playlistsDir = "${dirOf cfg.baseDir}/playlists";
93 in
94 mkIf cfg.enable {
95 # Install the music-playlist-dl tool
96 environment.systemPackages = with pkgs; [
97 music-playlist-dl
98 yt-dlp
99 ];
100
101 # Systemd timer for scheduled downloads
102 systemd.timers.music-playlist-dl = {
103 wantedBy = [ "timers.target" ];
104 timerConfig = {
105 OnCalendar = scheduleToCalendar cfg.schedule;
106 Persistent = true;
107 RandomizedDelaySec = "15m";
108 };
109 };
110
111 # Systemd service to run the downloader
112 systemd.services.music-playlist-dl = {
113 description = "Download music podcasts and generate playlists";
114 after = [ "network-online.target" ];
115 wants = [ "network-online.target" ];
116
117 serviceConfig = {
118 Type = "oneshot";
119 User = cfg.user;
120 Group = cfg.group;
121
122 # Run the downloader command
123 ExecStart = "${pkgs.music-playlist-dl}/bin/music-playlist-dl --config ${cfg.configFile}";
124
125 # Set cache directory to private temp (yt-dlp needs cache)
126 Environment = [ "XDG_CACHE_HOME=/tmp/cache" ];
127
128 # Notifications on success (if enabled)
129 ExecStartPost = mkIf cfg.notification.enable (
130 pkgs.writeShellScript "music-playlist-dl-notify-success" ''
131 ${
132 if cfg.notification.tokenFile != null then
133 ''
134 ${pkgs.curl}/bin/curl \
135 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.notification.tokenFile})" \
136 -H "Title: Music Playlist Download Complete" \
137 -H "Tags: musical_note,headphones" \
138 -d "Successfully downloaded music podcasts and updated playlists" \
139 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
140 ''
141 else
142 ''
143 ${pkgs.curl}/bin/curl \
144 -H "Title: Music Playlist Download Complete" \
145 -H "Tags: musical_note,headphones" \
146 -d "Successfully downloaded music podcasts and updated playlists" \
147 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
148 ''
149 }
150 ''
151 );
152
153 # Ensure base directory exists (playlists dir created by script)
154 ExecStartPre = pkgs.writeShellScript "music-playlist-dl-prepare" ''
155 mkdir -p "${cfg.baseDir}"
156 '';
157
158 # Resource limits
159 Nice = 15;
160 IOSchedulingClass = "idle";
161 CPUSchedulingPolicy = "idle";
162
163 # Security hardening
164 PrivateTmp = true;
165 NoNewPrivileges = true;
166 ProtectSystem = "strict";
167 ProtectHome = "read-only";
168 ReadWritePaths = [
169 cfg.baseDir
170 playlistsDir
171 ];
172 };
173
174 # Notify on failure (if enabled)
175 onFailure = mkIf cfg.notification.enable [ "music-playlist-dl-failure.service" ];
176 };
177
178 # Failure notification service
179 systemd.services.music-playlist-dl-failure = mkIf cfg.notification.enable {
180 description = "Notify on music playlist download failure";
181 serviceConfig = {
182 Type = "oneshot";
183 ExecStart = pkgs.writeShellScript "music-playlist-dl-notify-failure" ''
184 ${
185 if cfg.notification.tokenFile != null then
186 ''
187 ${pkgs.curl}/bin/curl \
188 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.notification.tokenFile})" \
189 -H "Title: Music Playlist Download Failed" \
190 -H "Priority: high" \
191 -H "Tags: warning,musical_note" \
192 -d "Music playlist download failed. Check logs: journalctl -u music-playlist-dl" \
193 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
194 ''
195 else
196 ''
197 ${pkgs.curl}/bin/curl \
198 -H "Title: Music Playlist Download Failed" \
199 -H "Priority: high" \
200 -H "Tags: warning,musical_note" \
201 -d "Music playlist download failed. Check logs: journalctl -u music-playlist-dl" \
202 ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
203 ''
204 }
205 '';
206 };
207 };
208 };
209}