changemaker.lite/media-manager/scripts/reencode_for_streaming.sh

771 lines
25 KiB
Bash
Executable File

#!/bin/bash
# Script to re-encode video files for optimal Plex/web streaming
# Uses H.265/HEVC with web-optimized quality (CRF 25)
# Features: NVENC GPU acceleration, faststart for streaming, in-place replacement
# Adds [E] flag to filename after successful encoding
# Enable strict error handling
set -o pipefail # Catch failures in piped commands
# Use environment variable if set (for Docker), otherwise use default path
# Target public directory only (curated content for streaming)
ROOT_DIR="${SOURCE_LIBRARY:-/media/bunker-admin/Internal/plex/xxx/media/public}"
LOG_FILE="$ROOT_DIR/reencode_log_$(date +%Y%m%d_%H%M%S).txt"
# Minimum file size ratio (encoded/original) - below this suggests encoding failure
MIN_SIZE_RATIO=0.05
# Quality settings
CRF_VALUE=25
AUDIO_BITRATE="128k"
cd "$ROOT_DIR" || exit 1
# Check dependencies
if ! command -v ffmpeg &> /dev/null; then
echo "Error: ffmpeg is not installed."
exit 1
fi
if ! command -v ffprobe &> /dev/null; then
echo "Error: ffprobe is not installed."
exit 1
fi
# Detect encoder
detect_encoder() {
# Check if CPU mode is forced
if [[ "$FORCE_CPU" == true ]]; then
echo "Using CPU encoder (libx265) [--cpu flag]"
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
return
fi
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -q hevc_nvenc; then
# Actually test NVENC with a real encode (not just check if listed)
local test_output="/tmp/nvenc_detect_test_$$.mp4"
local test_result
test_result=$(ffmpeg -y -f lavfi -i "color=black:s=256x256:d=0.1" \
-c:v hevc_nvenc -preset p4 -gpu 0 \
"$test_output" 2>&1)
local test_exit=$?
rm -f "$test_output"
if [[ $test_exit -ne 0 ]]; then
# Parse specific CUDA errors for helpful messages
echo ""
if echo "$test_result" | grep -q "CUDA_ERROR_UNKNOWN"; then
echo "WARNING: NVENC unavailable - GPU encoder is in a bad state"
echo " Error: CUDA context creation failed"
echo " Fix: Reboot, or log out/in to release GPU resources"
elif echo "$test_result" | grep -q "No capable devices"; then
echo "WARNING: NVENC unavailable - no capable GPU devices found"
else
echo "WARNING: NVENC test encode failed"
local err_line=$(echo "$test_result" | grep -i -E 'error|failed' | head -1)
if [[ -n "$err_line" ]]; then
echo " $err_line"
fi
fi
echo ""
echo "Falling back to CPU encoder (libx265)"
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
return
fi
# NVENC works - check if currently busy (use session count, not utilization %)
local enc_sessions=$(nvidia-smi --query-gpu=encoder.stats.sessionCount --format=csv,noheader 2>/dev/null | head -1)
if [[ -n "$enc_sessions" ]] && [[ "$enc_sessions" -gt 0 ]]; then
echo ""
echo "WARNING: GPU encoder has $enc_sessions active session(s)"
echo ""
echo "Options:"
echo " 1) Press Enter to continue with GPU (may queue or fail)"
echo " 2) Type 'cpu' to use CPU encoding (slower but reliable)"
echo " 3) Press Ctrl+C to cancel"
echo ""
read -p "Choice: " enc_choice
if [[ "$enc_choice" == "cpu" ]]; then
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
return
fi
fi
echo "Using NVENC GPU encoder (hevc_nvenc)"
ENCODER="hevc_nvenc"
# -cq is NVENC's quality mode (similar to CRF), p4 is balanced preset
# -gpu 0 explicitly selects first GPU
# -tag:v hvc1 ensures Apple device compatibility
ENCODER_OPTS="-preset p4 -cq $CRF_VALUE -rc vbr -gpu 0 -tag:v hvc1"
else
echo "NVENC not available, using CPU encoder (libx265)"
ENCODER="libx265"
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
fi
}
# Check if file is already H.265/HEVC
is_hevc() {
local file="$1"
local codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
[[ "$codec" == "hevc" ]] || [[ "$codec" == "h265" ]]
}
# Check if filename already has [E] encoded flag
has_encoded_flag() {
local file="$1"
local filename=$(basename "$file")
[[ "$filename" =~ \[E\] ]]
}
# Add [E] flag to filename (before [H/V] orientation tag, compatible with organize_by_studio.sh)
add_encoded_flag() {
local file="$1"
local dir=$(dirname "$file")
local filename=$(basename "$file")
local new_filename
# Insert [E] before [H] or [V] orientation tag if present
if [[ "$filename" =~ ^(.+)\ \[([HV])\]\.mp4$ ]]; then
local base="${BASH_REMATCH[1]}"
local orientation="${BASH_REMATCH[2]}"
new_filename="${base} [E] [${orientation}].mp4"
else
# Fallback: add before .mp4 if no orientation tag found
local name="${filename%.mp4}"
new_filename="${name} [E].mp4"
fi
local new_path="${dir}/${new_filename}"
mv "$file" "$new_path"
echo "$new_path"
}
# Extract duration in seconds from filename tags [H:MM] or [Xs]
get_duration_from_filename() {
local file="$1"
local filename=$(basename "$file")
local seconds=0
# Try [H:MM] format (e.g., [1:23] = 1 hour 23 minutes)
if [[ "$filename" =~ \[([0-9]+):([0-9]+)\] ]]; then
# Use 10# prefix to force base-10 interpretation (avoids octal errors with 08, 09)
local hours=$((10#${BASH_REMATCH[1]}))
local minutes=$((10#${BASH_REMATCH[2]}))
seconds=$((hours * 3600 + minutes * 60))
# Try [Xs] format for short clips (e.g., [45s])
elif [[ "$filename" =~ \[([0-9]+)s\] ]]; then
seconds=$((10#${BASH_REMATCH[1]}))
fi
echo "$seconds"
}
# Format seconds as human-readable time
format_time() {
local seconds=$1
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
local secs=$((seconds % 60))
if [[ $hours -gt 0 ]]; then
printf "%dh %dm %ds" "$hours" "$minutes" "$secs"
elif [[ $minutes -gt 0 ]]; then
printf "%dm %ds" "$minutes" "$secs"
else
printf "%ds" "$secs"
fi
}
# Format video duration for display
format_video_duration() {
local seconds=$1
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
if [[ $hours -gt 0 ]]; then
printf "%dh %dm" "$hours" "$minutes"
else
printf "%dm" "$minutes"
fi
}
# Check if file has faststart (moov atom at beginning)
has_faststart() {
local file="$1"
# Check if moov atom appears before mdat atom
local atoms=$(ffprobe -v error -show_format -show_entries format_tags "$file" 2>&1 | head -20)
# Simple heuristic: if file streams well, it likely has faststart
# More accurate: use atomicparsley or similar, but ffprobe works for our needs
return 0 # We'll re-encode anyway to ensure proper settings
}
# Get video info
get_video_info() {
local file="$1"
local codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
local width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
local height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
local bitrate=$(ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
if [[ -n "$bitrate" ]] && [[ "$bitrate" != "N/A" ]]; then
bitrate=$((bitrate / 1000))k
else
bitrate="unknown"
fi
echo "${codec}:${width}x${height}:${bitrate}"
}
# Format file size for display
format_size() {
local bytes=$1
if [[ $bytes -ge 1073741824 ]]; then
echo "$(echo "scale=2; $bytes / 1073741824" | bc)G"
elif [[ $bytes -ge 1048576 ]]; then
echo "$(echo "scale=2; $bytes / 1048576" | bc)M"
elif [[ $bytes -ge 1024 ]]; then
echo "$(echo "scale=2; $bytes / 1024" | bc)K"
else
echo "${bytes}B"
fi
}
# Log message to file and stdout
log() {
echo "$1" | tee -a "$LOG_FILE"
}
# Debug log function
debug_log() {
if [[ "$DEBUG" == true ]]; then
log " [DEBUG] $1"
fi
}
# Check GPU status
check_gpu_status() {
if ! command -v nvidia-smi &> /dev/null; then
debug_log "nvidia-smi not found"
return 1
fi
local gpu_info=$(nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu,encoder.stats.sessionCount --format=csv,noheader 2>/dev/null)
if [[ -n "$gpu_info" ]]; then
debug_log "GPU Status: $gpu_info"
fi
# Check encoder sessions
local enc_sessions=$(nvidia-smi --query-gpu=encoder.stats.sessionCount --format=csv,noheader 2>/dev/null | head -1)
if [[ -n "$enc_sessions" ]] && [[ "$enc_sessions" -gt 0 ]]; then
debug_log "Active encoder sessions: $enc_sessions"
fi
return 0
}
# Test NVENC with a quick encode
test_nvenc() {
debug_log "Testing NVENC availability..."
# Create a tiny test pattern and try to encode it
local test_output="/tmp/nvenc_test_$$.mp4"
local test_result
test_result=$(ffmpeg -y -f lavfi -i "color=black:s=256x256:d=0.1" \
-c:v hevc_nvenc -preset p4 -gpu 0 \
"$test_output" 2>&1)
local test_exit=$?
rm -f "$test_output"
if [[ $test_exit -eq 0 ]]; then
debug_log "NVENC test: SUCCESS"
return 0
else
debug_log "NVENC test: FAILED (exit $test_exit)"
debug_log "NVENC error: $(echo "$test_result" | grep -i -E 'error|failed|cannot' | head -3)"
return 1
fi
}
# Main encoding function
encode_file() {
local input="$1"
local temp_output="${input}.temp.mp4"
# Get original file size
local original_size=$(stat -c%s "$input" 2>/dev/null)
if [[ -z "$original_size" ]] || [[ "$original_size" -eq 0 ]]; then
log " ERROR: Cannot read input file"
return 1
fi
local original_size_fmt=$(format_size $original_size)
local video_info=$(get_video_info "$input")
log " Original: $video_info ($original_size_fmt)"
# Check GPU status if using NVENC
if [[ "$ENCODER" == "hevc_nvenc" ]] && [[ "$DEBUG" == true ]]; then
check_gpu_status
fi
local encode_output=""
local exit_code=1
local used_encoder="$ENCODER"
local attempt=0
# GPU encoding with retries
if [[ "$ENCODER" == "hevc_nvenc" ]]; then
for ((attempt=1; attempt<=GPU_RETRIES; attempt++)); do
debug_log "GPU encode attempt $attempt/$GPU_RETRIES"
# Clean up any previous temp file
rm -f "$temp_output"
# Build ffmpeg command
local ffmpeg_cmd="ffmpeg -nostdin -hide_banner -i \"$input\" \
-c:v hevc_nvenc $ENCODER_OPTS \
-c:a aac -b:a $AUDIO_BITRATE \
-movflags +faststart \
-y \"$temp_output\""
debug_log "Command: ffmpeg -c:v hevc_nvenc $ENCODER_OPTS -c:a aac -b:a $AUDIO_BITRATE"
# Run encoding
if [[ "$DEBUG" == true ]]; then
encode_output=$(eval "$ffmpeg_cmd" 2>&1)
exit_code=$?
# Show last few lines of output in debug mode
debug_log "Exit code: $exit_code"
echo "$encode_output" | tail -5 | while read -r line; do
debug_log "ffmpeg: $line"
done
else
encode_output=$(eval "$ffmpeg_cmd" 2>&1)
exit_code=$?
fi
# Check for success
if [[ $exit_code -eq 0 ]] && [[ -f "$temp_output" ]]; then
local temp_size=$(stat -c%s "$temp_output" 2>/dev/null)
if [[ -n "$temp_size" ]] && [[ "$temp_size" -gt 0 ]]; then
debug_log "GPU encode SUCCESS on attempt $attempt"
used_encoder="hevc_nvenc"
break
fi
fi
# Parse error for debugging
local nvenc_error=$(echo "$encode_output" | grep -i -E 'error|failed|cannot|no capable|cuda' | head -1)
if [[ -n "$nvenc_error" ]]; then
debug_log "NVENC error: $nvenc_error"
fi
# Wait before retry (increasing delay)
if [[ $attempt -lt $GPU_RETRIES ]]; then
local wait_time=$((attempt * 2))
debug_log "Waiting ${wait_time}s before retry..."
sleep $wait_time
fi
done
# If all GPU attempts failed, fall back to CPU
if [[ $exit_code -ne 0 ]] || [[ ! -f "$temp_output" ]]; then
# Always show the error (not just in debug mode)
local error_summary=$(echo "$encode_output" | grep -i -E 'error|failed|cannot|cuda' | head -1)
if [[ -n "$error_summary" ]]; then
log " NVENC failed: $error_summary"
else
log " NVENC failed after $GPU_RETRIES attempts"
fi
log " Falling back to CPU encoder..."
rm -f "$temp_output"
used_encoder="libx265"
local cpu_cmd="ffmpeg -nostdin -hide_banner -i \"$input\" \
-c:v libx265 -preset medium -crf $CRF_VALUE -x265-params log-level=error \
-c:a aac -b:a $AUDIO_BITRATE \
-movflags +faststart \
-y \"$temp_output\""
debug_log "Falling back to CPU: libx265 -preset medium -crf $CRF_VALUE"
if [[ "$DEBUG" == true ]]; then
encode_output=$(eval "$cpu_cmd" 2>&1)
exit_code=$?
debug_log "CPU encode exit code: $exit_code"
else
encode_output=$(eval "$cpu_cmd" 2>&1)
exit_code=$?
fi
fi
else
# CPU encoding (no retries needed)
debug_log "Using CPU encoder directly"
local cpu_cmd="ffmpeg -nostdin -hide_banner -i \"$input\" \
-c:v $ENCODER $ENCODER_OPTS \
-c:a aac -b:a $AUDIO_BITRATE \
-movflags +faststart \
-y \"$temp_output\""
if [[ "$DEBUG" == true ]]; then
encode_output=$(eval "$cpu_cmd" 2>&1)
exit_code=$?
debug_log "CPU encode exit code: $exit_code"
else
encode_output=$(eval "$cpu_cmd" 2>&1)
exit_code=$?
fi
used_encoder="$ENCODER"
fi
# Verify encoding success
if [[ $exit_code -ne 0 ]] || [[ ! -f "$temp_output" ]]; then
log " ERROR: Encoding failed (exit code: $exit_code)"
if [[ "$DEBUG" == true ]]; then
echo "$encode_output" | grep -i -E 'error|failed|invalid' | head -3 | while read -r line; do
debug_log "Error detail: $line"
done
fi
rm -f "$temp_output"
return 1
fi
# Check output file size
local new_size=$(stat -c%s "$temp_output" 2>/dev/null)
if [[ -z "$new_size" ]] || [[ "$new_size" -eq 0 ]]; then
log " ERROR: Output file is empty or missing"
rm -f "$temp_output"
return 1
fi
local new_size_fmt=$(format_size $new_size)
# Verify file isn't suspiciously small (encoding failure indicator)
local size_ratio=$(echo "scale=4; $new_size / $original_size" | bc)
if (( $(echo "$size_ratio < $MIN_SIZE_RATIO" | bc -l) )); then
log " ERROR: Output file too small ($(format_size $new_size)) - likely corrupted"
rm -f "$temp_output"
return 1
fi
# Verify output is playable
local verify_output=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$temp_output" 2>&1)
if [[ -z "$verify_output" ]] || [[ "$verify_output" == *"Invalid"* ]]; then
log " ERROR: Output file verification failed"
rm -f "$temp_output"
return 1
fi
# Calculate space savings
local saved=$((original_size - new_size))
local saved_fmt=$(format_size $saved)
local percent_saved=$(echo "scale=1; 100 - ($new_size * 100 / $original_size)" | bc)
# Replace original with encoded file
mv "$temp_output" "$input"
if [[ $saved -gt 0 ]]; then
log " Encoded: hevc/$used_encoder ($new_size_fmt) - saved $saved_fmt ($percent_saved%)"
else
log " Encoded: hevc/$used_encoder ($new_size_fmt) - increased by $(format_size $((-saved)))"
fi
return 0
}
# Show usage
show_usage() {
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Re-encodes video files to H.265/HEVC for streaming optimization."
echo "Adds [E] flag to filename after successful encoding."
echo ""
echo "Options:"
echo " --dry-run Show what would be encoded without making changes"
echo " --cpu Force CPU encoding (skip NVENC, slower but reliable)"
echo " --debug Show verbose ffmpeg output and GPU diagnostics"
echo " --skip-hevc Skip files already encoded in H.265/HEVC (default)"
echo " --force-all Re-encode all files, even if already HEVC"
echo " --dir PATH Process only files in specified directory"
echo " --single FILE Process only a single file"
echo " --help Show this help message"
echo ""
echo "Files with [E] in filename are always skipped."
echo ""
}
# Parse arguments
DRY_RUN=false
SKIP_HEVC=true # Default: skip already-encoded HEVC files
FORCE_CPU=false
DEBUG=false
GPU_RETRIES=3 # Number of times to retry GPU encoding before falling back
TARGET_DIR=""
SINGLE_FILE=""
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
--skip-hevc)
SKIP_HEVC=true
shift
;;
--force-all)
SKIP_HEVC=false
shift
;;
--cpu)
FORCE_CPU=true
shift
;;
--debug)
DEBUG=true
shift
;;
--dir)
TARGET_DIR="$2"
shift 2
;;
--single)
SINGLE_FILE="$2"
shift 2
;;
--help)
show_usage
exit 0
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Detect encoder
detect_encoder
# Run NVENC test if debug mode and using GPU
if [[ "$DEBUG" == true ]] && [[ "$ENCODER" == "hevc_nvenc" ]]; then
echo ""
echo "[DEBUG] Running NVENC diagnostics..."
check_gpu_status
if ! test_nvenc; then
echo "[DEBUG] WARNING: NVENC test failed - encoding may fall back to CPU"
fi
fi
echo ""
echo "========================================="
echo " Streaming-Optimized Re-encoder"
echo "========================================="
echo " Encoder: $ENCODER (H.265/HEVC)"
echo " Quality: CRF $CRF_VALUE (web-optimized)"
echo " Audio: AAC @ $AUDIO_BITRATE"
echo " Mode: In-place replacement + [E] flag"
if [[ "$SKIP_HEVC" == true ]]; then
echo " Skipping: Files with [E] flag or HEVC codec"
fi
if [[ "$DRY_RUN" == true ]]; then
echo " DRY RUN: No files will be modified"
fi
if [[ "$DEBUG" == true ]]; then
echo " DEBUG MODE: Verbose output enabled"
echo " GPU Retries: $GPU_RETRIES attempts before CPU fallback"
fi
echo " Log: $LOG_FILE"
echo "========================================="
echo ""
# Initialize counters
total_files=0
processed_files=0
skipped_files=0
flagged_files=0
failed_files=0
total_saved=0
total_video_duration=0
encoded_video_duration=0
start_time=$(date +%s)
# Build file list
if [[ -n "$SINGLE_FILE" ]]; then
files=("$SINGLE_FILE")
elif [[ -n "$TARGET_DIR" ]]; then
mapfile -t files < <(find "$TARGET_DIR" -type f -name "*.mp4" ! -name "*.temp.mp4" | sort)
else
# Find all MP4 files, excluding certain directories and temp files
mapfile -t files < <(find . -type f -name "*.mp4" \
! -name "*.temp.mp4" \
! -path "*/scripts/*" \
! -path "*/.temp/*" \
| sort)
fi
total_files=${#files[@]}
log "Found $total_files MP4 files to process"
# Pre-scan to calculate total duration needing encoding
echo "Scanning files to estimate encoding time..."
files_to_encode=0
for file in "${files[@]}"; do
# Skip files with [E] flag
if has_encoded_flag "$file"; then
continue
fi
# Skip HEVC files (they just get flagged, no encoding)
if [[ "$SKIP_HEVC" == true ]] && is_hevc "$file"; then
continue
fi
# This file needs encoding - add its duration
duration=$(get_duration_from_filename "$file")
total_video_duration=$((total_video_duration + duration))
((files_to_encode++))
done
if [[ $files_to_encode -gt 0 ]]; then
log "Files to encode: $files_to_encode ($(format_video_duration $total_video_duration) of video)"
else
log "No files need encoding"
fi
log ""
# Confirmation prompt (skip in dry-run mode)
if [[ "$DRY_RUN" == false ]] && [[ $files_to_encode -gt 0 ]]; then
echo ""
echo "Ready to start encoding $files_to_encode files."
echo "This will replace original files with H.265/HEVC versions."
echo ""
read -p "Proceed? [y/N] " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
echo ""
fi
# Process each file
for file in "${files[@]}"; do
((processed_files++))
# Clean up path for display
display_path="${file#./}"
log "[$processed_files/$total_files] $display_path"
# Check if already has [E] encoded flag
if has_encoded_flag "$file"; then
log " Skipped: Already has [E] flag"
((skipped_files++))
continue
fi
# Check if already HEVC (but no [E] flag - add the flag)
if [[ "$SKIP_HEVC" == true ]] && is_hevc "$file"; then
if [[ "$DRY_RUN" == true ]]; then
log " Would add [E] flag: Already HEVC encoded"
else
new_path=$(add_encoded_flag "$file")
log " Added [E] flag: Already HEVC encoded -> $(basename "$new_path")"
((flagged_files++))
fi
continue
fi
# Get original size for space savings calculation
original_size=$(stat -c%s "$file")
if [[ "$DRY_RUN" == true ]]; then
video_info=$(get_video_info "$file")
log " Would encode: $video_info ($(format_size $original_size))"
continue
fi
# Get video duration for ETA calculation
file_duration=$(get_duration_from_filename "$file")
# Encode the file
encode_start=$(date +%s)
if encode_file "$file"; then
encode_end=$(date +%s)
new_size=$(stat -c%s "$file")
saved=$((original_size - new_size))
total_saved=$((total_saved + saved))
# Track encoded duration for ETA
encoded_video_duration=$((encoded_video_duration + file_duration))
# Add [E] flag to filename
new_path=$(add_encoded_flag "$file")
log " Renamed: $(basename "$new_path")"
# Calculate and show ETA
if [[ $encoded_video_duration -gt 0 ]] && [[ $total_video_duration -gt 0 ]]; then
elapsed_so_far=$((encode_end - start_time))
remaining_video=$((total_video_duration - encoded_video_duration))
if [[ $elapsed_so_far -gt 0 ]]; then
# Calculate encoding speed (video seconds per real second)
speed=$(echo "scale=2; $encoded_video_duration / $elapsed_so_far" | bc)
# Estimate remaining time
if (( $(echo "$speed > 0" | bc -l) )); then
eta_seconds=$(echo "scale=0; $remaining_video / $speed" | bc)
log " ETA: $(format_time $eta_seconds) remaining ($(format_video_duration $remaining_video) of video left, ${speed}x speed)"
fi
fi
fi
else
((failed_files++))
log " FAILED - file unchanged"
fi
echo ""
done
# Calculate elapsed time
end_time=$(date +%s)
elapsed=$((end_time - start_time))
hours=$((elapsed / 3600))
minutes=$(((elapsed % 3600) / 60))
seconds=$((elapsed % 60))
# Summary
echo ""
echo "========================================="
echo " Re-encoding Complete!"
echo "========================================="
log " Total files: $total_files"
log " Re-encoded: $((processed_files - skipped_files - flagged_files - failed_files))"
log " Flagged (HEVC + added [E]): $flagged_files"
log " Skipped (already has [E]): $skipped_files"
log " Failed: $failed_files"
if [[ $encoded_video_duration -gt 0 ]]; then
log " Video encoded: $(format_video_duration $encoded_video_duration)"
if [[ $elapsed -gt 0 ]]; then
avg_speed=$(echo "scale=2; $encoded_video_duration / $elapsed" | bc)
log " Average speed: ${avg_speed}x realtime"
fi
fi
if [[ $total_saved -gt 0 ]]; then
log " Total space saved: $(format_size $total_saved)"
elif [[ $total_saved -lt 0 ]]; then
log " Total size increase: $(format_size $((-total_saved)))"
fi
log " Time elapsed: ${hours}h ${minutes}m ${seconds}s"
log " Log saved to: $LOG_FILE"
echo "========================================="
echo ""