Created
March 3, 2025 18:56
-
-
Save Its4Nik/05aed97695461ec7a67839a017648193 to your computer and use it in GitHub Desktop.
Convert audio Files using `ffmpeg`, `fzf` and `charmbracelet/gum`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
trap 'exit 1' SIGINT | |
# Configuration | |
INDENT=" " | |
PRESETS_FILE="${HOME}/.config/audio_presets.json" | |
SYSTEM_PRESETS_DIR="/etc/audio_presets.d" | |
DEFAULT_PRESETS='{ | |
"presets": [ | |
{ | |
"name": "Standard MP3", | |
"extension": "mp3", | |
"options": ["-b:a", "192k"] | |
}, | |
{ | |
"name": "High Quality Opus", | |
"extension": "opus", | |
"options": ["-b:a", "96k", "-vbr", "on"] | |
}, | |
{ | |
"name": "CD Quality WAV", | |
"extension": "wav", | |
"options": ["-ar", "44100", "-ac", "2"] | |
}, | |
{ | |
"name": "High Quality FLAC", | |
"extension": "flac", | |
"options": ["-compression_level", "5"] | |
}, | |
{ | |
"name": "Low Quality AAC", | |
"extension": "aac", | |
"options": ["-b:a", "128k"] | |
}, | |
{ | |
"name": "Podcast Quality OGG", | |
"extension": "ogg", | |
"options": ["-b:a", "64k"] | |
}, | |
{ | |
"name": "MP3 to OGG", | |
"extension": "ogg", | |
"options": ["-qscale:a", "5"] | |
}, | |
{ | |
"name": "OGG to MP3", | |
"extension": "mp3", | |
"options": ["-b:a", "192k"] | |
}, | |
{ | |
"name": "MP3 to FLAC", | |
"extension": "flac", | |
"options": ["-compression_level", "5"] | |
}, | |
{ | |
"name": "FLAC to MP3", | |
"extension": "mp3", | |
"options": ["-b:a", "192k"] | |
}, | |
{ | |
"name": "AAC to MP3", | |
"extension": "mp3", | |
"options": ["-b:a", "192k"] | |
} | |
] | |
}' | |
# Check dependencies | |
check_deps() { | |
local missing=() | |
command -v ffmpeg &>/dev/null || missing+=("ffmpeg") | |
command -v gum &>/dev/null || missing+=("gum") | |
command -v jq &>/dev/null || missing+=("jq") | |
command -v fzf &>/dev/null || missing+=("fzf") | |
if [ ${#missing[@]} -gt 0 ]; then | |
gum style --foreground 196 "${INDENT}Error: Missing dependencies: ${missing[*]}" | |
exit 1 | |
fi | |
} | |
# Select an inpuinit_presetst file using fzf | |
select_input_file() { | |
local file | |
file=$(find . -type f | fzf --height 40) | |
echo "$file" | |
} | |
# Initialize or load presets | |
init_presets() { | |
# Ensure user presets file exists | |
if [ ! -f "$PRESETS_FILE" ]; then | |
mkdir -p "$(dirname "$PRESETS_FILE")" | |
echo "$DEFAULT_PRESETS" > "$PRESETS_FILE" | |
fi | |
# Load user presets (expects structure: { "presets": [ ... ] }) | |
local user_presets | |
user_presets=$(jq -c '.presets' "$PRESETS_FILE") | |
# Load system presets if available | |
local sys_presets="[]" | |
if [ -d "$SYSTEM_PRESETS_DIR" ]; then | |
shopt -s nullglob | |
json_files=("$SYSTEM_PRESETS_DIR"/*.json) | |
shopt -u nullglob | |
if [ ${#json_files[@]} -gt 0 ]; then | |
sys_presets=$(jq -s 'map(.presets) | add' "${json_files[@]}") | |
fi | |
fi | |
# Combine user and system presets into a stream of preset objects | |
presets_data=$(jq -c -n --argjson user "$user_presets" --argjson sys "$sys_presets" '$user + $sys | .[]') | |
} | |
# Main conversion function | |
convert_audio() { | |
gum style --bold --foreground 212 "${INDENT}🎵 Audio Conversion Wizard" --border=rounded | |
echo | |
# Select input file using fzf | |
gum style --bold --foreground 212 "${INDENT}Select input audio file:" | |
local input_file | |
input_file=$(select_input_file) | |
[ -f "$input_file" ] || { gum style --foreground 196 "${INDENT}Input file not found!"; exit 1; } | |
# Use input file's directory as output destination | |
local input_dir | |
input_dir=$(dirname "$input_file") | |
gum style --bold --foreground 212 "${INDENT}Output directory:" | |
gum style --foreground 32 "${INDENT}$input_dir" | |
echo | |
# Ask if batch conversion is desired | |
local batch_mode=false | |
if gum confirm "${INDENT}Batch convert all files with the same extension in $input_dir?"; then | |
batch_mode=true | |
fi | |
# Determine input file extension and select files | |
local input_ext="${input_file##*.}" | |
local files_to_convert=() | |
if [ "$batch_mode" = true ]; then | |
for f in "$input_dir"/*."$input_ext"; do | |
[ -f "$f" ] && files_to_convert+=("$f") | |
done | |
else | |
files_to_convert=("$input_file") | |
fi | |
# Format selection using presets | |
gum style --bold --foreground 212 "${INDENT}Select output format:" | |
local format_choice | |
format_choice=$(echo -e "$presets_data\n{\"name\":\"Custom Format\",\"extension\":\"\"}" | | |
jq -r '.name + " (" + .extension + ")"' | | |
gum choose --height 10) | |
local preset_name | |
preset_name=$(echo "$format_choice" | cut -d'(' -f1 | xargs) | |
local new_extension | |
new_extension=$(echo "$format_choice" | grep -o '(.*)' | tr -d '() ') | |
local ffmpeg_opts=() | |
if [ "$preset_name" != "Custom Format" ]; then | |
local preset_opts | |
preset_opts=$(echo "$presets_data" | | |
jq -r "select(.name == \"$preset_name\") | .options[]" ) | |
for opt in $preset_opts; do | |
ffmpeg_opts+=("$opt") | |
done | |
else | |
gum style --bold --foreground 212 "${INDENT}Enter custom file extension:" | |
new_extension=$(gum input --placeholder "ext") | |
fi | |
# Additional ffmpeg options | |
if gum confirm "${INDENT}Add extra ffmpeg options?"; then | |
gum style --bold --foreground 212 "${INDENT}Enter extra options (space-separated):" | |
local extra_opts | |
extra_opts=$(gum input) | |
for opt in $extra_opts; do | |
ffmpeg_opts+=("$opt") | |
done | |
fi | |
# Conversion summary | |
gum style --foreground 212 "${INDENT}Conversion Summary:" | |
gum format -- "${INDENT}| Files to convert: ${#files_to_convert[@]} file(s) | |
${INDENT}| Output Format: $(gum style --foreground 32 "$new_extension") | |
${INDENT}| Options: ${ffmpeg_opts[*]}" | |
echo | |
if gum confirm "${INDENT}Start conversion?"; then | |
for file in "${files_to_convert[@]}"; do | |
local base_name | |
base_name=$(basename "$file" | sed 's/\(.*\)\..*/\1/') | |
local output_path="${input_dir}/${base_name}.${new_extension}" | |
local cmd=("ffmpeg" "-i" "$file" "${ffmpeg_opts[@]}" "-y" "$output_path") | |
echo "${INDENT}Converting: $file -> $output_path" | |
if "${cmd[@]}" 2>&1 | grep -i -v "deprecated"; then | |
gum style --bold --foreground 46 "${INDENT}✅ Converted: $file" | |
else | |
gum style --bold --foreground 196 "${INDENT}❌ Failed: $file" | |
fi | |
done | |
else | |
gum style --italic --foreground 240 "${INDENT}Conversion canceled" | |
exit 0 | |
fi | |
} | |
# Main execution | |
check_deps | |
init_presets | |
convert_audio |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment