Last active
March 16, 2024 05:49
-
-
Save aquilax/a151546e4f166cba9db0cec407736693 to your computer and use it in GitHub Desktop.
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 | |
set -o nounset # abort on unbound variable | |
set -o pipefail # don't hide errors within pipes | |
# Define usage function | |
usage() { | |
cat <<EOF | |
Usage: $(basename "${BASH_SOURCE[0]}") [-k|--keep] [--recursive] [--force] [--dry-run] INPUT_DIR [OUTPUT_DIR] | |
Batch re-encodes video files with x265 | |
Available options: | |
-h, --help Print this help and exit | |
-k, --keep Keeps the original files | |
--recursive Searches for files recursively | |
--dry-run Performs a dry run withour chaging files | |
--force Skip checking the target format | |
--downscale Downscale to 1280x720 is size is larger | |
EOF | |
exit | |
} | |
# Function to handle Ctrl-C | |
interrupt_handler() { | |
echo "Ctrl-C caught. Terminating loop on next iteration." | |
stop_loop=true | |
} | |
print_file_stats() { | |
local file=$1 | |
local output=$2 | |
size_before=$(stat -c%s "$file") | |
count_total=$((count_total + 1)) | |
size_before_total=$((size_before + size_before_total)) | |
size_after=$size_before | |
if [ "$DRY_RUN" != true ]; then | |
size_after=$(stat -c%s "$output") | |
fi | |
size_after_total=$((size_after + size_after_total)) | |
reduction_ratio=$(bc -l <<< "scale=2; ($size_before - $size_after) / $size_before") | |
printf "# number: %d\tsize before: %s\tsize after: %s\treduction: %s\tfilename: %s\n" "$count_total" "$size_before" "$size_after" "$reduction_ratio" "$file" | |
} | |
# Function to process single file | |
process_file() { | |
local file=$1 | |
codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file") | |
# Get the base filename without extension | |
filename=$(basename -- "$file") | |
directory=$(dirname "$file") | |
filename="${filename%.*}" | |
# Set the output directory to the file directory | |
OUTPUT_DIR="$directory" | |
if [[ ("$file" == *_x265.mp4) || ("$file" == *_x265.mkv) ]]; then | |
echo "Skipping [filename] $file" | |
return | |
fi | |
if [[ ($codec == "hevc") ]]; then | |
if [[ ("$FORCE" == true) ]]; then | |
echo "Force converting $file" | |
else | |
echo "Skipping [codec] $file" | |
return | |
fi | |
fi | |
# Determine the output file extension | |
if [[ "$file" == *.mkv ]]; then | |
output_ext=".mkv" | |
else | |
output_ext=".mp4" | |
fi | |
# Set the output file name and path | |
output="$OUTPUT_DIR/${filename}_x265${output_ext}" | |
options=(-c:v libx265 -crf 28 -preset medium -c:a aac) | |
if $DOWNSCALE; then | |
# options=("${options[@]}" "-vf" "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:-1:-1:color=black") | |
options=("${options[@]}" "-vf" "scale='if(gt(iw,1280),min(1280,iw),iw)':'if(gt(ih,720),min(720,ih),ih)'") | |
fi | |
exit_code=0 | |
# Re-encode the video file using FFmpeg | |
if [ "$DRY_RUN" = true ]; then | |
echo "> ffmpeg -nostdin -i $file ${options[*]} $output" | |
else | |
ffmpeg -nostdin -i "$file" "${options[@]}" "$output" | |
exit_code=$? | |
fi | |
# Check if the re-encoding process was successful | |
if [ $exit_code -eq 0 ]; then | |
print_file_stats "$file" "$output" | |
# Delete the failed attempt if it exists | |
if ! $KEEP; then | |
if [ "$DRY_RUN" = true ]; then | |
echo "> rm -f $file" | |
else | |
rm -f "$file" | |
fi | |
fi | |
else | |
# Delete the failed output | |
if [ "$DRY_RUN" = true ]; then | |
echo "> rm -f $output" | |
else | |
rm -f "$output" | |
fi | |
fi | |
} | |
# Parse command line arguments | |
KEEP=false | |
DRY_RUN=false | |
FORCE=false | |
RECURSIVE=false | |
DOWNSCALE=false | |
INPUT_DIR="" | |
OUTPUT_DIR="" | |
size_before_total=0 | |
size_after_total=0 | |
count_total=0 | |
stop_loop=false | |
while [[ $# -gt 0 ]]; do | |
key="$1" | |
case $key in | |
-h|--help) | |
usage | |
;; | |
-k|--keep) | |
KEEP=true | |
shift | |
;; | |
--dry-run) | |
DRY_RUN=true | |
shift | |
;; | |
--force) | |
FORCE=true | |
shift | |
;; | |
--recursive) | |
RECURSIVE=true | |
shift | |
;; | |
--downscale) | |
DOWNSCALE=true | |
shift | |
;; | |
*) | |
if [ -z "$INPUT_DIR" ]; then | |
INPUT_DIR="$1" | |
elif [ -z "$OUTPUT_DIR" ]; then | |
OUTPUT_DIR="$1" | |
else | |
usage | |
fi | |
shift | |
;; | |
esac | |
done | |
if [ -z "$INPUT_DIR" ]; then | |
usage | |
fi | |
if [ "$RECURSIVE" = true ]; then | |
find_options=() | |
else | |
find_options=(-maxdepth 1) | |
fi | |
# Set the interrupt handler | |
trap interrupt_handler SIGINT | |
# Loop through all video files in the input directory | |
while IFS= read -r -d '' file; do | |
if "$stop_loop"; then | |
break | |
fi | |
process_file "$file" | |
done < <(find "$INPUT_DIR" "${find_options[@]}" -type f \( -iname "*.flv" -o -iname "*.wmv" -o -iname "*.mpg" -o -iname "*.mpeg" -o -iname "*.avi" -o -iname "*.mkv" -o -iname "*.mp4" \) ! -name "*_x265.mp4" ! -name "*_x265.mkv" -print0) | |
if [ $size_before_total -gt 0 ]; then | |
reduction_ratio_total=$(bc -l <<< "scale=2; ($size_before_total - $size_after_total) / $size_before_total") | |
printf "# totals: count %d\tsize before: %s\tsize after: %s\treduction: %s\n" "$count_total" "$size_before_total" "$size_after_total" "$reduction_ratio_total" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment