Commit 01c9557e2650
Changed files (2)
modules
beets-auto-import
systems
aion
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;