obsolete.computer

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."

Meta