#!/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 ""