Commit 01c9557e2650

Vincent Demeester <vincent@sbr.pm>
2026-01-29 10:45:34
feat(aion): add beets-auto-import service for scheduled music import
Add NixOS module that runs daily: - beet import -q on library/, soundtrack/, compilation/ directories - beet splupdate to regenerate smart playlists - ntfy notifications on success/failure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ce5f04c
Changed files (2)
modules
beets-auto-import
systems
modules/beets-auto-import/default.nix
@@ -0,0 +1,240 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.beets-auto-import;
+
+  # Convert schedule shortcuts to systemd OnCalendar format
+  scheduleToCalendar =
+    schedule:
+    if schedule == "hourly" then
+      "hourly"
+    else if schedule == "daily" then
+      "daily"
+    else if schedule == "weekly" then
+      "weekly"
+    else if schedule == "monthly" then
+      "monthly"
+    else
+      schedule;
+in
+{
+  options.services.beets-auto-import = {
+    enable = mkEnableOption "Beets auto-import and playlist update service";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.beets;
+      defaultText = literalExpression "pkgs.beets";
+      description = "Beets package to use (can include custom plugins)";
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "vincent";
+      description = "User to run the beets service as";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "users";
+      description = "Group to run the beets service as";
+    };
+
+    musicDir = mkOption {
+      type = types.str;
+      default = "/neo/music";
+      description = "Base music directory";
+    };
+
+    importDirs = mkOption {
+      type = types.listOf types.str;
+      default = [
+        "library"
+        "soundtrack"
+        "compilation"
+      ];
+      description = "Subdirectories under musicDir to import (relative paths)";
+    };
+
+    updatePlaylists = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Run beet splupdate after import to regenerate smart playlists";
+    };
+
+    schedule = mkOption {
+      type = types.str;
+      default = "daily";
+      description = ''
+        When to run the import. Can be "hourly", "daily", "weekly", "monthly", or a systemd OnCalendar format.
+        See systemd.time(7) for OnCalendar format details.
+      '';
+      example = "weekly";
+    };
+
+    notification = {
+      enable = mkEnableOption "notifications via ntfy";
+
+      ntfyUrl = mkOption {
+        type = types.str;
+        default = "https://ntfy.sbr.pm";
+        description = "URL of ntfy server";
+      };
+
+      topic = mkOption {
+        type = types.str;
+        default = "homelab";
+        description = "ntfy topic for notifications";
+      };
+
+      tokenFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Path to file containing ntfy authentication token (optional)";
+      };
+    };
+  };
+
+  config =
+    let
+      # Build the import paths
+      importPaths = map (dir: "${cfg.musicDir}/${dir}") cfg.importDirs;
+
+      # Build the beet import command
+      beetsImportScript = pkgs.writeShellScript "beets-auto-import-run" ''
+        set -euo pipefail
+
+        echo "Starting beets auto-import..."
+        echo "Import directories: ${concatStringsSep " " importPaths}"
+
+        # Import each directory (skip if empty, quiet mode, non-interactive)
+        for dir in ${concatStringsSep " " importPaths}; do
+          if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then
+            echo "Importing: $dir"
+            ${cfg.package}/bin/beet import -q "$dir" || true
+          else
+            echo "Skipping empty or missing directory: $dir"
+          fi
+        done
+
+        ${optionalString cfg.updatePlaylists ''
+          echo "Updating smart playlists..."
+          ${cfg.package}/bin/beet splupdate
+        ''}
+
+        echo "Beets auto-import complete!"
+      '';
+    in
+    mkIf cfg.enable {
+      # Systemd timer for scheduled import
+      systemd.timers.beets-auto-import = {
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnCalendar = scheduleToCalendar cfg.schedule;
+          Persistent = true;
+          RandomizedDelaySec = "15m";
+        };
+      };
+
+      # Systemd service to run beets
+      systemd.services.beets-auto-import = {
+        description = "Auto-import music to beets and update playlists";
+        after = [ "network-online.target" ];
+        wants = [ "network-online.target" ];
+
+        environment = {
+          HOME = "/home/${cfg.user}";
+        };
+
+        serviceConfig = {
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+
+          # Run the import script
+          ExecStart = beetsImportScript;
+
+          # Notifications on success (if enabled)
+          ExecStartPost = mkIf cfg.notification.enable (
+            pkgs.writeShellScript "beets-auto-import-notify-success" ''
+              ${
+                if cfg.notification.tokenFile != null then
+                  ''
+                    ${pkgs.curl}/bin/curl \
+                      -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.notification.tokenFile})" \
+                      -H "Title: Beets Import Complete" \
+                      -H "Tags: musical_note,white_check_mark" \
+                      -d "Successfully imported music and updated playlists" \
+                      ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
+                  ''
+                else
+                  ''
+                    ${pkgs.curl}/bin/curl \
+                      -H "Title: Beets Import Complete" \
+                      -H "Tags: musical_note,white_check_mark" \
+                      -d "Successfully imported music and updated playlists" \
+                      ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
+                  ''
+              }
+            ''
+          );
+
+          # Resource limits (low priority)
+          Nice = 15;
+          IOSchedulingClass = "idle";
+          CPUSchedulingPolicy = "idle";
+
+          # Security hardening
+          PrivateTmp = true;
+          NoNewPrivileges = true;
+          ProtectSystem = "strict";
+          ProtectHome = "read-only";
+          ReadWritePaths = [
+            cfg.musicDir
+            "/home/${cfg.user}/.config/beets" # beets config/db
+          ];
+        };
+
+        # Notify on failure (if enabled)
+        onFailure = mkIf cfg.notification.enable [ "beets-auto-import-failure.service" ];
+      };
+
+      # Failure notification service
+      systemd.services.beets-auto-import-failure = mkIf cfg.notification.enable {
+        description = "Notify on beets auto-import failure";
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = pkgs.writeShellScript "beets-auto-import-notify-failure" ''
+            ${
+              if cfg.notification.tokenFile != null then
+                ''
+                  ${pkgs.curl}/bin/curl \
+                    -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.notification.tokenFile})" \
+                    -H "Title: Beets Import Failed" \
+                    -H "Priority: high" \
+                    -H "Tags: warning,musical_note" \
+                    -d "Beets auto-import failed. Check logs: journalctl -u beets-auto-import" \
+                    ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
+                ''
+              else
+                ''
+                  ${pkgs.curl}/bin/curl \
+                    -H "Title: Beets Import Failed" \
+                    -H "Priority: high" \
+                    -H "Tags: warning,musical_note" \
+                    -d "Beets auto-import failed. Check logs: journalctl -u beets-auto-import" \
+                    ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
+                ''
+            }
+          '';
+        };
+      };
+    };
+}
systems/aion/extra.nix
@@ -43,6 +43,7 @@ in
     ../common/services/homepage.nix
     ../common/services/prometheus-exporters-node.nix
     ../../modules/audible-sync
+    ../../modules/beets-auto-import
     ../../modules/music-playlist-dl
     ../../modules/harmonia
     ../../modules/xmpp-research-bot
@@ -154,6 +155,25 @@ in
       };
     };
 
+    beets-auto-import = {
+      enable = true;
+      user = "vincent";
+      musicDir = "/neo/music";
+      importDirs = [
+        "library"
+        "soundtrack"
+        "compilation"
+      ];
+      updatePlaylists = true;
+      schedule = "daily"; # Run daily
+      notification = {
+        enable = true;
+        ntfyUrl = "https://ntfy.sbr.pm";
+        topic = "homelab";
+        tokenFile = config.age.secrets."ntfy-token".path;
+      };
+    };
+
     audiobookshelf = serviceDefaults // {
       enable = true;
       port = 13378;