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}