207 lines
5.7 KiB
Bash
Executable File
207 lines
5.7 KiB
Bash
Executable File
#!/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 🎉"
|