changemaker.lite/media-manager/scripts/create_quad_compilation.sh

971 lines
32 KiB
Bash
Executable File

#!/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<num_videos; i++)); do
offset=$((offset + ${durations[$((i-1))]} - CROSSFADE_DURATION))
local prev_v="v$((i-1))"
local prev_a="a$((i-1))"
if [[ $i -eq $((num_videos - 1)) ]]; then
filter_complex="$filter_complex;[$prev_v][$i:v]xfade=transition=fade:duration=${CROSSFADE_DURATION}:offset=${offset}[vout];[$prev_a][$i:a]acrossfade=d=${CROSSFADE_DURATION}[aout]"
else
filter_complex="$filter_complex;[$prev_v][$i:v]xfade=transition=fade:duration=${CROSSFADE_DURATION}:offset=${offset}[v$i];[$prev_a][$i:a]acrossfade=d=${CROSSFADE_DURATION}[a$i]"
fi
done
fi
local xfade_output="$temp_dir/xfade_output.mp4"
eval "ffmpeg -nostdin $inputs \
-filter_complex \"$filter_complex\" \
-map \"[vout]\" -map \"[aout]\" \
-c:v $ENCODER $ENCODER_OPTS \
-c:a aac -b:a 128k \
-loglevel error \
-y \"$xfade_output\"" 2>/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 "$@"