main
  1{
  2  config,
  3  lib,
  4  pkgs,
  5  ...
  6}:
  7
  8with lib;
  9
 10let
 11  cfg = config.services.jellyfin-auto-collections;
 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;
 26
 27  # Generate complete YAML config
 28  # We'll use a template and substitute secrets at runtime
 29  configYaml = lib.generators.toYAML { } (
 30    {
 31      jellyfin = {
 32        server_url = cfg.jellyfinUrl;
 33        api_key = "JELLYFIN_API_KEY_PLACEHOLDER";
 34        user_id = cfg.userId;
 35      };
 36    }
 37    // lib.optionalAttrs (cfg.jellyseerr.enable && cfg.jellyseerr.passwordFile != null) {
 38      jellyseerr = {
 39        server_url = cfg.jellyseerr.serverUrl;
 40        inherit (cfg.jellyseerr) email;
 41        password = "JELLYSEERR_PASSWORD_PLACEHOLDER";
 42        user_type = cfg.jellyseerr.userType;
 43      };
 44    }
 45    // cfg.settings
 46  );
 47
 48  # Script to generate config file and run the application
 49  startScript = pkgs.writeShellScript "jellyfin-auto-collections-start" ''
 50        # Load API key from file if specified
 51        ${lib.optionalString (cfg.apiKeyFile != null) ''
 52          JELLYFIN_API_KEY=$(cat ${cfg.apiKeyFile})
 53        ''}
 54
 55        # Load Jellyseerr password from file if specified
 56        ${lib.optionalString (cfg.jellyseerr.enable && cfg.jellyseerr.passwordFile != null) ''
 57          JELLYSEERR_PASSWORD=$(cat ${cfg.jellyseerr.passwordFile})
 58        ''}
 59
 60        # Generate config.yml by substituting the placeholders
 61        cat > ${cfg.dataDir}/config.yml << 'EOF'
 62    ${configYaml}
 63    EOF
 64
 65        # Replace the placeholders with actual secrets
 66        sed -i "s/JELLYFIN_API_KEY_PLACEHOLDER/$JELLYFIN_API_KEY/g" ${cfg.dataDir}/config.yml
 67        ${lib.optionalString (cfg.jellyseerr.enable && cfg.jellyseerr.passwordFile != null) ''
 68          sed -i "s/JELLYSEERR_PASSWORD_PLACEHOLDER/$JELLYSEERR_PASSWORD/g" ${cfg.dataDir}/config.yml
 69        ''}
 70
 71        chmod 600 ${cfg.dataDir}/config.yml
 72
 73        # Run the main script with config file
 74        exec ${cfg.package}/bin/jellyfin-auto-collections --config ${cfg.dataDir}/config.yml
 75  '';
 76in
 77{
 78  options.services.jellyfin-auto-collections = {
 79    enable = mkEnableOption "Jellyfin Auto Collections service";
 80
 81    package = mkOption {
 82      type = types.package;
 83      default = pkgs.jellyfin-auto-collections;
 84      defaultText = literalExpression "pkgs.jellyfin-auto-collections";
 85      description = "The jellyfin-auto-collections package to use.";
 86    };
 87
 88    schedule = mkOption {
 89      type = types.str;
 90      default = "daily";
 91      description = ''
 92        When to run the collection update. Can be "hourly", "daily", "weekly", "monthly", or a systemd OnCalendar format.
 93        See systemd.time(7) for OnCalendar format details.
 94      '';
 95      example = "daily";
 96    };
 97
 98    jellyfinUrl = mkOption {
 99      type = types.str;
100      example = "http://localhost:8096";
101      description = "URL of the Jellyfin server";
102    };
103
104    apiKeyFile = mkOption {
105      type = types.nullOr types.path;
106      default = null;
107      description = ''
108        Path to a file containing the Jellyfin API key.
109        The file should contain only the API key.
110      '';
111    };
112
113    userId = mkOption {
114      type = types.str;
115      description = "Jellyfin user ID";
116    };
117
118    jellyseerr = {
119      enable = mkEnableOption "Jellyseerr integration";
120
121      serverUrl = mkOption {
122        type = types.str;
123        default = "http://localhost:5055";
124        description = "URL of the Jellyseerr server";
125      };
126
127      email = mkOption {
128        type = types.str;
129        description = "Jellyseerr user email";
130      };
131
132      passwordFile = mkOption {
133        type = types.nullOr types.path;
134        default = null;
135        description = ''
136          Path to a file containing the Jellyseerr password.
137          The file should contain only the password.
138        '';
139      };
140
141      userType = mkOption {
142        type = types.str;
143        default = "local";
144        description = "Jellyseerr user type (local or plex)";
145      };
146    };
147
148    settings = mkOption {
149      type = types.attrs;
150      default = { };
151      description = ''
152        Configuration for Jellyfin Auto Collections.
153        This will be converted to a config file.
154      '';
155      example = literalExpression ''
156        {
157          jellyfin = {
158            url = "http://localhost:8096";
159            user_id = "your-user-id";
160          };
161          lists = [
162            {
163              type = "imdb";
164              url = "https://www.imdb.com/list/ls123456789/";
165              name = "IMDb Top 250";
166            }
167          ];
168        }
169      '';
170    };
171
172    dataDir = mkOption {
173      type = types.path;
174      default = "/var/lib/jellyfin-auto-collections";
175      description = "Directory to store jellyfin-auto-collections data and cache";
176    };
177
178    user = mkOption {
179      type = types.str;
180      default = "jellyfin-auto-collections";
181      description = "User account under which jellyfin-auto-collections runs.";
182    };
183
184    group = mkOption {
185      type = types.str;
186      default = "jellyfin-auto-collections";
187      description = "Group under which jellyfin-auto-collections runs.";
188    };
189  };
190
191  config = mkIf cfg.enable {
192    # Create the user and group
193    users.users.${cfg.user} = {
194      isSystemUser = true;
195      inherit (cfg) group;
196      home = cfg.dataDir;
197      createHome = true;
198    };
199
200    users.groups.${cfg.group} = { };
201
202    # Systemd service
203    systemd.services.jellyfin-auto-collections = {
204      description = "Jellyfin Auto Collections";
205      after = [ "network.target" ];
206
207      serviceConfig = {
208        Type = "oneshot";
209        User = cfg.user;
210        Group = cfg.group;
211        WorkingDirectory = cfg.dataDir;
212
213        ExecStart = "${startScript}";
214
215        # Security hardening
216        PrivateTmp = true;
217        ProtectSystem = "strict";
218        ProtectHome = true;
219        ReadWritePaths = [ cfg.dataDir ];
220        NoNewPrivileges = true;
221        ProtectKernelTunables = true;
222        ProtectKernelModules = true;
223        ProtectControlGroups = true;
224        RestrictRealtime = true;
225        RestrictSUIDSGID = true;
226        PrivateMounts = true;
227        LockPersonality = true;
228      };
229
230      environment = {
231        JELLYFIN_SERVER_URL = cfg.jellyfinUrl;
232        JELLYFIN_USER_ID = cfg.userId;
233        HOME = cfg.dataDir;
234      };
235    };
236
237    # Systemd timer
238    systemd.timers.jellyfin-auto-collections = {
239      description = "Timer for Jellyfin Auto Collections";
240      wantedBy = [ "timers.target" ];
241
242      timerConfig = {
243        OnCalendar = scheduleToCalendar cfg.schedule;
244        Persistent = true;
245        RandomizedDelaySec = "5m";
246      };
247    };
248  };
249}