Files
dotfiles/home/bin/sync-music
T

103 lines
3.4 KiB
Bash
Raw Normal View History

#!/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" </dev/null 2>&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