#!/bin/bash # ============================================================================ # QUAD HORIZONTAL COMPILATION CREATOR # ============================================================================ # Creates a 4K compilation with: # - Four 1920x1080 horizontal videos in a 2x2 grid (perfectly tiles 4K) # - Center horizontal overlay with crossfade transitions # - Smart duration filling to reach target length # # Canvas: 3840x2160 (4K) # Grid: 2x2 of 1920x1080 videos (no gaps, perfect tile) # Positions: TL (0,0), TR (1920,0), BL (0,1080), BR (1920,1080) # Middle overlay: Centered on top, 800px wide # ============================================================================ # Enable strict error handling set -o pipefail # Catch failures in piped commands # 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 } # Quadrant dimensions and positions (2x2 grid, perfectly tiles 4K canvas) QUAD_WIDTH=1920 QUAD_HEIGHT=1080 QUAD_TL_X=0 QUAD_TL_Y=0 QUAD_TR_X=1920 QUAD_TR_Y=0 QUAD_BL_X=0 QUAD_BL_Y=1080 QUAD_BR_X=1920 QUAD_BR_Y=1080 # Middle overlay settings (sits on top of the 4 quadrants) MIDDLE_WIDTH=800 CROSSFADE_DURATION=1 # seconds # Diversity settings MAX_VIDEOS_PER_PERFORMER=1 # Max videos from same performer per quadrant # Canvas CANVAS_WIDTH=3840 CANVAS_HEIGHT=2160 # ============================================================================ # UTILITY FUNCTIONS # ============================================================================ 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() { 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 } get_duration_from_filename() { local filename="$1" local basename=$(basename "$filename") if [[ "$basename" =~ \[([0-9]+):([0-9]{2})\] ]]; then local hours="${BASH_REMATCH[1]}" local minutes="${BASH_REMATCH[2]}" echo $(( 10#$hours * 3600 + 10#$minutes * 60 )) return 0 fi if [[ "$basename" =~ \[([0-9]+)s\] ]]; then echo $(( 10#${BASH_REMATCH[1]} )) return 0 fi ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$filename" 2>/dev/null | cut -d. -f1 } 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_duration() { local seconds="$1" local hours=$((seconds / 3600)) local minutes=$(((seconds % 3600) / 60)) printf "%d:%02d" "$hours" "$minutes" } 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" ]] } is_valid_video() { local file="$1" 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 if ! timeout 10 ffmpeg -nostdin -v error -i "$file" -vframes 5 -f null - 2>/dev/null; then return 1 fi return 0 } # ============================================================================ # VIDEO INVENTORY FUNCTIONS # ============================================================================ # Find all horizontal [H] videos from the main library (not gifs) find_library_horizontal_videos() { local -n result_array=$1 local skipped=0 local checked=0 echo " Scanning library for horizontal [H] videos..." while IFS= read -r -d '' file; do ((checked++)) printf "\r Checking file %d..." "$checked" if ! has_audio "$file"; then ((skipped++)) continue fi 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 "*\[H\].mp4" ! -path "*/gifs/*" ! -path "*/scripts/*" -print0 2>/dev/null | filter_excluded_null) echo "" echo " Found ${#result_array[@]} valid horizontal videos in library (skipped $skipped)" } # Find all horizontal gif videos find_gif_horizontal_videos() { local -n result_array=$1 local skipped=0 local checked=0 local total_files=$(find "$GIFS_DIR" -path "*/horizontal/*.mp4" -type f 2>/dev/null | filter_excluded | wc -l) echo " Scanning for horizontal 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 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 "*/horizontal/*.mp4" -type f -print0 2>/dev/null | filter_excluded_null) echo "" echo " Found ${#result_array[@]} valid horizontal gif clips (skipped $skipped)" } # Find horizontal videos in a specific creator folder (for middle overlay) find_creator_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 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 # ============================================================================ USED_VIDEOS_FILE="" 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_used() { local filepath="$1" echo "$filepath" >> "$USED_VIDEOS_FILE" sync } # Extract performer name from filename (format: "Studio - Performer - Title [metadata].mp4") get_performer_from_filename() { local filepath="$1" local basename=$(basename "$filepath" .mp4) # Try to extract performer from "Studio - Performer - Title" format if [[ "$basename" =~ ^[^-]+-[[:space:]]*([^-]+)[[:space:]]*- ]]; then local performer="${BASH_REMATCH[1]}" # Trim whitespace and normalize performer=$(echo "$performer" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]') echo "$performer" return 0 fi # Fallback: use first part before any dash or bracket local fallback=$(echo "$basename" | sed 's/[[-].*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]') echo "$fallback" } # Select videos to fill a target duration for one quadrant select_videos_for_quadrant() { local target_seconds=$1 local -n library_videos=$2 local -n gif_videos=$3 local quadrant_name=$4 local library_selected=() local gif_selected=() local library_duration=0 local gif_duration=0 local min_gap=30 local library_target=$((target_seconds / 2)) local gif_target=$((target_seconds / 2)) # Track performer counts for diversity declare -A performer_counts # 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 for entry in "${shuffled_library[@]}"; do local duration="${entry%%:*}" local filepath="${entry#*:}" if is_video_used "$filepath"; then continue fi # Check performer diversity limit local performer=$(get_performer_from_filename "$filepath") local current_count=${performer_counts[$performer]:-0} if [[ "$current_count" -ge "$MAX_VIDEOS_PER_PERFORMER" ]]; 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)) performer_counts[$performer]=$((current_count + 1)) fi done # Select gif videos 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 # Check performer diversity limit local performer=$(get_performer_from_filename "$filepath") local current_count=${performer_counts[$performer]:-0} if [[ "$current_count" -ge "$MAX_VIDEOS_PER_PERFORMER" ]]; 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)) performer_counts[$performer]=$((current_count + 1)) fi done # Interleave library and gif videos local final_videos=() local lib_idx=0 local gif_idx=0 local lib_count=${#library_selected[@]} local gif_count=${#gif_selected[@]} 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 if [[ $lib_idx -lt $lib_count ]]; then local entry="${library_selected[$lib_idx]}" final_videos+=("${entry#*:}") ((lib_idx++)) fi 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)) local unique_performers=${#performer_counts[@]} echo " Quadrant $quadrant_name: ${#final_videos[@]} videos from $unique_performers performers, $(format_duration $total_duration) total (${lib_pct}% library, $((100-lib_pct))% gifs)" >&2 printf '%s\n' "${final_videos[@]}" } # ============================================================================ # VIDEO PROCESSING FUNCTIONS # ============================================================================ # Create a single quadrant video using concat filter create_quadrant_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 quadrant" return 1 fi 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..." 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 to quadrant size (horizontal 16:9), normalize fps/SAR filter_v="$filter_v[$i:v]scale=${QUAD_WIDTH}:${QUAD_HEIGHT}:force_original_aspect_ratio=decrease,pad=${QUAD_WIDTH}:${QUAD_HEIGHT}:(ow-iw)/2:(oh-ih)/2,fps=30,setsar=1,setpts=PTS-STARTPTS[v$i];" 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 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 quadrant (single-pass concat filter)..." 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 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 " Quadrant 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 local scaled_height=$((MIDDLE_WIDTH * 9 / 16)) # 16:9 aspect ratio local temp_dir=$(mktemp -d) 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,setsar=1" \ -c:v $ENCODER $ENCODER_OPTS \ -af "aresample=48000" \ -c:a aac -b:a 128k -ar 48000 \ -loglevel error \ -y "$output_file" 2>/dev/null rm -rf "$temp_dir" return 0 fi # 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,setsar=1,setpts=PTS-STARTPTS" \ -c:v $ENCODER $ENCODER_OPTS \ -af "aresample=48000,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="" for i in "${!prepared_files[@]}"; do inputs="$inputs -i \"${prepared_files[$i]}\"" done local durations=() for prep_file in "${prepared_files[@]}"; do local dur=$(get_duration_ffprobe "$prep_file") durations+=("$dur") done 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 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 local xfade_duration=$(get_duration_ffprobe "$xfade_output") 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 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 4 quadrants and middle overlay create_final_composite() { local output_file="$1" local quad_tl="$2" local quad_tr="$3" local quad_bl="$4" local quad_br="$5" local middle_overlay="$6" local target_duration="$7" local middle_height=$((MIDDLE_WIDTH * 9 / 16)) local middle_x=$(( (CANVAS_WIDTH - MIDDLE_WIDTH) / 2 )) local middle_y=$(( (CANVAS_HEIGHT - middle_height) / 2 )) echo " Compositing final video..." 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]; [4:v]setpts=PTS-STARTPTS[v4]; [base][v0]overlay=${QUAD_TL_X}:${QUAD_TL_Y}:eof_action=repeat[tmp1]; [tmp1][v1]overlay=${QUAD_TR_X}:${QUAD_TR_Y}:eof_action=repeat[tmp2]; [tmp2][v2]overlay=${QUAD_BL_X}:${QUAD_BL_Y}:eof_action=repeat[tmp3]; [tmp3][v3]overlay=${QUAD_BR_X}:${QUAD_BR_Y}:eof_action=repeat[tmp4]; [tmp4][v4]overlay=${middle_x}:${middle_y}:eof_action=repeat[vout]; [0:a][1:a][2:a][3:a][4:a]amix=inputs=5:duration=first:dropout_transition=2[aout] " ffmpeg -nostdin \ -i "$quad_tl" \ -i "$quad_tr" \ -i "$quad_bl" \ -i "$quad_br" \ -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 "" 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 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 " QUAD HORIZONTAL COMPILATION CREATOR" echo "==============================================" echo "" check_dependencies setup_encoder # Log excluded directories if any if [[ -n "$EXCLUDED_DIRS" ]]; then echo "" echo "Excluding directories: $EXCLUDED_DIRS" fi select_target_duration select_middle_overlay_source local current_date=$(date +%d-%m-%Y) local start_time=$(date +%s) echo "" echo "Building Video Inventory" echo "" USED_VIDEOS_FILE=$(mktemp) export USED_VIDEOS_FILE declare -a LIBRARY_VIDEOS declare -a GIF_VIDEOS declare -a MIDDLE_VIDEOS find_library_horizontal_videos LIBRARY_VIDEOS find_gif_horizontal_videos GIF_VIDEOS find_creator_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 horizontal 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 "Selecting Videos for Quadrants" echo "" mapfile -t QUAD_TL_VIDEOS < <(select_videos_for_quadrant "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Top-Left") mapfile -t QUAD_TR_VIDEOS < <(select_videos_for_quadrant "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Top-Right") mapfile -t QUAD_BL_VIDEOS < <(select_videos_for_quadrant "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Bottom-Left") mapfile -t QUAD_BR_VIDEOS < <(select_videos_for_quadrant "$TARGET_DURATION" LIBRARY_VIDEOS GIF_VIDEOS "Bottom-Right") echo "" echo "Preview Selection" echo "" echo "Top-Left (${#QUAD_TL_VIDEOS[@]} videos):" for v in "${QUAD_TL_VIDEOS[@]:0:3}"; do echo " $(basename "$v")" done [[ ${#QUAD_TL_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#QUAD_TL_VIDEOS[@]} - 3)) more" echo "" echo "Top-Right (${#QUAD_TR_VIDEOS[@]} videos):" for v in "${QUAD_TR_VIDEOS[@]:0:3}"; do echo " $(basename "$v")" done [[ ${#QUAD_TR_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#QUAD_TR_VIDEOS[@]} - 3)) more" echo "" echo "Bottom-Left (${#QUAD_BL_VIDEOS[@]} videos):" for v in "${QUAD_BL_VIDEOS[@]:0:3}"; do echo " $(basename "$v")" done [[ ${#QUAD_BL_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#QUAD_BL_VIDEOS[@]} - 3)) more" echo "" echo "Bottom-Right (${#QUAD_BR_VIDEOS[@]} videos):" for v in "${QUAD_BR_VIDEOS[@]:0:3}"; do echo " $(basename "$v")" done [[ ${#QUAD_BR_VIDEOS[@]} -gt 3 ]] && echo " ... and $((${#QUAD_BR_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 TEMP_DIR=$(mktemp -d) trap "rm -rf '$TEMP_DIR'; rm -f '$USED_VIDEOS_FILE'" EXIT echo "" echo "Processing Quadrants" echo "" echo " Processing Top-Left (${#QUAD_TL_VIDEOS[@]} videos)..." create_quadrant_video "$TEMP_DIR/quad_tl.mp4" "$TARGET_DURATION" "${QUAD_TL_VIDEOS[@]}" echo " Processing Top-Right (${#QUAD_TR_VIDEOS[@]} videos)..." create_quadrant_video "$TEMP_DIR/quad_tr.mp4" "$TARGET_DURATION" "${QUAD_TR_VIDEOS[@]}" echo " Processing Bottom-Left (${#QUAD_BL_VIDEOS[@]} videos)..." create_quadrant_video "$TEMP_DIR/quad_bl.mp4" "$TARGET_DURATION" "${QUAD_BL_VIDEOS[@]}" echo " Processing Bottom-Right (${#QUAD_BR_VIDEOS[@]} videos)..." create_quadrant_video "$TEMP_DIR/quad_br.mp4" "$TARGET_DURATION" "${QUAD_BR_VIDEOS[@]}" echo "" echo "Processing Middle Overlay" echo "" 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 "Creating Final Composite" echo "" local temp_output="$TEMP_DIR/final.mp4" create_final_composite "$temp_output" \ "$TEMP_DIR/quad_tl.mp4" \ "$TEMP_DIR/quad_tr.mp4" \ "$TEMP_DIR/quad_bl.mp4" \ "$TEMP_DIR/quad_br.mp4" \ "$TEMP_DIR/middle.mp4" \ "$TARGET_DURATION" if [[ -f "$temp_output" ]]; then local actual_duration=$(get_duration_ffprobe "$temp_output") local duration_formatted=$(format_duration "$actual_duration") # Create final filename with versioning local base_filename="Quad Comp [${current_date}] [${duration_formatted}] [4K] [E] [H]" local final_filename="${base_filename}.mp4" local final_path="$OUTPUT_DIR/$final_filename" 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 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 " QUAD COMPILATION COMPLETE!" echo "==============================================" echo " Time elapsed: ${hours}h ${minutes}m ${seconds}s" echo "==============================================" echo "" } main "$@"