#!/usr/bin/env bash set -euo pipefail # convert_video.sh # # Convert video files to a target format (mp4 or webm) using ffmpeg. # Accepts either a single video file or a directory (will recurse into subdirectories). # Source common library SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" # shellcheck source=../lib/common.sh source "$SCRIPT_DIR/../lib/common.sh" # Default settings TARGET_FORMAT="mp4" CRF="" # Will be set based on format if not specified PRESET="medium" DELETE_ORIGINAL=false TARGET_PATH="" # Video extensions to search for ALL_VIDEO_EXTENSIONS=("mp4" "webm" "mkv" "avi" "mov" "wmv" "flv" "m4v" "mpg" "mpeg" "3gp" "ogv" "ts" "mts" "m2ts" "vob" "asf" "rm" "rmvb" "divx" "f4v") usage() { cat </dev/null 2>&1; then echo "Error: 'ffmpeg' is not installed or not in PATH." >&2 exit 1 fi } get_video_extensions_except() { local exclude="$1" local exts=() for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do if [[ ${ext,,} != "${exclude,,}" ]]; then exts+=("$ext") fi done echo "${exts[@]}" } is_video_file() { local file="$1" local ext="${file##*.}" ext="${ext,,}" # lowercase for video_ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do if [[ $ext == "${video_ext,,}" ]]; then return 0 fi done return 1 } convert_video() { local input_file="$1" local output_file="${input_file%.*}.${TARGET_FORMAT}" # Skip if output already exists if [[ -f $output_file ]]; then log "Skipping '$input_file': output '$output_file' already exists" return 0 fi log "Converting '$input_file' -> '$output_file'" local ffmpeg_args=() ffmpeg_args+=(-hide_banner -loglevel warning -i "$input_file") if [[ $TARGET_FORMAT == "mp4" ]]; then # H.264 codec for video and AAC for audio (maximum compatibility) ffmpeg_args+=(-c:v libx264 -crf "$CRF" -preset "$PRESET") ffmpeg_args+=(-c:a aac -b:a 192k) ffmpeg_args+=(-movflags +faststart) elif [[ $TARGET_FORMAT == "webm" ]]; then # VP9 codec for video and Opus for audio ffmpeg_args+=(-c:v libvpx-vp9 -crf "$CRF" -b:v 0) ffmpeg_args+=(-c:a libopus -b:a 128k) fi ffmpeg_args+=("$output_file") if ffmpeg "${ffmpeg_args[@]}"; then log "Successfully converted '$input_file'" if [[ $DELETE_ORIGINAL == true ]]; then log "Deleting original: '$input_file'" rm "$input_file" fi else log "Error converting '$input_file'" [[ -f $output_file ]] && rm "$output_file" return 1 fi } process_directory() { local dir="$1" local count=0 local failed=0 log "Searching for video files in '$dir'..." # Build find command dynamically local find_args=(-type f \() local first=true for ext in "${ALL_VIDEO_EXTENSIONS[@]}"; do if [[ ${ext,,} != "${TARGET_FORMAT,,}" ]]; then if [[ $first == true ]]; then first=false else find_args+=(-o) fi find_args+=(-iname "*.$ext") fi done find_args+=(\) -print0) while IFS= read -r -d '' file; do ((count++)) || true if ! convert_video "$file"; then ((failed++)) || true fi done < <(find "$dir" "${find_args[@]}" 2>/dev/null) log "Processed $count video file(s), $failed failed" if [[ $count -eq 0 ]]; then log "No video files found in '$dir'" fi } parse_args() { while getopts ":f:c:p:dh" opt; do case "$opt" in f) TARGET_FORMAT="${OPTARG,,}" if [[ $TARGET_FORMAT != "mp4" && $TARGET_FORMAT != "webm" ]]; then echo "Error: Format must be 'mp4' or 'webm'" >&2 exit 1 fi ;; c) CRF="$OPTARG" ;; p) PRESET="$OPTARG" ;; d) DELETE_ORIGINAL=true ;; h) usage exit 0 ;; :) echo "Error: Option -$OPTARG requires an argument." >&2 usage exit 1 ;; \?) echo "Error: Invalid option -$OPTARG" >&2 usage exit 1 ;; esac done shift $((OPTIND - 1)) if [[ $# -lt 1 ]]; then echo "Error: No path specified." >&2 usage exit 1 fi TARGET_PATH="$1" # Set default CRF based on format if not specified if [[ -z $CRF ]]; then if [[ $TARGET_FORMAT == "mp4" ]]; then CRF=23 else CRF=30 fi fi } main() { ensure_ffmpeg parse_args "$@" if [[ ! -e $TARGET_PATH ]]; then echo "Error: Path '$TARGET_PATH' does not exist." >&2 exit 1 fi if [[ -f $TARGET_PATH ]]; then # Single file if [[ ${TARGET_PATH,,} == *."$TARGET_FORMAT" ]]; then log "File '$TARGET_PATH' is already in $TARGET_FORMAT format, skipping." exit 0 fi if is_video_file "$TARGET_PATH"; then convert_video "$TARGET_PATH" else echo "Error: '$TARGET_PATH' is not a recognized video file." >&2 exit 1 fi elif [[ -d $TARGET_PATH ]]; then process_directory "$TARGET_PATH" else echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2 exit 1 fi log "Done!" } main "$@"