296 lines
8.3 KiB
Bash
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 "$@"
|