#!/usr/bin/env bash # ----------------------------------------------------------------------------- # Title: flac2m4a # Purpose: Comprehensive flac to m4a auto-converter # Version: 1.0.0 # Developer: h@x # ----------------------------------------------------------------------------- set -euo pipefail trap 'echo "✗ ERROR on line $LINENO (exit code $?): ${BASH_COMMAND}" >&2' ERR IFS=$'\n\t' # === Input & output directories === SRC_DIR="${1:-.}" DST_DIR="${2:-./m4a_output}" [[ -d "$SRC_DIR" ]] || { echo "ERROR: Source directory '$SRC_DIR' not found." >&2 exit 1 } mkdir -p "$DST_DIR" # === FFmpeg capability check === if ! ffmpeg -hide_banner -muxers 2>/dev/null | grep -qw mov; then echo "ERROR: FFmpeg build lacks the MP4/mov muxer required for .m4a outputs." >&2 exit 1 fi # === Codec selection === declare -a CODEC_OPTS if ffmpeg -hide_banner -encoders 2>/dev/null | grep -qw libfdk_aac; then CODEC_OPTS=( -c:a libfdk_aac -profile:a aac_he_v2 -cutoff 18000 -b:a 256k ) echo "INFO: Using libfdk_aac HE-AAC v2 @256 kbps." >&2 else CODEC_OPTS=( -c:a aac -b:a 256k ) echo "WARNING: Falling back to native AAC @256 kbps." >&2 fi declare -a META_OPTS=( -map_metadata 0 -id3v2_version 3 -write_id3v1 1 -metadata:s:a encoder="ffmpeg/${CODEC_OPTS[1]}" ) declare -a FFMPEG_OPTS=( -hide_banner -loglevel error -nostdin -n ) # === Discover .flac files === mapfile -d '' files < <(find "$SRC_DIR" -type f -iname '*.flac' -print0) TOTAL=${#files[@]} (( TOTAL > 0 )) || { echo "Nothing to do: no .flac files found under '$SRC_DIR'." >&2 exit 0 } echo "FOUND: $TOTAL .flac file(s) to process." >&2 # === Progress-bar initialization === BAR_WIDTH=40 COUNT=0 LAST_PCT=-1 # pre-fill bar with dots, print initial line empty_bar=$(printf '%*s' "$BAR_WIDTH" '' | tr ' ' '.') printf "Progress: [%-${BAR_WIDTH}s] 0%% (%d/%d)\n" \ "$empty_bar" 0 "$TOTAL" >&2 # === Conversion loop with ANSI overwrite === ##declare -a SKIPPED CONVERTED ##declare -A FAILED declare -a SKIPPED=() declare -a CONVERTED=() declare -A FAILED=() for SRC_FILE in "${files[@]}"; do (( COUNT += 1 )) REL_PATH="${SRC_FILE#"$SRC_DIR"/}" DST_FILE="$DST_DIR/${REL_PATH%.flac}.m4a" mkdir -p "$(dirname "$DST_FILE")" # calculate and render updated bar if % changed PCT=$(( COUNT * 100 / TOTAL )) if (( PCT != LAST_PCT )); then LAST_PCT=$PCT filled=$(( COUNT * BAR_WIDTH / TOTAL )) fbar=$(printf '%*s' "$filled" '' | tr ' ' '#') ebar=$(printf '%*s' "$(( BAR_WIDTH - filled ))" '' | tr ' ' '.') # move cursor up one line, carriage-return, then overwrite printf "\033[1A\rProgress: [%s%s] %3d%% (%d/%d)\n" \ "$fbar" "$ebar" "$PCT" "$COUNT" "$TOTAL" >&2 fi # skip or convert if [[ -f "$DST_FILE" ]]; then SKIPPED+=("$REL_PATH") else if ffmpeg "${FFMPEG_OPTS[@]}" -i "$SRC_FILE" \ "${CODEC_OPTS[@]}" -vn "${META_OPTS[@]}" -f ipod "$DST_FILE"; then CONVERTED+=("$REL_PATH") else FAILED["$REL_PATH"]=1 fi fi done # === Summary === echo >&2 printf "========== SUMMARY ==========\n" >&2 printf "Skipped (already present): %d\n" "${#SKIPPED[@]}" >&2 for f in "${SKIPPED[@]}"; do printf " • %s\n" "$f"; done >&2 echo >&2 printf "Converted: %d\n" "${#CONVERTED[@]}" >&2 for f in "${CONVERTED[@]}"; do printf " • %s\n" "$f"; done >&2 echo >&2 printf "Failed: %d\n" "${#FAILED[@]}" >&2 for f in "${!FAILED[@]}"; do printf " • %s\n" "$f"; done >&2 echo >&2 printf "Output directory: '%s'\n" "$DST_DIR" >&2 echo "=============================" >&2 # appropriate exit code (( ${#FAILED[@]} )) && exit 2 || exit 0