misc-scripts/music_to_usb.sh
File Type: text/x-shellscript
#!/bin/bash
# Music USB Copier with Transcoding
# Usage: ./music_to_usb.sh <USB_MOUNT> [--no-sync]
show_help() {
cat << EOF
Music USB Copier with Transcoding
Usage: $0 <USB_MOUNT> [--no-sync]
Description:
- Reads folders to copy from $MUSIC_DIR/usb_folders.txt
- Transcodes FLAC/OGG/WAV/M4A/OPUS → high-quality VBR MP3 (LAME V0)
- Builds everything in staging dir first, then rsyncs to USB
- Skips files that already exist on destination
- Optimized for USB flash drives
Options:
--no-sync Skip final sync (faster, but less safe before unplugging)
Requirements:
- ffmpeg (for transcoding)
- rsync
- usb_folders.txt in $MUSIC_DIR with one folder per line
Example:
$0 /run/media/user/MYUSB
$0 /run/media/user/MYUSB --no-sync
Staging directory: $HOME/Temp/music_usb_staging (your cleanup job will handle it)
EOF
}
if [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then
show_help
exit 0
fi
USB_MOUNT="${1:-}"
if [ -z "$USB_MOUNT" ] || [ ! -d "$USB_MOUNT" ]; then
echo "Error: Provide valid USB mount point as first argument." >&2
echo "Use --help for usage." >&2
exit 1
fi
TRANSCODE_TEMP="$(mktemp -d /tmp/music_transcode_XXXXXX)"
WORK_DIR="$HOME/Temp/music_usb_staging"
NO_SYNC=false
if [[ "${2:-}" == "--no-sync" ]]; then
NO_SYNC=true
fi
MUSIC_DIR="${MUSIC_DIR:-$HOME/Music}"
LIST_FILE="$MUSIC_DIR/usb_folders.txt"
if [ ! -f "$LIST_FILE" ]; then
echo "Error: $LIST_FILE not found." >&2
exit 1
fi
mkdir -p "$WORK_DIR"
echo "Transcoding temp: $TRANSCODE_TEMP"
echo "Staging: $WORK_DIR (persistent)"
echo "Target: $USB_MOUNT"
echo "============================="
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "ERROR: ffmpeg not found." >&2
rm -rf "$TRANSCODE_TEMP"
exit 1
fi
echo "ffmpeg version: $(ffmpeg -version | head -n1)"
START_TIME=$(date +%s)
INITIAL_USED=$(df -B1 "$USB_MOUNT" 2>/dev/null | awk 'NR==2 {print $3}' || echo 0)
failures=0
process_file() {
local file="$1"
local SRC="$2"
local WORK_DEST="$3"
local TRANSCODE_TEMP="$4"
local rel=$(realpath --relative-to="$SRC" "$file")
local dest_path="$WORK_DEST/$rel"
mkdir -p "$(dirname "$dest_path")"
local orig_ext="${file##*.}"
local mp3_dest="${dest_path%."$orig_ext"}.mp3"
if [ -f "$dest_path" ] || [ -f "$mp3_dest" ]; then
echo " Skipping: $(basename "$file")"
return
fi
local ext_lower=$(echo "$orig_ext" | tr '[:upper:]' '[:lower:]')
local is_media=false
for e in flac ogg wav m4a opus; do
[ "$ext_lower" = "$e" ] && { is_media=true; break; }
done
if $is_media; then
local base=$(basename "$file" ."$orig_ext")
local temp_mp3="$TRANSCODE_TEMP/${base}_$$.mp3"
echo " Transcoding: $(basename "$file")"
if ffmpeg -y -loglevel error -i "$file" -codec:a libmp3lame -q:a 0 "$temp_mp3" 2>&1; then
mv "$temp_mp3" "$mp3_dest"
echo " → Success"
else
echo " → FAILED to transcode $(basename "$file")" >&2
((failures++))
rm -f "$temp_mp3" 2>/dev/null || true
fi
else
echo " Copying: $(basename "$file")"
cp -a "$file" "$dest_path"
fi
}
export -f process_file
while IFS= read -r folder || [ -n "$folder" ]; do
[[ -z "$folder" || "$folder" =~ ^[[:space:]]*# ]] && continue
SRC="$MUSIC_DIR/$folder"
WORK_DEST="$WORK_DIR/$folder"
if [ ! -d "$SRC" ]; then
echo "Warning: $SRC not found — skipped" >&2
continue
fi
echo "→ Staging: $folder"
mkdir -p "$WORK_DEST"
find "$SRC" -type f -print0 | xargs -0 -I {} bash -c '
process_file "$1" "$2" "$3" "$4" || true
' _ {} "$SRC" "$WORK_DEST" "$TRANSCODE_TEMP"
done < "$LIST_FILE"
echo "============================="
if [ $failures -gt 0 ]; then
echo "❌ $failures transcode failure(s)." >&2
echo "Aborting." >&2
rm -rf "$TRANSCODE_TEMP"
exit 1
fi
echo "Staging complete. Checking space against total drive capacity..."
STAGING_SIZE=$(du -sb "$WORK_DIR" 2>/dev/null | cut -f1 || echo 0)
USB_TOTAL=$(df -B1 "$USB_MOUNT" 2>/dev/null | awk 'NR==2 {print $2}' || echo 0)
echo "Staging size: $(numfmt --to=iec "$STAGING_SIZE")"
echo "USB total capacity: $(numfmt --to=iec "$USB_TOTAL")"
if [ "$STAGING_SIZE" -gt "$USB_TOTAL" ]; then
echo "Error: Staging larger than USB capacity!" >&2
rm -rf "$TRANSCODE_TEMP"
exit 1
fi
echo "Space check passed. Starting optimized rsync..."
rsync -aH --delete-before --inplace --partial --no-whole-file --info=progress2 "$WORK_DIR/" "$USB_MOUNT/"
echo "============================="
echo "✅ Operation completed successfully!"
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
FINAL_USED=$(df -B1 "$USB_MOUNT" 2>/dev/null | awk 'NR==2 {print $3}' || echo 0)
USED=$((FINAL_USED - INITIAL_USED))
AVAIL=$(df -B1 "$USB_MOUNT" 2>/dev/null | awk 'NR==2 {print $4}' || echo 0)
echo "Total duration: ${DURATION} seconds"
echo "Space used this run: $(numfmt --to=iec "$USED" 2>/dev/null || echo "$USED bytes")"
echo "Space remaining: $(numfmt --to=iec "$AVAIL" 2>/dev/null || echo "$AVAIL bytes")"
if [ "$NO_SYNC" = false ]; then
echo "Flushing write cache..."
sync
echo "Sync complete."
fi
rm -rf "$TRANSCODE_TEMP"
echo "Staging directory left at $WORK_DIR"
echo "All done."