118 lines
3.1 KiB
Bash
Executable File
118 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# to_mp3.sh — Convert .m4a and .wav files to .mp3
|
|
#
|
|
# Usage:
|
|
# ./to_mp3.sh <file_or_folder> [options]
|
|
#
|
|
# Options:
|
|
# --quality N VBR quality 0-9 (default: 2 ≈ 190kbps; lower = better)
|
|
# --bitrate N CBR bitrate e.g. 192k (overrides --quality)
|
|
# --replace Delete originals after successful conversion
|
|
# --dry-run Show what would be converted without doing anything
|
|
#
|
|
# Examples:
|
|
# ./to_mp3.sh recordings/
|
|
# ./to_mp3.sh interview.m4a
|
|
# ./to_mp3.sh recordings/ --replace
|
|
# ./to_mp3.sh recordings/ --bitrate 128k
|
|
|
|
set -euo pipefail
|
|
|
|
QUALITY=2
|
|
BITRATE=""
|
|
REPLACE=false
|
|
DRY_RUN=false
|
|
TARGET=""
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--quality) QUALITY="$2"; shift 2 ;;
|
|
--bitrate) BITRATE="$2"; shift 2 ;;
|
|
--replace) REPLACE=true; shift ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
*) TARGET="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$TARGET" ]]; then
|
|
echo "Usage: $(basename "$0") <file_or_folder> [--quality N] [--bitrate N] [--replace] [--dry-run]"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v ffmpeg &>/dev/null; then
|
|
echo "Error: ffmpeg not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Collect files
|
|
files=()
|
|
if [[ -f "$TARGET" ]]; then
|
|
files=("$TARGET")
|
|
elif [[ -d "$TARGET" ]]; then
|
|
while IFS= read -r -d '' f; do
|
|
files+=("$f")
|
|
done < <(find "$TARGET" -maxdepth 1 -type f \( -iname "*.m4a" -o -iname "*.wav" \) -print0 | sort -z)
|
|
else
|
|
echo "Error: '$TARGET' is not a file or directory." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
echo "No .m4a or .wav files found in: $TARGET"
|
|
exit 0
|
|
fi
|
|
|
|
# Build audio quality flags
|
|
if [[ -n "$BITRATE" ]]; then
|
|
audio_flags=(-b:a "$BITRATE")
|
|
quality_desc="CBR ${BITRATE}"
|
|
else
|
|
audio_flags=(-q:a "$QUALITY")
|
|
quality_desc="VBR quality ${QUALITY}"
|
|
fi
|
|
|
|
echo "Converting ${#files[@]} file(s) to MP3 (${quality_desc})"
|
|
[[ "$REPLACE" == true ]] && echo " Originals will be deleted after conversion."
|
|
[[ "$DRY_RUN" == true ]] && echo " Dry-run mode — no files will be written."
|
|
echo ""
|
|
|
|
converted=0
|
|
skipped=0
|
|
errors=0
|
|
|
|
for src in "${files[@]}"; do
|
|
out="${src%.*}.mp3"
|
|
|
|
if [[ -f "$out" && "$DRY_RUN" == false ]]; then
|
|
echo " $(basename "$src"): output already exists, skipping"
|
|
((skipped++)) || true
|
|
continue
|
|
fi
|
|
|
|
size_mb=$(( $(stat -f%z "$src" 2>/dev/null || stat -c%s "$src") / 1048576 ))
|
|
printf " %-40s (%d MB)" "$(basename "$src")" "$size_mb"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo " [dry-run] → $(basename "$out")"
|
|
continue
|
|
fi
|
|
|
|
if ffmpeg -i "$src" -vn "${audio_flags[@]}" -y "$out" -loglevel error; then
|
|
out_kb=$(( $(stat -f%z "$out" 2>/dev/null || stat -c%s "$out") / 1024 ))
|
|
echo " → $(basename "$out") (${out_kb} KB)"
|
|
((converted++)) || true
|
|
if [[ "$REPLACE" == true ]]; then
|
|
rm "$src"
|
|
fi
|
|
else
|
|
echo " ERROR"
|
|
((errors++)) || true
|
|
[[ -f "$out" ]] && rm "$out"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Done: ${converted} converted, ${skipped} skipped, ${errors} errors."
|