521 lines
21 KiB
Bash
Executable File
521 lines
21 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Script to create compilation videos from GIF directories
|
|
# Creates horizontal (1920x1080) and vertical (1080x1920) compilations
|
|
# Uses NVIDIA NVENC GPU acceleration for fast encoding
|
|
|
|
# Enable strict error handling
|
|
set -o pipefail # Catch failures in piped commands
|
|
|
|
# Use environment variable if set (for Docker), otherwise use default path
|
|
GIFS_DIR="${GIFS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/gifs}"
|
|
OUTPUT_DIR="${INBOX_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/inbox}"
|
|
|
|
# Directory exclusion (colon-separated list of directory names to exclude)
|
|
EXCLUDED_DIRS="${EXCLUDED_DIRS:-}"
|
|
|
|
# Check if a directory should be excluded
|
|
is_excluded() {
|
|
local dir_name="$1"
|
|
if [[ -z "$EXCLUDED_DIRS" ]]; then
|
|
return 1 # Not excluded
|
|
fi
|
|
IFS=':' read -ra DIRS <<< "$EXCLUDED_DIRS"
|
|
for excluded in "${DIRS[@]}"; do
|
|
if [[ "$dir_name" == "$excluded" ]]; then
|
|
return 0 # Is excluded
|
|
fi
|
|
done
|
|
return 1 # Not excluded
|
|
}
|
|
|
|
cd "$GIFS_DIR" || exit 1
|
|
|
|
# Get current date in DD-MM-YYYY format
|
|
CURRENT_DATE=$(date +%d-%m-%Y)
|
|
|
|
# Get video duration in seconds using ffprobe
|
|
get_duration_seconds() {
|
|
local file="$1"
|
|
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null | cut -d. -f1
|
|
}
|
|
|
|
# Format duration as H:MM or Xs depending on length
|
|
format_duration() {
|
|
local seconds="$1"
|
|
if [[ "$seconds" -lt 60 ]]; then
|
|
echo "${seconds}s"
|
|
elif [[ "$seconds" -lt 3600 ]]; then
|
|
local minutes=$((seconds / 60))
|
|
echo "0:$(printf "%02d" $minutes)"
|
|
else
|
|
local hours=$((seconds / 3600))
|
|
local minutes=$(((seconds % 3600) / 60))
|
|
printf "%d:%02d" "$hours" "$minutes"
|
|
fi
|
|
}
|
|
|
|
# Check if ffmpeg is available
|
|
if ! command -v ffmpeg &> /dev/null; then
|
|
echo "Error: ffmpeg is not installed. Please install ffmpeg."
|
|
exit 1
|
|
fi
|
|
|
|
# Check if NVENC is available
|
|
if ! ffmpeg -hide_banner -encoders 2>/dev/null | grep -q h264_nvenc; then
|
|
echo "Warning: NVENC encoder not available. Falling back to CPU encoding."
|
|
ENCODER="libx264"
|
|
ENCODER_OPTS="-preset medium -crf 23"
|
|
else
|
|
# Check if NVENC encoder is currently in use (e.g., by Steam)
|
|
enc_usage=$(nvidia-smi dmon -c 1 2>/dev/null | tail -1 | awk '{print $7}')
|
|
if [[ "$enc_usage" -gt 50 ]]; then
|
|
if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then
|
|
echo "GPU encoder in use (${enc_usage}%), proceeding anyway in non-interactive mode"
|
|
ENCODER="h264_nvenc"
|
|
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0"
|
|
else
|
|
echo ""
|
|
echo "⚠️ WARNING: GPU encoder is currently in use (${enc_usage}% utilization)"
|
|
echo " This is likely Steam or another application using NVENC."
|
|
echo ""
|
|
echo " Options:"
|
|
echo " 1) Close Steam/other apps and press Enter to continue with GPU"
|
|
echo " 2) Type 'cpu' to use CPU encoding instead (slower)"
|
|
echo " 3) Press Ctrl+C to cancel"
|
|
echo ""
|
|
read -p "Choice: " enc_choice
|
|
|
|
if [[ "$enc_choice" == "cpu" ]]; then
|
|
echo "Using CPU encoding..."
|
|
ENCODER="libx264"
|
|
ENCODER_OPTS="-preset medium -crf 23"
|
|
else
|
|
# Re-check encoder usage
|
|
enc_usage=$(nvidia-smi dmon -c 1 2>/dev/null | tail -1 | awk '{print $7}')
|
|
if [[ "$enc_usage" -gt 50 ]]; then
|
|
echo "⚠️ Encoder still in use (${enc_usage}%). Will try GPU but may fall back to CPU."
|
|
else
|
|
echo "✓ GPU encoder now available"
|
|
fi
|
|
ENCODER="h264_nvenc"
|
|
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0"
|
|
fi
|
|
fi
|
|
else
|
|
echo "✓ NVENC GPU encoder detected and available"
|
|
ENCODER="h264_nvenc"
|
|
# Using -gpu 0 to explicitly set GPU, -rc vbr for variable bitrate
|
|
# Adding -b_ref_mode 0 to disable B-frame references (fixes issues on some GPUs)
|
|
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "========================================="
|
|
echo " GIF Compilation Generator"
|
|
echo " Encoder: $ENCODER"
|
|
echo "========================================="
|
|
echo ""
|
|
|
|
# Log excluded directories if any
|
|
if [[ -n "$EXCLUDED_DIRS" ]]; then
|
|
echo "Excluding directories: $EXCLUDED_DIRS"
|
|
echo ""
|
|
fi
|
|
|
|
# Build list of available creators
|
|
declare -a creator_list
|
|
creator_index=0
|
|
for creator_dir in */; do
|
|
if [[ -d "$creator_dir" ]]; then
|
|
creator_name="${creator_dir%/}"
|
|
# Skip excluded directories
|
|
if is_excluded "$creator_name"; then
|
|
continue
|
|
fi
|
|
if [[ -d "$creator_name/horizontal" ]] || [[ -d "$creator_name/vertical" ]]; then
|
|
creator_list[$creator_index]="$creator_name"
|
|
((creator_index++))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
total_creators=${#creator_list[@]}
|
|
echo "Found $total_creators creators available"
|
|
echo ""
|
|
|
|
# Non-interactive mode: use CREATOR_SELECTION from environment
|
|
selected_creators=()
|
|
|
|
if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then
|
|
if [[ "$CREATOR_SELECTION" == "all" ]] || [[ -z "$CREATOR_SELECTION" ]]; then
|
|
selected_creators=("${creator_list[@]}")
|
|
echo "Non-interactive mode: Processing all ${#selected_creators[@]} creators"
|
|
else
|
|
# Check if specified creator exists
|
|
found=0
|
|
for creator in "${creator_list[@]}"; do
|
|
if [[ "$creator" == "$CREATOR_SELECTION" ]]; then
|
|
selected_creators=("$creator")
|
|
found=1
|
|
break
|
|
fi
|
|
done
|
|
if [[ $found -eq 0 ]]; then
|
|
echo "Error: Creator '$CREATOR_SELECTION' not found"
|
|
exit 1
|
|
fi
|
|
echo "Non-interactive mode: Processing only '$CREATOR_SELECTION'"
|
|
fi
|
|
else
|
|
# Interactive menu to select creator(s)
|
|
echo "Select processing mode:"
|
|
echo " 1) Process ALL creators (default)"
|
|
echo " 2) Select specific creator"
|
|
echo ""
|
|
read -p "Enter choice [1-2] (default: 1): " choice
|
|
echo ""
|
|
|
|
if [[ "$choice" == "2" ]]; then
|
|
# Show creator selection menu
|
|
echo "Available creators:"
|
|
for i in "${!creator_list[@]}"; do
|
|
printf " %2d) %s\n" "$((i+1))" "${creator_list[$i]}"
|
|
done
|
|
echo ""
|
|
read -p "Enter creator number [1-$total_creators]: " creator_num
|
|
|
|
if [[ "$creator_num" =~ ^[0-9]+$ ]] && [[ $creator_num -ge 1 ]] && [[ $creator_num -le $total_creators ]]; then
|
|
selected_index=$((creator_num - 1))
|
|
selected_creators=("${creator_list[$selected_index]}")
|
|
echo ""
|
|
echo "Processing only: ${selected_creators[0]}"
|
|
else
|
|
echo "Invalid selection. Processing all creators."
|
|
selected_creators=("${creator_list[@]}")
|
|
fi
|
|
else
|
|
# Process all creators
|
|
selected_creators=("${creator_list[@]}")
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "========================================="
|
|
echo " Processing ${#selected_creators[@]} creator(s)"
|
|
echo "========================================="
|
|
echo ""
|
|
|
|
# Track progress
|
|
current_creator=0
|
|
total_compilations_created=0
|
|
total_compilations_skipped=0
|
|
start_time=$(date +%s)
|
|
|
|
# Function to show progress bar
|
|
show_progress() {
|
|
local current=$1
|
|
local total=$2
|
|
local width=40
|
|
local percentage=$((current * 100 / total))
|
|
local filled=$((width * current / total))
|
|
local empty=$((width - filled))
|
|
|
|
printf "\rOverall Progress: ["
|
|
printf "%${filled}s" | tr ' ' '█'
|
|
printf "%${empty}s" | tr ' ' '░'
|
|
printf "] %3d%% (%d/%d)" "$percentage" "$current" "$total"
|
|
}
|
|
|
|
# Process selected creators
|
|
total_to_process=${#selected_creators[@]}
|
|
for creator_name in "${selected_creators[@]}"; do
|
|
((current_creator++))
|
|
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "[$current_creator/$total_to_process] Processing: $creator_name"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
# Process horizontal compilation
|
|
if [[ -d "$creator_name/horizontal" ]]; then
|
|
compilation_file="$OUTPUT_DIR/${creator_name} - Compilation [H].mp4"
|
|
|
|
# Check if compilation already exists (with or without metadata tags)
|
|
existing_compilation=$(find "$OUTPUT_DIR" -maxdepth 1 -type f -name "${creator_name} - Compilation*\[H\].mp4" 2>/dev/null | head -1)
|
|
|
|
if [[ -z "$existing_compilation" ]]; then
|
|
echo ""
|
|
echo "📹 Creating horizontal compilation..."
|
|
|
|
# Count MP4 files
|
|
file_count=$(find "$creator_name/horizontal" -maxdepth 1 -type f -name "*.mp4" | wc -l)
|
|
|
|
if [[ $file_count -gt 0 ]]; then
|
|
# Create file list for ffmpeg concat
|
|
temp_list=$(mktemp)
|
|
|
|
# Randomize files and add to list
|
|
while IFS= read -r -d '' file; do
|
|
# Escape single quotes in filename for ffmpeg
|
|
escaped_file=$(echo "$(realpath "$file")" | sed "s/'/'\\\\\''/g")
|
|
echo "file '$escaped_file'" >> "$temp_list"
|
|
done < <(find "$creator_name/horizontal" -maxdepth 1 -type f -name "*.mp4" -print0 | shuf -z)
|
|
|
|
echo " → Input: $file_count videos (horizontal 1920x1080)"
|
|
echo " → Output: $compilation_file"
|
|
echo " → Encoding with $ENCODER..."
|
|
echo ""
|
|
|
|
# Create compilation with NVENC GPU encoding
|
|
# Using h264_nvenc with P4 preset (balanced) and CQ 23 (quality)
|
|
# Scale filter maintains aspect ratio and pads to 1920x1080
|
|
# NOTE: Using concat protocol instead of demuxer to avoid NVENC segfaults
|
|
|
|
# First, re-encode all videos to intermediate format (NVENC-friendly)
|
|
temp_dir=$(mktemp -d)
|
|
file_index=0
|
|
|
|
echo " → Encoding individual clips..."
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^file\ \'(.*)\'$ ]]; then
|
|
input_file="${BASH_REMATCH[1]}"
|
|
intermediate_file="$temp_dir/$(printf "%05d" $file_index).ts"
|
|
|
|
printf "\r → Processing clip %d/%d" "$((file_index + 1))" "$file_count"
|
|
|
|
# Try NVENC encoding (software decode, GPU encode)
|
|
# Capture stderr to check for NVENC failures
|
|
nvenc_output=$(ffmpeg -nostdin -i "$input_file" \
|
|
-c:v $ENCODER $ENCODER_OPTS \
|
|
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,fps=30" \
|
|
-c:a aac -b:a 128k \
|
|
-f mpegts \
|
|
-y "$intermediate_file" 2>&1)
|
|
nvenc_exit=$?
|
|
|
|
# Check if NVENC failed (exit code or error messages)
|
|
if [[ $nvenc_exit -ne 0 ]] || [[ "$nvenc_output" == *"No capable devices"* ]] || [[ "$nvenc_output" == *"CUDA_ERROR"* ]]; then
|
|
# NVENC failed, fall back to CPU encoding
|
|
ffmpeg -nostdin -i "$input_file" \
|
|
-c:v libx264 -preset fast -crf 23 \
|
|
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,fps=30" \
|
|
-c:a aac -b:a 128k \
|
|
-f mpegts \
|
|
-loglevel error \
|
|
-y "$intermediate_file" 2>&1
|
|
fi
|
|
|
|
# Small delay to let GPU resources release between clips
|
|
sleep 0.1
|
|
|
|
((file_index++))
|
|
fi
|
|
done < "$temp_list"
|
|
echo "" # New line after progress
|
|
|
|
# Concatenate using concat demuxer with list file (no re-encoding)
|
|
echo " → Merging clips..."
|
|
concat_list_file="$temp_dir/concat_list.txt"
|
|
|
|
# Create concat list file
|
|
for ts_file in "$temp_dir"/*.ts; do
|
|
echo "file '$(realpath "$ts_file")'" >> "$concat_list_file"
|
|
done
|
|
|
|
ffmpeg -f concat -safe 0 -i "$concat_list_file" \
|
|
-c copy \
|
|
-movflags +faststart \
|
|
-loglevel error \
|
|
-y "$compilation_file" 2>&1
|
|
|
|
ffmpeg_exit=$?
|
|
|
|
# Cleanup intermediate files
|
|
rm -rf "$temp_dir"
|
|
rm "$temp_list"
|
|
|
|
if [[ $ffmpeg_exit -eq 0 ]] && [[ -f "$compilation_file" ]]; then
|
|
file_size=$(stat -c%s "$compilation_file")
|
|
# Check if file is at least 1MB (files <1MB are likely corrupted)
|
|
if [[ $file_size -gt 1048576 ]]; then
|
|
# Get duration and create proper tagged filename
|
|
duration_seconds=$(get_duration_seconds "$compilation_file")
|
|
duration_formatted=$(format_duration "$duration_seconds")
|
|
final_filename="$OUTPUT_DIR/${creator_name} - Compilation [${CURRENT_DATE}] [${duration_formatted}] [1080p] [E] [H].mp4"
|
|
|
|
# Rename to final filename with tags
|
|
mv "$compilation_file" "$final_filename"
|
|
|
|
file_size_human=$(du -h "$final_filename" | cut -f1)
|
|
echo " ✓ Success! Created $(basename "$final_filename") ($file_size_human)"
|
|
((total_compilations_created++))
|
|
else
|
|
echo " ✗ Failed: File too small ($file_size bytes), likely corrupted"
|
|
rm -f "$compilation_file"
|
|
fi
|
|
else
|
|
echo " ✗ Failed to create horizontal compilation (exit code: $ffmpeg_exit)"
|
|
fi
|
|
else
|
|
echo " ⚠ No MP4 files found in horizontal directory"
|
|
fi
|
|
else
|
|
echo " ✓ Horizontal compilation already exists, skipping"
|
|
((total_compilations_skipped++))
|
|
fi
|
|
fi
|
|
|
|
# Process vertical compilation
|
|
if [[ -d "$creator_name/vertical" ]]; then
|
|
compilation_file="$OUTPUT_DIR/${creator_name} - Compilation [V].mp4"
|
|
|
|
# Check if compilation already exists (with or without metadata tags)
|
|
existing_compilation=$(find "$OUTPUT_DIR" -maxdepth 1 -type f -name "${creator_name} - Compilation*\[V\].mp4" 2>/dev/null | head -1)
|
|
|
|
if [[ -z "$existing_compilation" ]]; then
|
|
echo ""
|
|
echo "📹 Creating vertical compilation..."
|
|
|
|
# Count MP4 files
|
|
file_count=$(find "$creator_name/vertical" -maxdepth 1 -type f -name "*.mp4" | wc -l)
|
|
|
|
if [[ $file_count -gt 0 ]]; then
|
|
# Create file list for ffmpeg concat
|
|
temp_list=$(mktemp)
|
|
|
|
# Randomize files and add to list
|
|
while IFS= read -r -d '' file; do
|
|
# Escape single quotes in filename for ffmpeg
|
|
escaped_file=$(echo "$(realpath "$file")" | sed "s/'/'\\\\\''/g")
|
|
echo "file '$escaped_file'" >> "$temp_list"
|
|
done < <(find "$creator_name/vertical" -maxdepth 1 -type f -name "*.mp4" -print0 | shuf -z)
|
|
|
|
echo " → Input: $file_count videos (vertical 1080x1920)"
|
|
echo " → Output: $compilation_file"
|
|
echo " → Encoding with $ENCODER..."
|
|
echo ""
|
|
|
|
# Create compilation with NVENC GPU encoding
|
|
# Scale filter maintains aspect ratio and pads to 1080x1920
|
|
# NOTE: Using concat protocol instead of demuxer to avoid NVENC segfaults
|
|
|
|
# First, re-encode all videos to intermediate format (NVENC-friendly)
|
|
temp_dir=$(mktemp -d)
|
|
file_index=0
|
|
|
|
echo " → Encoding individual clips..."
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^file\ \'(.*)\'$ ]]; then
|
|
input_file="${BASH_REMATCH[1]}"
|
|
intermediate_file="$temp_dir/$(printf "%05d" $file_index).ts"
|
|
|
|
printf "\r → Processing clip %d/%d" "$((file_index + 1))" "$file_count"
|
|
|
|
# Try NVENC encoding (software decode, GPU encode)
|
|
# Capture stderr to check for NVENC failures
|
|
nvenc_output=$(ffmpeg -nostdin -i "$input_file" \
|
|
-c:v $ENCODER $ENCODER_OPTS \
|
|
-vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,fps=30" \
|
|
-c:a aac -b:a 128k \
|
|
-f mpegts \
|
|
-y "$intermediate_file" 2>&1)
|
|
nvenc_exit=$?
|
|
|
|
# Check if NVENC failed (exit code or error messages)
|
|
if [[ $nvenc_exit -ne 0 ]] || [[ "$nvenc_output" == *"No capable devices"* ]] || [[ "$nvenc_output" == *"CUDA_ERROR"* ]]; then
|
|
# NVENC failed, fall back to CPU encoding
|
|
ffmpeg -nostdin -i "$input_file" \
|
|
-c:v libx264 -preset fast -crf 23 \
|
|
-vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,fps=30" \
|
|
-c:a aac -b:a 128k \
|
|
-f mpegts \
|
|
-loglevel error \
|
|
-y "$intermediate_file" 2>&1
|
|
fi
|
|
|
|
# Small delay to let GPU resources release between clips
|
|
sleep 0.1
|
|
|
|
((file_index++))
|
|
fi
|
|
done < "$temp_list"
|
|
echo "" # New line after progress
|
|
|
|
# Concatenate using concat demuxer with list file (no re-encoding)
|
|
echo " → Merging clips..."
|
|
concat_list_file="$temp_dir/concat_list.txt"
|
|
|
|
# Create concat list file
|
|
for ts_file in "$temp_dir"/*.ts; do
|
|
echo "file '$(realpath "$ts_file")'" >> "$concat_list_file"
|
|
done
|
|
|
|
ffmpeg -f concat -safe 0 -i "$concat_list_file" \
|
|
-c copy \
|
|
-movflags +faststart \
|
|
-loglevel error \
|
|
-y "$compilation_file" 2>&1
|
|
|
|
ffmpeg_exit=$?
|
|
|
|
# Cleanup intermediate files
|
|
rm -rf "$temp_dir"
|
|
rm "$temp_list"
|
|
|
|
if [[ $ffmpeg_exit -eq 0 ]] && [[ -f "$compilation_file" ]]; then
|
|
file_size=$(stat -c%s "$compilation_file")
|
|
# Check if file is at least 1MB (files <1MB are likely corrupted)
|
|
if [[ $file_size -gt 1048576 ]]; then
|
|
# Get duration and create proper tagged filename
|
|
duration_seconds=$(get_duration_seconds "$compilation_file")
|
|
duration_formatted=$(format_duration "$duration_seconds")
|
|
final_filename="$OUTPUT_DIR/${creator_name} - Compilation [${CURRENT_DATE}] [${duration_formatted}] [1080p] [E] [V].mp4"
|
|
|
|
# Rename to final filename with tags
|
|
mv "$compilation_file" "$final_filename"
|
|
|
|
file_size_human=$(du -h "$final_filename" | cut -f1)
|
|
echo " ✓ Success! Created $(basename "$final_filename") ($file_size_human)"
|
|
((total_compilations_created++))
|
|
else
|
|
echo " ✗ Failed: File too small ($file_size bytes), likely corrupted"
|
|
rm -f "$compilation_file"
|
|
fi
|
|
else
|
|
echo " ✗ Failed to create vertical compilation (exit code: $ffmpeg_exit)"
|
|
fi
|
|
else
|
|
echo " ⚠ No MP4 files found in vertical directory"
|
|
fi
|
|
else
|
|
echo " ✓ Vertical compilation already exists, skipping"
|
|
((total_compilations_skipped++))
|
|
fi
|
|
fi
|
|
|
|
# Show overall progress
|
|
if [[ $total_to_process -gt 0 ]]; then
|
|
show_progress "$current_creator" "$total_to_process"
|
|
fi
|
|
done
|
|
|
|
# Calculate elapsed time
|
|
end_time=$(date +%s)
|
|
elapsed=$((end_time - start_time))
|
|
hours=$((elapsed / 3600))
|
|
minutes=$(((elapsed % 3600) / 60))
|
|
seconds=$((elapsed % 60))
|
|
|
|
echo ""
|
|
echo ""
|
|
echo "========================================="
|
|
echo " Compilation Complete!"
|
|
echo "========================================="
|
|
echo " Created: $total_compilations_created compilations"
|
|
echo " Skipped: $total_compilations_skipped (already exist)"
|
|
echo " Time elapsed: ${hours}h ${minutes}m ${seconds}s"
|
|
echo "========================================="
|
|
echo ""
|