#!/bin/bash # ============================================================================ # MEGA COMPILATION CREATOR # ============================================================================ # Creates a 4K compilation with: # - Three vertical video columns (longer library videos + gif padding) # - Center horizontal overlay with crossfade transitions # - Smart duration filling to reach target length # # Canvas: 3840x2160 (4K) # Column positions: Left (150,120), Center (1380,120), Right (2610,120) # Middle overlay: Centered, ~600px wide # ============================================================================ # Don't exit on error - we'll handle errors explicitly # set -e # Use environment variables if set (for Docker), otherwise use default paths GIFS_DIR="${GIFS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/gifs}" LIBRARY_DIR="${SOURCE_LIBRARY:-/media/bunker-admin/Internal/plex/xxx/media/local/studios}" 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:-}" # Filter function for excluding directories (handles null-terminated input) filter_excluded_null() { if [[ -z "$EXCLUDED_DIRS" ]]; then cat # Pass through unchanged else local pattern pattern=$(echo "$EXCLUDED_DIRS" | sed 's/:/|/g') grep -zEv "/($pattern)/" fi } # Filter function for excluding directories (handles newline-terminated input) filter_excluded() { if [[ -z "$EXCLUDED_DIRS" ]]; then cat # Pass through unchanged else local pattern pattern=$(echo "$EXCLUDED_DIRS" | sed 's/:/|/g') grep -Ev "/($pattern)/" fi } # Column dimensions and positions COLUMN_WIDTH=1080 COLUMN_HEIGHT=1920 COL_LEFT_X=150 COL_CENTER_X=1380 COL_RIGHT_X=2610 COL_Y=120 # Middle overlay settings MIDDLE_WIDTH=600 CROSSFADE_DURATION=1 # seconds # Canvas CANVAS_WIDTH=3840 CANVAS_HEIGHT=2160 # ============================================================================ # UTILITY FUNCTIONS # ============================================================================ # Check dependencies check_dependencies() { local missing=() if ! command -v ffmpeg &> /dev/null; then missing+=("ffmpeg") fi if ! command -v ffprobe &> /dev/null; then missing+=("ffprobe") fi if [[ ${#missing[@]} -gt 0 ]]; then echo "Error: Missing required tools: ${missing[*]}" exit 1 fi } # Setup encoder (NVENC or fallback to CPU) setup_encoder() { if ! ffmpeg -hide_banner -encoders 2>/dev/null | grep -q h264_nvenc; then echo "Warning: NVENC encoder not available. Using CPU encoding." ENCODER="libx264" ENCODER_OPTS="-preset medium -crf 23" else local 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 "" echo " Options:" echo " 1) Close 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 ENCODER="libx264" ENCODER_OPTS="-preset medium -crf 23" else 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" ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0" fi fi } # Extract duration from filename tag [H:MM] or [Xs] - returns seconds get_duration_from_filename() { local filename="$1" local basename=$(basename "$filename") # Try [H:MM] format first (e.g., [1:23] = 1 hour 23 minutes) if [[ "$basename" =~ \[([0-9]+):([0-9]{2})\] ]]; then local hours="${BASH_REMATCH[1]}" local minutes="${BASH_REMATCH[2]}" # Use 10# prefix to force base-10 (avoids octal issues with 08, 09) echo $(( 10#$hours * 3600 + 10#$minutes * 60 )) return 0 fi # Try [Xs] format (e.g., [45s] = 45 seconds) if [[ "$basename" =~ \[([0-9]+)s\] ]]; then # Use 10# prefix to force base-10 echo $(( 10#${BASH_REMATCH[1]} )) return 0 fi # Fallback: probe the file ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$filename" 2>/dev/null | cut -d. -f1 } # Get actual duration using ffprobe get_duration_ffprobe() { 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 seconds as H:MM format_duration() { local seconds="$1" local hours=$((seconds / 3600)) local minutes=$(((seconds % 3600) / 60)) printf "%d:%02d" "$hours" "$minutes" } # Check if video has audio stream has_audio() { local file="$1" local audio_streams=$(ffprobe -v error -select_streams a -show_entries stream=index -of csv=p=0 "$file" 2>/dev/null) [[ -n "$audio_streams" ]] } # Validate video file is not corrupted (quick check) is_valid_video() { local file="$1" # Check if ffprobe can read the file (timeout after 5 seconds) if ! timeout 5 ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of csv=p=0 "$file" &>/dev/null; then return 1 fi # Try to decode first few frames (timeout after 10 seconds) if ! timeout 10 ffmpeg -nostdin -v error -i "$file" -vframes 5 -f null - 2>/dev/null; then return 1 fi return 0 } # Show progress bar show_progress() { local current=$1 local total=$2 local label=$3 local width=40 local percentage=$((current * 100 / total)) local filled=$((width * current / total)) local empty=$((width - filled)) printf "\r → %s %d/%d [" "$label" "$current" "$total" printf "%${filled}s" | tr ' ' '█' printf "%${empty}s" | tr ' ' '░' printf "] %3d%%" "$percentage" } # ============================================================================ # VIDEO INVENTORY FUNCTIONS # ============================================================================ # Find all vertical [V] videos from the main library (not gifs) find_library_vertical_videos() { local -n result_array=$1 local skipped=0 local checked=0 echo " → Scanning library for vertical [V] videos..." # Find all MP4 files with [V] tag, excluding gifs directory while IFS= read -r -d '' file; do ((checked++)) printf "\r → Checking file %d..." "$checked" # Skip if no audio if ! has_audio "$file"; then ((skipped++)) continue fi # Validate video is not corrupted if ! is_valid_video "$file"; then echo "" echo " ⚠ Skipping corrupted: $(basename "$file")" ((skipped++)) continue fi local duration=$(get_duration_from_filename "$file") if [[ -n "$duration" ]] && [[ "$duration" -gt 0 ]]; then result_array+=("$duration:$file") fi done < <(find "$LIBRARY_DIR" -type f -name "*\[V\].mp4" ! -path "*/gifs/*" ! -path "*/scripts/*" -print0 2>/dev/null | filter_excluded_null) echo "" echo " → Found ${#result_array[@]} valid vertical videos in library (skipped $skipped)" } # Find all vertical gif videos find_gif_vertical_videos() { local -n result_array=$1 local skipped=0 local checked=0 local total_files=$(find "$GIFS_DIR" -path "*/vertical/*.mp4" -type f 2>/dev/null | filter_excluded | wc -l) echo " → Scanning for vertical gif content ($total_files files)..." while IFS= read -r -d '' file; do ((checked++)) printf "\r → Validating gif %d/%d..." "$checked" "$total_files" if ! has_audio "$file"; then ((skipped++)) continue fi # Quick validation (less thorough for gifs since there are many) if ! timeout 3 ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "$file" &>/dev/null; then ((skipped++)) continue fi local duration=$(get_duration_from_filename "$file") if [[ -n "$duration" ]] && [[ "$duration" -gt 0 ]]; then result_array+=("$duration:$file") fi done < <(find "$GIFS_DIR" -path "*/vertical/*.mp4" -type f -print0 2>/dev/null | filter_excluded_null) echo "" echo " → Found ${#result_array[@]} valid vertical gif clips (skipped $skipped)" } # Find horizontal videos in a specific creator folder find_horizontal_videos() { local creator_dir="$1" local -n result_array=$2 local skipped=0 while IFS= read -r -d '' file; do if ! has_audio "$file"; then ((skipped++)) continue fi # Quick validation if ! timeout 3 ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "$file" &>/dev/null; then ((skipped++)) continue fi result_array+=("$file") done < <(find "$creator_dir/horizontal" -type f -name "*.mp4" -print0 2>/dev/null) } # ============================================================================ # VIDEO SELECTION ALGORITHM # ============================================================================ # File to track used videos across columns (persists across subshells) USED_VIDEOS_FILE="" # Check if video is already used is_video_used() { local filepath="$1" if [[ -f "$USED_VIDEOS_FILE" ]] && [[ -s "$USED_VIDEOS_FILE" ]]; then grep -qF "$filepath" "$USED_VIDEOS_FILE" return $? fi return 1 } # Mark video as used mark_video_used() { local filepath="$1" echo "$filepath" >> "$USED_VIDEOS_FILE" sync # Ensure write is flushed to disk for subshells } # Select videos to fill a target duration for one column # Returns newline-separated list of video paths # Ensures ~50/50 split between library and gif content, interleaved select_videos_for_column() { local target_seconds=$1 local -n library_videos=$2 local -n gif_videos=$3 local column_name=$4 local library_selected=() local gif_selected=() local library_duration=0 local gif_duration=0 local min_gap=30 # Target ~50% for each type local library_target=$((target_seconds / 2)) local gif_target=$((target_seconds / 2)) # Shuffle both pools local shuffled_library=() while IFS= read -r line; do [[ -n "$line" ]] && shuffled_library+=("$line") done < <(printf '%s\n' "${library_videos[@]}" | shuf) local shuffled_gifs=() while IFS= read -r line; do [[ -n "$line" ]] && shuffled_gifs+=("$line") done < <(printf '%s\n' "${gif_videos[@]}" | shuf) # Select library videos (up to ~50% of target) for entry in "${shuffled_library[@]}"; do local duration="${entry%%:*}" local filepath="${entry#*:}" if is_video_used "$filepath"; then continue fi local remaining=$((library_target - library_duration)) if [[ "$remaining" -lt "$min_gap" ]]; then break fi if [[ "$duration" -le "$remaining" ]]; then library_selected+=("$duration:$filepath") mark_video_used "$filepath" library_duration=$((library_duration + duration)) fi done # Select gif videos (up to ~50% of target, or more if library didn't fill) local gif_remaining_target=$((target_seconds - library_duration)) for entry in "${shuffled_gifs[@]}"; do local duration="${entry%%:*}" local filepath="${entry#*:}" if is_video_used "$filepath"; then continue fi local remaining=$((gif_remaining_target - gif_duration)) if [[ "$remaining" -lt "$min_gap" ]]; then break fi if [[ "$duration" -le "$remaining" ]]; then gif_selected+=("$duration:$filepath") mark_video_used "$filepath" gif_duration=$((gif_duration + duration)) fi done # Now interleave: alternate library video, then gif(s), then library, etc. local final_videos=() local lib_idx=0 local gif_idx=0 local lib_count=${#library_selected[@]} local gif_count=${#gif_selected[@]} # Calculate how many gifs to insert between each library video local gifs_per_gap=1 if [[ $lib_count -gt 0 ]]; then gifs_per_gap=$(( (gif_count + lib_count - 1) / lib_count )) [[ $gifs_per_gap -lt 1 ]] && gifs_per_gap=1 fi while [[ $lib_idx -lt $lib_count ]] || [[ $gif_idx -lt $gif_count ]]; do # Add one library video if [[ $lib_idx -lt $lib_count ]]; then local entry="${library_selected[$lib_idx]}" final_videos+=("${entry#*:}") ((lib_idx++)) fi # Add some gifs after the library video local gifs_added=0 while [[ $gif_idx -lt $gif_count ]] && [[ $gifs_added -lt $gifs_per_gap ]]; do local entry="${gif_selected[$gif_idx]}" final_videos+=("${entry#*:}") ((gif_idx++)) ((gifs_added++)) done done local total_duration=$((library_duration + gif_duration)) local lib_pct=0 [[ $total_duration -gt 0 ]] && lib_pct=$((library_duration * 100 / total_duration)) echo " → Column $column_name: ${#final_videos[@]} videos, $(format_duration $total_duration) total (${lib_pct}% library, $((100-lib_pct))% gifs)" >&2 # Output the selected videos printf '%s\n' "${final_videos[@]}" } # ============================================================================ # VIDEO PROCESSING FUNCTIONS # ============================================================================ # Create a single column video from a list of video files # Uses FFmpeg concat FILTER (not demuxer) for perfect timestamp continuity create_column_video() { local output_file="$1" local target_duration="$2" shift 2 local video_files=("$@") if [[ ${#video_files[@]} -eq 0 ]]; then echo " Error: No videos provided for column" return 1 fi # Validate all input files exist local valid_files=() for video in "${video_files[@]}"; do if [[ -f "$video" ]]; then valid_files+=("$video") else echo " Warning: File not found: $video" fi done if [[ ${#valid_files[@]} -eq 0 ]]; then echo " Error: No valid video files found" return 1 fi local num_videos=${#valid_files[@]} echo " Building concat filter for $num_videos videos..." # Build FFmpeg inputs and filter_complex local inputs="" local filter_v="" local filter_a="" local concat_inputs_v="" local concat_inputs_a="" for i in "${!valid_files[@]}"; do local video="${valid_files[$i]}" inputs="$inputs -i \"$video\"" # Scale video, pad to column size, normalize fps/SAR, set pts # setsar=1 is critical - concat filter requires identical SAR across all inputs filter_v="$filter_v[$i:v]scale=${COLUMN_WIDTH}:${COLUMN_HEIGHT}:force_original_aspect_ratio=decrease,pad=${COLUMN_WIDTH}:${COLUMN_HEIGHT}:(ow-iw)/2:(oh-ih)/2,fps=30,setsar=1,setpts=PTS-STARTPTS[v$i];" # Normalize audio sample rate and set pts filter_a="$filter_a[$i:a]aresample=48000,asetpts=PTS-STARTPTS[a$i];" concat_inputs_v="$concat_inputs_v[v$i]" concat_inputs_a="$concat_inputs_a[a$i]" done # Build the concat filter local filter_complex="${filter_v}${filter_a}${concat_inputs_v}concat=n=${num_videos}:v=1:a=0[vout];${concat_inputs_a}concat=n=${num_videos}:v=0:a=1[aout]" echo " Encoding column (single-pass concat filter)..." # Execute FFmpeg with concat filter - single pass, no intermediate files local ffmpeg_cmd="ffmpeg -nostdin $inputs -filter_complex \"$filter_complex\" -map \"[vout]\" -map \"[aout]\" -c:v $ENCODER $ENCODER_OPTS -c:a aac -b:a 128k -ar 48000 -loglevel error -y \"$output_file\"" eval "$ffmpeg_cmd" 2>&1 local exit_code=$? if [[ $exit_code -ne 0 ]]; then echo " Error: FFmpeg concat filter failed (exit code: $exit_code)" return 1 fi # Verify output was created and has matching durations if [[ ! -f "$output_file" ]]; then echo " Error: Output file was not created" return 1 fi local video_dur=$(ffprobe -v error -select_streams v:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$output_file" 2>/dev/null | cut -d. -f1) local audio_dur=$(ffprobe -v error -select_streams a:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$output_file" 2>/dev/null | cut -d. -f1) if [[ -n "$video_dur" ]] && [[ -n "$audio_dur" ]]; then local diff=$((video_dur > audio_dur ? video_dur - audio_dur : audio_dur - video_dur)) if [[ $diff -gt 5 ]]; then echo " ⚠ Warning: Duration mismatch (video: ${video_dur}s, audio: ${audio_dur}s)" else echo " ✓ Durations match (${video_dur}s)" fi fi echo " Column video created successfully" } # Create the middle overlay video with crossfades create_middle_overlay() { local output_file="$1" local target_duration="$2" shift 2 local video_files=("$@") if [[ ${#video_files[@]} -eq 0 ]]; then echo "Error: No videos provided for middle overlay" return 1 fi # Calculate scaled height maintaining aspect ratio (1920x1080 -> 600x?) local scaled_height=$((MIDDLE_WIDTH * 1080 / 1920)) # 337.5 -> 338 local temp_dir=$(mktemp -d) # If only one video, just scale and loop it if [[ ${#video_files[@]} -eq 1 ]]; then ffmpeg -nostdin -stream_loop -1 -i "${video_files[0]}" \ -t "$target_duration" \ -vf "scale=${MIDDLE_WIDTH}:${scaled_height},fps=30" \ -c:v $ENCODER $ENCODER_OPTS \ -af "aresample=async=1" \ -c:a aac -b:a 128k -ar 48000 \ -loglevel error \ -y "$output_file" 2>/dev/null rm -rf "$temp_dir" return 0 fi # For multiple videos, create crossfade transitions # First, prepare all videos to same format local prepared_files=() local idx=0 for video in "${video_files[@]}"; do local prep_file="$temp_dir/prep_$(printf "%05d" $idx).mp4" ffmpeg -nostdin -i "$video" \ -vf "scale=${MIDDLE_WIDTH}:${scaled_height},fps=30,setpts=PTS-STARTPTS" \ -c:v $ENCODER $ENCODER_OPTS \ -af "aresample=async=1,asetpts=PTS-STARTPTS" \ -c:a aac -b:a 128k -ar 48000 \ -loglevel error \ -y "$prep_file" 2>/dev/null prepared_files+=("$prep_file") ((idx++)) done # Build xfade filter chain local num_videos=${#prepared_files[@]} local inputs="" local filter_complex="" local current_offset=0 for i in "${!prepared_files[@]}"; do inputs="$inputs -i \"${prepared_files[$i]}\"" done # Calculate offsets and build filter # Each video plays, then crossfades to next local durations=() for prep_file in "${prepared_files[@]}"; do local dur=$(get_duration_ffprobe "$prep_file") durations+=("$dur") done # Build the xfade chain if [[ $num_videos -eq 2 ]]; then local offset=$((${durations[0]} - CROSSFADE_DURATION)) filter_complex="[0:v][1:v]xfade=transition=fade:duration=${CROSSFADE_DURATION}:offset=${offset}[vout];[0:a][1:a]acrossfade=d=${CROSSFADE_DURATION}[aout]" else # For 3+ videos, chain the xfades local offset=$((${durations[0]} - CROSSFADE_DURATION)) filter_complex="[0:v][1:v]xfade=transition=fade:duration=${CROSSFADE_DURATION}:offset=${offset}[v1];[0:a][1:a]acrossfade=d=${CROSSFADE_DURATION}[a1]" for ((i=2; i/dev/null # Get duration of xfade output local xfade_duration=$(get_duration_ffprobe "$xfade_output") # Validate xfade output was created and has valid duration if [[ ! -f "$xfade_output" ]] || [[ -z "$xfade_duration" ]] || [[ "$xfade_duration" -eq 0 ]]; then echo " Error: xfade output failed or has zero duration" rm -rf "$temp_dir" return 1 fi # If we need more duration, loop the content if [[ "$xfade_duration" -lt "$target_duration" ]]; then local loop_count=$(( (target_duration / xfade_duration) + 1 )) ffmpeg -nostdin -stream_loop "$loop_count" -i "$xfade_output" \ -t "$target_duration" \ -c:v $ENCODER $ENCODER_OPTS \ -c:a aac -b:a 128k \ -loglevel error \ -y "$output_file" 2>/dev/null else # Trim to target duration ffmpeg -nostdin -i "$xfade_output" \ -t "$target_duration" \ -c copy \ -loglevel error \ -y "$output_file" 2>/dev/null fi rm -rf "$temp_dir" } # Final composite: combine columns and middle overlay create_final_composite() { local output_file="$1" local col_left="$2" local col_center="$3" local col_right="$4" local middle_overlay="$5" local target_duration="$6" # Calculate middle overlay position (centered) local middle_height=$((MIDDLE_WIDTH * 1080 / 1920)) local middle_x=$(( (CANVAS_WIDTH - MIDDLE_WIDTH) / 2 )) local middle_y=$(( (CANVAS_HEIGHT - middle_height) / 2 )) echo " → Compositing final video..." # Complex filter to: # 1. Create black canvas # 2. Overlay three columns # 3. Overlay middle section # 4. Mix all audio # Use eof_action=repeat to hold last frame if any input ends early # This prevents video freezing due to timestamp issues in inputs # The black canvas duration controls final video length local filter_complex=" color=black:s=${CANVAS_WIDTH}x${CANVAS_HEIGHT}:d=${target_duration}:r=30[base]; [0:v]setpts=PTS-STARTPTS[v0]; [1:v]setpts=PTS-STARTPTS[v1]; [2:v]setpts=PTS-STARTPTS[v2]; [3:v]setpts=PTS-STARTPTS[v3]; [base][v0]overlay=${COL_LEFT_X}:${COL_Y}:eof_action=repeat[tmp1]; [tmp1][v1]overlay=${COL_CENTER_X}:${COL_Y}:eof_action=repeat[tmp2]; [tmp2][v2]overlay=${COL_RIGHT_X}:${COL_Y}:eof_action=repeat[tmp3]; [tmp3][v3]overlay=${middle_x}:${middle_y}:eof_action=repeat[vout]; [0:a][1:a][2:a][3:a]amix=inputs=4:duration=first:dropout_transition=2[aout] " ffmpeg -nostdin \ -i "$col_left" \ -i "$col_center" \ -i "$col_right" \ -i "$middle_overlay" \ -filter_complex "$filter_complex" \ -map "[vout]" -map "[aout]" \ -c:v $ENCODER $ENCODER_OPTS \ -c:a aac -b:a 192k \ -movflags +faststart \ -loglevel error \ -y "$output_file" } # ============================================================================ # INTERACTIVE MENU # ============================================================================ select_target_duration() { # Non-interactive mode: use TARGET_DURATION from environment (in seconds) if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then TARGET_DURATION="${TARGET_DURATION:-3600}" # Default 60 minutes echo "Non-interactive mode: Target duration $(format_duration $TARGET_DURATION)" return fi echo "" echo "Select target compilation duration:" echo " 1) 30 minutes" echo " 2) 45 minutes" echo " 3) 60 minutes (1 hour)" echo " 4) 90 minutes (1.5 hours)" echo " 5) 120 minutes (2 hours)" echo " 6) Custom duration" echo "" read -p "Enter choice [1-6] (default: 3): " duration_choice case "$duration_choice" in 1) TARGET_DURATION=$((30 * 60)) ;; 2) TARGET_DURATION=$((45 * 60)) ;; 4) TARGET_DURATION=$((90 * 60)) ;; 5) TARGET_DURATION=$((120 * 60)) ;; 6) read -p "Enter duration in minutes: " custom_minutes TARGET_DURATION=$((custom_minutes * 60)) ;; *) TARGET_DURATION=$((60 * 60)) ;; esac echo "" echo "Target duration: $(format_duration $TARGET_DURATION)" } select_middle_overlay_source() { # Non-interactive mode: use OVERLAY_SOURCE from environment if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then if [[ -n "$OVERLAY_SOURCE" ]]; then MIDDLE_SOURCE_NAME="$OVERLAY_SOURCE" MIDDLE_SOURCE_DIR="$GIFS_DIR/$MIDDLE_SOURCE_NAME" if [[ ! -d "$MIDDLE_SOURCE_DIR/horizontal" ]]; then echo "Error: Overlay source '$OVERLAY_SOURCE' not found or has no horizontal content" exit 1 fi echo "Non-interactive mode: Using overlay source '$MIDDLE_SOURCE_NAME'" return else echo "Error: OVERLAY_SOURCE environment variable required in non-interactive mode" exit 1 fi fi echo "" echo "Select source for middle horizontal overlay:" echo "" # Find all creator folders with horizontal content local creators=() while IFS= read -r -d '' dir; do if [[ -d "$dir/horizontal" ]]; then local count=$(find "$dir/horizontal" -type f -name "*.mp4" 2>/dev/null | wc -l) if [[ $count -gt 0 ]]; then local name=$(basename "$dir") creators+=("$name:$count") fi fi done < <(find "$GIFS_DIR" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | filter_excluded_null | sort -z) if [[ ${#creators[@]} -eq 0 ]]; then echo "Error: No horizontal content found in gifs directory" exit 1 fi # Display menu local idx=1 for entry in "${creators[@]}"; do local name="${entry%%:*}" local count="${entry#*:}" printf " %2d) %s (%d videos)\n" "$idx" "$name" "$count" ((idx++)) done echo "" read -p "Enter choice [1-${#creators[@]}]: " creator_choice if [[ -z "$creator_choice" ]] || [[ "$creator_choice" -lt 1 ]] || [[ "$creator_choice" -gt ${#creators[@]} ]]; then echo "Invalid choice" exit 1 fi local selected="${creators[$((creator_choice - 1))]}" MIDDLE_SOURCE_NAME="${selected%%:*}" MIDDLE_SOURCE_DIR="$GIFS_DIR/$MIDDLE_SOURCE_NAME" echo "" echo "Selected: $MIDDLE_SOURCE_NAME" } # ============================================================================ # MAIN # ============================================================================ main() { echo "" echo "==============================================" echo " MEGA COMPILATION CREATOR" echo "==============================================" echo "" # Check dependencies check_dependencies # Setup encoder setup_encoder # Log excluded directories if any if [[ -n "$EXCLUDED_DIRS" ]]; then echo "" echo "Excluding directories: $EXCLUDED_DIRS" fi # Get user selections select_target_duration select_middle_overlay_source local current_date=$(date +%d-%m-%Y) local start_time=$(date +%s) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Building Video Inventory" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Reset used videos tracker USED_VIDEOS=() # Build video inventories declare -a LIBRARY_VIDEOS declare -a GIF_VIDEOS declare -a MIDDLE_VIDEOS find_library_vertical_videos LIBRARY_VIDEOS find_gif_vertical_videos GIF_VIDEOS find_horizontal_videos "$MIDDLE_SOURCE_DIR" MIDDLE_VIDEOS echo " → Found ${#MIDDLE_VIDEOS[@]} horizontal videos for middle overlay" if [[ ${#LIBRARY_VIDEOS[@]} -eq 0 ]] && [[ ${#GIF_VIDEOS[@]} -eq 0 ]]; then echo "" echo "Error: No vertical videos found" exit 1 fi if [[ ${#MIDDLE_VIDEOS[@]} -eq 0 ]]; then echo "" echo "Error: No horizontal videos found for middle overlay" exit 1 fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Selecting Videos for Columns" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Initialize temp file to track used videos across subshells USED_VIDEOS_FILE=$(mktemp) export USED_VIDEOS_FILE # Export so subshells can see it # Select videos for each column (unique videos per column) mapfile -t COL_LEFT_VIDEOS < <(select_videos_for_column "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Left") mapfile -t COL_CENTER_VIDEOS < <(select_videos_for_column "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Center") mapfile -t COL_RIGHT_VIDEOS < <(select_videos_for_column "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Right") # Show preview and confirm echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Preview Selection" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Left column (${#COL_LEFT_VIDEOS[@]} videos):" for v in "${COL_LEFT_VIDEOS[@]:0:3}"; do echo " • $(basename "$v")" done [[ ${#COL_LEFT_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#COL_LEFT_VIDEOS[@]} - 3)) more" echo "" echo "Center column (${#COL_CENTER_VIDEOS[@]} videos):" for v in "${COL_CENTER_VIDEOS[@]:0:3}"; do echo " • $(basename "$v")" done [[ ${#COL_CENTER_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#COL_CENTER_VIDEOS[@]} - 3)) more" echo "" echo "Right column (${#COL_RIGHT_VIDEOS[@]} videos):" for v in "${COL_RIGHT_VIDEOS[@]:0:3}"; do echo " • $(basename "$v")" done [[ ${#COL_RIGHT_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#COL_RIGHT_VIDEOS[@]} - 3)) more" echo "" echo "Middle overlay: $MIDDLE_SOURCE_NAME (${#MIDDLE_VIDEOS[@]} horizontal videos)" echo "" echo "Target duration: $(format_duration $TARGET_DURATION)" echo "" # Skip confirmation in non-interactive mode if [[ "${NONINTERACTIVE:-0}" != "1" ]]; then read -p "Proceed with encoding? [Y/n]: " confirm if [[ "$confirm" =~ ^[Nn] ]]; then echo "Cancelled." exit 0 fi fi # Create temp directory for intermediate files TEMP_DIR=$(mktemp -d) trap "rm -rf '$TEMP_DIR'; rm -f '$USED_VIDEOS_FILE'" EXIT echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Processing Columns" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Create column videos echo " → Processing left column (${#COL_LEFT_VIDEOS[@]} videos)..." create_column_video "$TEMP_DIR/col_left.mp4" "$TARGET_DURATION" "${COL_LEFT_VIDEOS[@]}" echo " → Processing center column (${#COL_CENTER_VIDEOS[@]} videos)..." create_column_video "$TEMP_DIR/col_center.mp4" "$TARGET_DURATION" "${COL_CENTER_VIDEOS[@]}" echo " → Processing right column (${#COL_RIGHT_VIDEOS[@]} videos)..." create_column_video "$TEMP_DIR/col_right.mp4" "$TARGET_DURATION" "${COL_RIGHT_VIDEOS[@]}" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Processing Middle Overlay" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Shuffle and limit middle videos mapfile -t SHUFFLED_MIDDLE < <(printf '%s\n' "${MIDDLE_VIDEOS[@]}" | shuf | head -20) echo " → Creating middle overlay with ${#SHUFFLED_MIDDLE[@]} videos and crossfades..." create_middle_overlay "$TEMP_DIR/middle.mp4" "$TARGET_DURATION" "${SHUFFLED_MIDDLE[@]}" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Creating Final Composite" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Create final composite local temp_output="$TEMP_DIR/final.mp4" create_final_composite "$temp_output" \ "$TEMP_DIR/col_left.mp4" \ "$TEMP_DIR/col_center.mp4" \ "$TEMP_DIR/col_right.mp4" \ "$TEMP_DIR/middle.mp4" \ "$TARGET_DURATION" if [[ -f "$temp_output" ]]; then # Get actual duration local actual_duration=$(get_duration_ffprobe "$temp_output") local duration_formatted=$(format_duration "$actual_duration") # Create final filename with versioning to prevent overwrites local base_filename="Mega Comp [${current_date}] [${duration_formatted}] [4K] [E] [H]" local final_filename="${base_filename}.mp4" local final_path="$OUTPUT_DIR/$final_filename" # Check if file exists and add version number if needed local version=1 while [[ -f "$final_path" ]]; do final_filename="${base_filename} v${version}.mp4" final_path="$OUTPUT_DIR/$final_filename" ((version++)) done mv "$temp_output" "$final_path" local file_size=$(du -h "$final_path" | cut -f1) echo "" echo " ✓ Success! Created: $final_filename ($file_size)" else echo "" echo " ✗ Failed to create compilation" exit 1 fi # Calculate elapsed time local end_time=$(date +%s) local elapsed=$((end_time - start_time)) local hours=$((elapsed / 3600)) local minutes=$(((elapsed % 3600) / 60)) local seconds=$((elapsed % 60)) echo "" echo "" echo "==============================================" echo " MEGA COMPILATION COMPLETE!" echo "==============================================" echo " Time elapsed: ${hours}h ${minutes}m ${seconds}s" echo "==============================================" echo "" } # Run main main "$@"