#!/usr/bin/env bash set -euo pipefail # to_mp4.sh # # Convert video files (non-mp4) to mp4 format using ffmpeg. # Accepts either a single video file or a directory (will recurse into subdirectories). # Video extensions to search for (excluding mp4 since that's our target) VIDEO_EXTENSIONS=("webm" "mkv" "avi" "mov" "wmv" "flv" "m4v" "mpg" "mpeg" "3gp" "ogv" "ts" "mts" "m2ts" "vob" "asf" "rm" "rmvb" "divx" "f4v") # Conversion settings CRF=23 # Quality (0-51, lower = better quality, 18-28 is reasonable for H.264) PRESET="medium" # Encoding speed: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow DELETE_ORIGINAL=false log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" } usage() { cat </dev/null 2>&1; then echo "Error: 'ffmpeg' is not installed or not in PATH." >&2 exit 1 fi } is_video_file() { local file="$1" local ext="${file##*.}" ext="${ext,,}" # lowercase for video_ext in "${VIDEO_EXTENSIONS[@]}"; do if [[ "$ext" == "${video_ext,,}" ]]; then return 0 fi done return 1 } convert_to_mp4() { local input_file="$1" local output_file="${input_file%.*}.mp4" # 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'" # Use H.264 codec for video and AAC for audio (maximum compatibility) if ffmpeg -hide_banner -loglevel warning -i "$input_file" \ -c:v libx264 -crf "$CRF" -preset "$PRESET" \ -c:a aac -b:a 192k \ -movflags +faststart \ "$output_file"; 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'" # Remove partial output file if it exists [[ -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'..." # Find all video files (case-insensitive) while IFS= read -r -d '' file; do # Skip mp4 files if [[ "${file,,}" == *.mp4 ]]; then continue fi ((count++)) || true if ! convert_to_mp4 "$file"; then ((failed++)) || true fi done < <(find "$dir" -type f \( -iname "*.webm" -o -iname "*.mkv" -o -iname "*.avi" -o -iname "*.mov" \ -o -iname "*.wmv" -o -iname "*.flv" -o -iname "*.m4v" -o -iname "*.mpg" -o -iname "*.mpeg" \ -o -iname "*.3gp" -o -iname "*.ogv" -o -iname "*.ts" -o -iname "*.mts" -o -iname "*.m2ts" \ -o -iname "*.vob" -o -iname "*.asf" -o -iname "*.rm" -o -iname "*.rmvb" -o -iname "*.divx" \ -o -iname "*.f4v" \) -print0 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 ":c:p:dh" opt; do case "$opt" in 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" } 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,,}" == *.mp4 ]]; then log "File '$TARGET_PATH' is already in mp4 format, skipping." exit 0 fi if is_video_file "$TARGET_PATH"; then convert_to_mp4 "$TARGET_PATH" else echo "Error: '$TARGET_PATH' is not a recognized video file." >&2 exit 1 fi elif [[ -d "$TARGET_PATH" ]]; then # Directory process_directory "$TARGET_PATH" else echo "Error: '$TARGET_PATH' is neither a file nor a directory." >&2 exit 1 fi log "Done!" } main "$@"