Files
encoderPro/reencode-movies.sh
2026-01-24 17:43:28 -05:00

296 lines
8.3 KiB
Bash

#!/bin/bash
################################################################################
# ENCODERPRO SCRIPT - PHASE 1
################################################################################
# Purpose: Remove subtitle streams from movie files and re-encode safely
# Features:
# - Recursive directory scanning
# - Safe file replacement using staging
# - Original file archiving
# - Restart-safe operation
# - Detailed logging
################################################################################
set -euo pipefail # Exit on error, undefined vars, pipe failures
################################################################################
# CONFIGURATION
################################################################################
# Directories (CUSTOMIZE THESE)
MOVIES_DIR="${MOVIES_DIR:-/mnt/user/movies}"
ARCHIVE_DIR="${ARCHIVE_DIR:-/mnt/user/archive/movies}"
WORK_DIR="${WORK_DIR:-/mnt/user/temp/encoderpro-work}"
LOG_FILE="${LOG_FILE:-/var/log/encoderpro-movies.log}"
# Encoding settings
VIDEO_CODEC="${VIDEO_CODEC:-libx265}"
VIDEO_PRESET="${VIDEO_PRESET:-medium}"
VIDEO_CRF="${VIDEO_CRF:-23}"
# File extensions to process
FILE_EXTENSIONS=("mkv" "mp4" "avi" "m4v")
# Dry run mode (set to 1 to test without encoding)
DRY_RUN="${DRY_RUN:-0}"
################################################################################
# LOGGING FUNCTIONS
################################################################################
log() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $*" | tee -a "$LOG_FILE"
}
log_error() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ERROR: $*" | tee -a "$LOG_FILE" >&2
}
log_success() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] SUCCESS: $*" | tee -a "$LOG_FILE"
}
################################################################################
# VALIDATION FUNCTIONS
################################################################################
validate_dependencies() {
local missing_deps=0
for cmd in ffmpeg ffprobe; do
if ! command -v "$cmd" &> /dev/null; then
log_error "Required command '$cmd' not found"
missing_deps=1
fi
done
if [[ $missing_deps -eq 1 ]]; then
log_error "Missing dependencies. Please install ffmpeg."
exit 1
fi
log "All dependencies satisfied"
}
validate_directories() {
if [[ ! -d "$MOVIES_DIR" ]]; then
log_error "Movies directory does not exist: $MOVIES_DIR"
exit 1
fi
# Create necessary directories
mkdir -p "$ARCHIVE_DIR" "$WORK_DIR"
# Ensure log directory exists
local log_dir=$(dirname "$LOG_FILE")
mkdir -p "$log_dir"
log "Directory validation complete"
log " Movies: $MOVIES_DIR"
log " Archive: $ARCHIVE_DIR"
log " Work: $WORK_DIR"
log " Log: $LOG_FILE"
}
################################################################################
# FILE PROCESSING FUNCTIONS
################################################################################
build_file_list() {
local pattern=""
for ext in "${FILE_EXTENSIONS[@]}"; do
pattern="${pattern} -o -name *.${ext}"
done
pattern="${pattern# -o }" # Remove leading " -o "
find "$MOVIES_DIR" -type f \( $pattern \) | sort
}
get_relative_path() {
local file="$1"
echo "${file#$MOVIES_DIR/}"
}
create_archive_path() {
local relative_path="$1"
echo "$ARCHIVE_DIR/$relative_path"
}
create_work_path() {
local relative_path="$1"
local filename=$(basename "$relative_path")
local work_subdir="$WORK_DIR/$(dirname "$relative_path")"
mkdir -p "$work_subdir"
echo "$work_subdir/$filename"
}
reencode_file() {
local input_file="$1"
local output_file="$2"
log "Encoding: $input_file"
log " Output: $output_file"
log " Codec: $VIDEO_CODEC, Preset: $VIDEO_PRESET, CRF: $VIDEO_CRF"
# FFmpeg command:
# -i: input file
# -map 0:v: map all video streams
# -map 0:a: map all audio streams
# -c:v: video codec
# -preset: encoding preset
# -crf: quality setting (lower = better)
# -c:a copy: copy audio without re-encoding
# -sn: strip all subtitle streams
# -movflags +faststart: optimize for streaming (mp4)
# -n: never overwrite output file
ffmpeg -hide_banner -loglevel warning -stats \
-i "$input_file" \
-map 0:v -map 0:a \
-c:v "$VIDEO_CODEC" \
-preset "$VIDEO_PRESET" \
-crf "$VIDEO_CRF" \
-c:a copy \
-sn \
-movflags +faststart \
-n \
"$output_file" 2>&1 | tee -a "$LOG_FILE"
return ${PIPESTATUS[0]}
}
verify_output() {
local output_file="$1"
if [[ ! -f "$output_file" ]]; then
log_error "Output file does not exist: $output_file"
return 1
fi
local filesize=$(stat -c%s "$output_file" 2>/dev/null || stat -f%z "$output_file" 2>/dev/null)
if [[ $filesize -lt 1024 ]]; then
log_error "Output file is suspiciously small: $output_file ($filesize bytes)"
return 1
fi
# Verify with ffprobe
if ! ffprobe -v error "$output_file" &>/dev/null; then
log_error "Output file failed ffprobe validation: $output_file"
return 1
fi
log "Output file verified: $output_file ($filesize bytes)"
return 0
}
process_file() {
local input_file="$1"
local relative_path=$(get_relative_path "$input_file")
local archive_path=$(create_archive_path "$relative_path")
local work_path=$(create_work_path "$relative_path")
log "=========================================="
log "Processing: $relative_path"
if [[ $DRY_RUN -eq 1 ]]; then
log "[DRY RUN] Would encode: $input_file -> $work_path"
log "[DRY RUN] Would archive: $input_file -> $archive_path"
log "[DRY RUN] Would move: $work_path -> $input_file"
return 0
fi
# Step 1: Re-encode to work directory
if ! reencode_file "$input_file" "$work_path"; then
log_error "Encoding failed for: $input_file"
rm -f "$work_path"
return 1
fi
# Step 2: Verify output
if ! verify_output "$work_path"; then
log_error "Verification failed for: $work_path"
rm -f "$work_path"
return 1
fi
# Step 3: Archive original
local archive_dir=$(dirname "$archive_path")
mkdir -p "$archive_dir"
if ! mv "$input_file" "$archive_path"; then
log_error "Failed to archive original: $input_file"
rm -f "$work_path"
return 1
fi
log "Archived original: $archive_path"
# Step 4: Move new file into place
if ! mv "$work_path" "$input_file"; then
log_error "Failed to move encoded file into place: $work_path -> $input_file"
log_error "Original is in archive: $archive_path"
return 1
fi
log_success "Completed: $relative_path"
return 0
}
################################################################################
# MAIN EXECUTION
################################################################################
cleanup() {
log "Script interrupted or completed"
}
trap cleanup EXIT
main() {
log "=========================================="
log "ENCODERPRO - PHASE 1"
log "=========================================="
log "Started at: $(date)"
if [[ $DRY_RUN -eq 1 ]]; then
log "DRY RUN MODE ENABLED"
fi
# Validate environment
validate_dependencies
validate_directories
# Build file list
log "Scanning for media files..."
local file_count=0
local success_count=0
local failure_count=0
while IFS= read -r file; do
((file_count++)) || true
if process_file "$file"; then
((success_count++)) || true
else
((failure_count++)) || true
log_error "Failed to process: $file"
fi
done < <(build_file_list)
# Summary
log "=========================================="
log "PROCESSING COMPLETE"
log "=========================================="
log "Total files found: $file_count"
log "Successfully processed: $success_count"
log "Failed: $failure_count"
log "Completed at: $(date)"
}
# Run main function
main "$@"