Commit d393cc333be5

Vincent Demeester <vincent@sbr.pm>
2025-12-15 10:56:58
feat(homelab): Add automated Audible to Audiobookshelf sync
- Enable daily scheduled sync to eliminate manual audiobook conversion - Preserve downloaded AAX files between runs to save bandwidth and time - Provide ntfy notifications for sync success/failure visibility Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 4f82343
Changed files (4)
modules
pkgs
audible-converter
systems
modules/audible-sync.nix
@@ -0,0 +1,182 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.audible-sync;
+in
+{
+  options.services.audible-sync = {
+    enable = mkEnableOption "Audible to Audiobookshelf sync service";
+
+    user = mkOption {
+      type = types.str;
+      default = "vincent";
+      description = "User to run the sync service as";
+    };
+
+    outputDir = mkOption {
+      type = types.str;
+      default = "/neo/audiobooks";
+      description = "Output directory for converted audiobooks";
+    };
+
+    tempDir = mkOption {
+      type = types.str;
+      default = "/neo/audiobooks/zz_import";
+      description = "Temporary directory for downloads";
+    };
+
+    quality = mkOption {
+      type = types.enum [
+        "best"
+        "high"
+        "normal"
+      ];
+      default = "best";
+      description = "Audio quality for downloads";
+    };
+
+    format = mkOption {
+      type = types.enum [
+        "m4b"
+        "mp3"
+        "m4a"
+      ];
+      default = "m4b";
+      description = "Output format for converted audiobooks";
+    };
+
+    schedule = mkOption {
+      type = types.str;
+      default = "daily";
+      description = "Systemd timer schedule (daily, weekly, etc.)";
+    };
+
+    time = mkOption {
+      type = types.str;
+      default = "03:00";
+      description = "Time of day to run sync (24-hour format)";
+    };
+
+    onCalendar = mkOption {
+      type = types.str;
+      default = "*-*-* 03:00:00";
+      description = "Full OnCalendar specification for systemd timer";
+    };
+
+    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 = "audible-sync";
+        description = "ntfy topic for notifications";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Install the converter tool
+    environment.systemPackages = with pkgs; [
+      audible-converter
+    ];
+
+    # Systemd timer for scheduled sync
+    systemd.timers.audible-sync = {
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = cfg.onCalendar;
+        Persistent = true;
+        RandomizedDelaySec = "10m";
+      };
+    };
+
+    # Systemd service to run the sync
+    systemd.services.audible-sync = {
+      description = "Sync Audible library to Audiobookshelf";
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+
+      environment = {
+        AUDIBLE_OUTPUT_DIR = cfg.outputDir;
+        AUDIBLE_TEMP_DIR = cfg.tempDir;
+        AUDIBLE_QUALITY = cfg.quality;
+        AUDIBLE_FORMAT = cfg.format;
+        HOME = "/home/${cfg.user}";
+      };
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = "users";
+
+        # Run the sync command
+        ExecStart = "${pkgs.audible-converter}/bin/audible-converter sync";
+
+        # Notifications on success (if enabled)
+        ExecStartPost = mkIf cfg.notification.enable (
+          pkgs.writeShellScript "audible-sync-notify-success" ''
+            ${pkgs.curl}/bin/curl -H "Title: Audible Sync Complete" \
+              -H "Tags: white_check_mark,books" \
+              -d "Successfully synced Audible library to ${cfg.outputDir}" \
+              ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
+          ''
+        );
+
+        # Ensure directories exist
+        ExecStartPre = pkgs.writeShellScript "audible-sync-prepare" ''
+          mkdir -p "${cfg.outputDir}"
+          mkdir -p "${cfg.tempDir}"
+        '';
+
+        # Note: We keep tempDir intact to reuse downloaded AAX files
+        # and reduce bandwidth usage on subsequent runs
+
+        # Resource limits
+        Nice = 15;
+        IOSchedulingClass = "idle";
+        CPUSchedulingPolicy = "idle";
+
+        # Security hardening
+        PrivateTmp = true;
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ReadWritePaths = [
+          cfg.outputDir
+          cfg.tempDir
+        ];
+      };
+
+      # Notify on failure (if enabled)
+      onFailure = mkIf cfg.notification.enable [ "audible-sync-failure.service" ];
+    };
+
+    # Failure notification service
+    systemd.services.audible-sync-failure = mkIf cfg.notification.enable {
+      description = "Notify on Audible sync failure";
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = pkgs.writeShellScript "audible-sync-notify-failure" ''
+          ${pkgs.curl}/bin/curl -H "Title: Audible Sync Failed" \
+            -H "Priority: high" \
+            -H "Tags: warning,books" \
+            -d "Audible sync failed. Check logs: journalctl -u audible-sync" \
+            ${cfg.notification.ntfyUrl}/${cfg.notification.topic}
+        '';
+      };
+    };
+  };
+}
pkgs/audible-converter/convert.sh
@@ -10,6 +10,7 @@ OUTPUT_DIR="${AUDIBLE_OUTPUT_DIR:-$HOME/audiobooks}"
 QUALITY="${AUDIBLE_QUALITY:-best}"
 FORMAT="${AUDIBLE_FORMAT:-m4b}"
 AUTHCODE="${AUDIBLE_AUTHCODE:-}"
+CLEANUP_ON_EXIT="${AUDIBLE_CLEANUP_ON_EXIT:-false}"  # Set to 'true' to auto-cleanup temp files
 
 # Colors for output
 RED='\033[0;31m'
@@ -53,11 +54,12 @@ OPTIONS:
     -h, --help           Show this help message
 
 ENVIRONMENT VARIABLES:
-    AUDIBLE_OUTPUT_DIR   Output directory for converted books
-    AUDIBLE_TEMP_DIR     Temporary directory for downloads
-    AUDIBLE_QUALITY      Audio quality setting
-    AUDIBLE_FORMAT       Output format
-    AUDIBLE_AUTHCODE     Activation bytes for AAX DRM removal
+    AUDIBLE_OUTPUT_DIR      Output directory for converted books
+    AUDIBLE_TEMP_DIR        Temporary directory for downloads (kept by default)
+    AUDIBLE_QUALITY         Audio quality setting
+    AUDIBLE_FORMAT          Output format
+    AUDIBLE_AUTHCODE        Activation bytes for AAX DRM removal
+    AUDIBLE_CLEANUP_ON_EXIT Set to 'true' to auto-delete temp files on exit
 
 EXAMPLES:
     # Sync library (download and convert new books)
@@ -108,11 +110,13 @@ setup_dirs() {
     mkdir -p "$OUTPUT_DIR"
 }
 
-# Cleanup temporary files
+# Cleanup temporary files (manual or via AUDIBLE_CLEANUP_ON_EXIT=true)
+# By default, we keep temp files to reuse downloaded AAX files on subsequent runs
 cleanup() {
     if [ -d "$TEMP_DIR" ]; then
-        log_info "Cleaning up temporary files..."
+        log_info "Cleaning up temporary files in: $TEMP_DIR"
         rm -rf "$TEMP_DIR"
+        log_info "Cleanup complete"
     fi
 }
 
@@ -121,7 +125,7 @@ download_all() {
     log_info "Exporting library metadata..."
     local library_json="$TEMP_DIR/library.json"
 
-    audible library export --output "$library_json"
+    audible library export --format json --output "$library_json"
 
     local total_books
     total_books=$(jq length "$library_json")
@@ -347,8 +351,13 @@ main() {
         esac
     done
 
-    # Trap cleanup on exit
-    trap cleanup EXIT
+    # Optionally cleanup on exit (default: keep files for reuse)
+    if [ "$CLEANUP_ON_EXIT" = "true" ]; then
+        log_info "Auto-cleanup enabled (AUDIBLE_CLEANUP_ON_EXIT=true)"
+        trap cleanup EXIT
+    else
+        log_info "Keeping temp files in $TEMP_DIR for reuse (set AUDIBLE_CLEANUP_ON_EXIT=true to auto-delete)"
+    fi
 
     # Check dependencies
     check_dependencies
pkgs/audible-converter/README.md
@@ -107,9 +107,10 @@ audible-converter --output ~/audiobooks convert book.aax
 Configure defaults using environment variables:
 
 - `AUDIBLE_OUTPUT_DIR` - Output directory for converted books
-- `AUDIBLE_TEMP_DIR` - Temporary directory for downloads
+- `AUDIBLE_TEMP_DIR` - Temporary directory for downloads (kept by default for reuse)
 - `AUDIBLE_QUALITY` - Audio quality setting (best, high, normal)
 - `AUDIBLE_FORMAT` - Output format (m4b, mp3, m4a)
+- `AUDIBLE_CLEANUP_ON_EXIT` - Set to `true` to auto-delete temp files on exit (default: false)
 
 Example:
 
@@ -123,9 +124,12 @@ On rhea (with NFS-mounted storage):
 
 ```bash
 export AUDIBLE_OUTPUT_DIR="/neo/audiobooks"
+export AUDIBLE_TEMP_DIR="/neo/audiobooks/zz_import"  # Persistent for reuse
 audible-converter sync
 ```
 
+**Note**: By default, downloaded AAX files in `AUDIBLE_TEMP_DIR` are **kept** between runs to save bandwidth and time. Only new books will be downloaded on subsequent syncs. Set `AUDIBLE_CLEANUP_ON_EXIT=true` if you want to auto-delete temp files after each run.
+
 ## Output Structure
 
 Books are automatically organized for Audiobookshelf:
systems/rhea/extra.nix
@@ -10,6 +10,7 @@
   imports = [
     ../common/services/samba.nix
     ../common/services/homepage.nix
+    ../../modules/audible-sync.nix
   ];
 
   age.secrets."gandi.env" = {
@@ -469,6 +470,20 @@
       group = "users";
       openFirewall = true;
     };
+    audible-sync = {
+      enable = true;
+      user = "vincent";
+      outputDir = "/neo/audiobooks";
+      tempDir = "/neo/audiobooks/zz_import"; # Keep AAX files for reuse
+      quality = "best";
+      format = "m4b";
+      onCalendar = "*-*-* 03:00:00"; # Daily at 3 AM
+      notification = {
+        enable = true;
+        ntfyUrl = "https://ntfy.sbr.pm";
+        topic = "homelab";
+      };
+    };
     transmission = {
       enable = true;
       user = "vincent";
@@ -613,6 +628,7 @@
     lm_sensors
     gnumake
     audible-converter
+    audible-cli
   ];
 
 }