#!/usr/bin/env bash TARGET_BITRATE="192k" ROOT_DIR="." DRY_RUN=0 CLEANUP_BACKUPS=0 GREEN="\033[1;32m" BLUE="\033[1;34m" YELLOW="\033[1;33m" RED="\033[1;31m" DIM="\033[2m" RESET="\033[0m" usage() { echo -e "${BLUE}Usage:${RESET} $0 [-n] [-d DIR] [-c]" echo -e " -n Dry-run (only show files that would be converted)" echo -e " -d DIR Working directory (default: current directory)" echo -e " -c Remove .bak files after successful run" echo -e " -h Show this help" } log_step() { echo -e "\n${BLUE}▶${RESET} $*"; } log_info() { echo -e "${BLUE}ℹ${RESET} $*"; } log_warn() { echo -e "${YELLOW}⚠${RESET} $*"; } log_error() { echo -e "${RED}✖${RESET} $*"; } log_success() { echo -e "${GREEN}✔${RESET} $*"; } get_total_size_mb() { local dir="$1" find "$dir" -name "*.mp3" -print0 | xargs -0 stat -c '%s' 2>/dev/null | \ awk '{sum += $1} END {printf "%.2f MB\n", sum/1024/1024}' } get_bitrate_kbps() { local file="$1" local metadata metadata=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate \ -of default=noprint_wrappers=1:nokey=1 "$file" < /dev/null 2>/dev/null) if ! [[ "$metadata" =~ ^[0-9]+$ ]]; then metadata=$(ffprobe -v error -show_entries format=bit_rate \ -of default=noprint_wrappers=1:nokey=1 "$file" < /dev/null 2>/dev/null) fi if [[ "$metadata" =~ ^[0-9]+$ ]]; then echo $((metadata / 1024)) return 0 fi return 1 } restore_metadata() { local source_file="$1" local target_file="$2" local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Use Python script with mutagen to properly copy all ID3 tags including multi-value fields "$script_dir/copy_id3_tags.py" "$source_file" "$target_file" 2>/dev/null return $? } process_file() { local file="$1" local current_bitrate local backup_file local temp_file current_bitrate=$(get_bitrate_kbps "$file") || { log_warn "Could not read bitrate for: ${file}" ((warn_count++)) return 0 } if [ "$current_bitrate" -le "${TARGET_BITRATE%k}" ]; then log_info "Skip (already <= ${TARGET_BITRATE}): ${file}" ((skip_count++)) return 0 fi if [ "$DRY_RUN" -eq 1 ]; then log_info "Would convert (${current_bitrate} kbps ➜ ${TARGET_BITRATE}): ${file}" ((dry_run_count++)) return 0 fi backup_file="${file%.mp3}.bak" if [ ! -f "$backup_file" ]; then cp "$file" "$backup_file" log_success "Backup created: ${backup_file}" fi temp_file="${file%.mp3}_temp_$$.mp3" if ffmpeg -nostdin -y -i "$file" -b:a "${TARGET_BITRATE}" -id3v2_version 3 "$temp_file" > /dev/null 2>&1; then mv "$temp_file" "$file" # Restore all metadata from backup file if restore_metadata "$backup_file" "$file"; then log_success "Converted: ${file} (${current_bitrate} kbps ➜ ${TARGET_BITRATE}) ✅" ((converted_count++)) else log_error "Metadata restoration failed: ${file}" # Revert to backup since metadata restoration failed cp "$backup_file" "$file" log_warn "Reverted to backup (conversion skipped due to metadata error)" ((error_count++)) fi else rm -f "$temp_file" log_error "ffmpeg failed: ${file}" ((error_count++)) fi } # ==================== MAIN ==================== while getopts ":nd:ch" opt; do case "$opt" in n) DRY_RUN=1 ;; d) ROOT_DIR="$OPTARG" ;; c) CLEANUP_BACKUPS=1 ;; h) usage exit 0 ;; *) usage exit 1 ;; esac done if ! command -v ffprobe >/dev/null 2>&1 || ! command -v ffmpeg >/dev/null 2>&1; then log_error "ffprobe/ffmpeg not found. Please install ffmpeg." exit 1 fi if ! python3 -c "import mutagen" 2>/dev/null; then log_error "Python mutagen library not found. Please install python-mutagen (Arch) or python3-mutagen (Debian/Ubuntu)." exit 1 fi if [ ! -d "$ROOT_DIR" ]; then log_error "Directory not found: ${ROOT_DIR}" exit 1 fi log_step "Starting Bitrate Reduction" log_info "Target bitrate: ${TARGET_BITRATE}" log_info "Working directory: ${ROOT_DIR}" if [ "$DRY_RUN" -eq 1 ]; then log_info "Dry-run: enabled" fi if [ "$CLEANUP_BACKUPS" -eq 1 ]; then log_info "Cleanup backups: enabled" fi total_before=$(get_total_size_mb "$ROOT_DIR") log_info "Total MP3 size before: ${total_before:-0.00 MB}" converted_count=0 skip_count=0 warn_count=0 error_count=0 dry_run_count=0 file_count=0 log_step "Scanning for MP3 files..." while IFS= read -r -d '' file; do ((file_count++)) process_file "$file" done < <(find "$ROOT_DIR" -type f -name "*.mp3" -print0) if [ "$file_count" -eq 0 ]; then log_warn "No MP3 files found in ${ROOT_DIR}" fi total_after=$(get_total_size_mb "$ROOT_DIR") log_info "Total MP3 size after: ${total_after:-0.00 MB}" log_step "Summary" log_info "Files scanned: ${file_count}" log_info "Converted: ${converted_count}" log_info "Skipped: ${skip_count}" log_info "Dry-run matches: ${dry_run_count}" log_info "Warnings: ${warn_count}" if [ "$error_count" -gt 0 ]; then log_error "Errors: ${error_count}" else log_success "Errors: 0" fi if [ "$CLEANUP_BACKUPS" -eq 1 ] && [ "$DRY_RUN" -eq 0 ] && [ "$error_count" -eq 0 ]; then log_step "Cleaning up backups" find "$ROOT_DIR" -name "*.bak" -type f -delete log_success "All .bak files removed" elif [ "$CLEANUP_BACKUPS" -eq 1 ] && [ "$DRY_RUN" -eq 1 ]; then log_warn "Cleanup skipped in dry-run mode" elif [ "$CLEANUP_BACKUPS" -eq 1 ] && [ "$error_count" -gt 0 ]; then log_warn "Cleanup skipped due to errors" fi log_step "Done 🎉"