#!/bin/bash # sync-music — manually sync ~/Music into the beets library on the Unraid NAS. # # sync-music # shell alias -> ~/.local/bin/sync-music # # How it works: # 1. Ensures the NAS SMB share is mounted at /Volumes/data (uses your keychain # credentials; falls back to a clear error if it can't mount). # 2. Imports every audio file in ~/Music into beets via # `beet import --noautotag --move --quiet`, which moves each file into the # library and removes it from ~/Music. Files already in the library are # skipped by beets (left in place). Safe to re-run any time. # # Why a manual script (not the old daemon)? The daemon kept dying because # launchd/WezTerm spawn a minimal PATH that lacks /opt/homebrew/bin (where # `beet` lives). Running this script yourself in your own shell avoids that. # # Configure your server below. NAS_HOST="unraid.local" NAS_SHARE="data" NAS_USER="backup" MOUNT_POINT="/Volumes/data" LIBRARY_DIR="$MOUNT_POINT/media/music" WATCH_DIR="$HOME/Music" LOG_FILE="$HOME/Library/Logs/beet-watch.log" AUDIO_EXT="flac|mp3|m4a|aac|wav|alac|ogg|wma|opus|aiff|aif" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; } # --- locate beet — works whether it comes from nix profile or Homebrew --- BEET="$(command -v beet 2>/dev/null || true)" if [ ! -x "$BEET" ]; then echo "ERROR: beet not found." >&2 exit 1 fi # --- 1. ensure the NAS is mounted --- if [ ! -d "$MOUNT_POINT" ]; then log "NAS not mounted; mounting smb://$NAS_USER@$NAS_HOST/$NAS_SHARE ..." open "smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" 2>/dev/null || true for _ in $(seq 1 30); do [ -d "$MOUNT_POINT" ] && break sleep 1 done fi if [ ! -d "$MOUNT_POINT" ]; then echo "ERROR: $MOUNT_POINT is not mounted." >&2 echo " Mount it, then re-run:" >&2 echo " open smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" >&2 exit 1 fi # --- 2. import every (finished) audio file in ~/Music --- log "sync-music: scanning $WATCH_DIR -> $LIBRARY_DIR" shopt -s nullglob nocaseglob files=() for f in "$WATCH_DIR"/*; do [ -f "$f" ] || continue [[ "${f##*.}" =~ ^($AUDIO_EXT)$ ]] && files+=("$f") done shopt -u nocaseglob nullglob total=${#files[@]} log "Found $total audio file(s)." ok=0; skipped=0; failed=0 for f in "${files[@]}"; do # skip files still being written (size must be stable for 1s) s1=$(stat -f%z "$f" 2>/dev/null || echo 0) sleep 1 s2=$(stat -f%z "$f" 2>/dev/null || echo 0) if [ "$s1" != "$s2" ]; then log " still writing, skipping: $(basename "$f")" skipped=$((skipped + 1)) continue fi bn=$(basename "$f") log " importing: $bn" # stdin from /dev/null so beets never blocks waiting for a prompt out=$("$BEET" import --noautotag --move --quiet "$f" &1) rc=$? if [ $rc -eq 0 ]; then if [ -f "$f" ]; then log " already in library (skipped, left in place): $bn" else log " moved into library: $bn" fi ok=$((ok + 1)) else log " FAILED (rc=$rc): $bn :: $(printf '%s' "$out" | head -c 200)" failed=$((failed + 1)) fi done log "sync-music complete: $total considered, ok=$ok, still-writing-skipped=$skipped, failed=$failed" [ "$failed" -gt 0 ] && log " some imports failed; fix and re-run." exit 0