#!/bin/bash # 4-seasons.sh — create one daily clip from yesterday's images, sized to its # share of the corresponding Vivaldi movement's music duration. # Run once per day (e.g. from cron at 01:00). set -euo pipefail set -x # ── Configuration ────────────────────────────────────────────────────────────── base_dir="/home/motion/drives/local-2tb" music_base_dir="$base_dir/music/4 Seasons" # Fallback: use music/ directory next to this script (repo copy) [ ! -d "$music_base_dir" ] && music_base_dir="$(dirname "$(realpath "$0")")/music" TARGET_W=1280 TARGET_H=720 # ── Date setup ──────────────────────────────────────────────────────────────── yesterday=$(date --date="yesterday" +%Y-%m-%d) day_of_year=$(date --date="$yesterday" +%j | sed 's/^0*//') # strip leading zeros # ── Season / movement detection ──────────────────────────────────────────────── if [ "$day_of_year" -ge 80 ] && [ "$day_of_year" -le 172 ]; then season="Spring"; season_start=80; days_in_season=93 elif [ "$day_of_year" -ge 173 ] && [ "$day_of_year" -le 264 ]; then season="Summer"; season_start=173; days_in_season=92 elif [ "$day_of_year" -ge 265 ] && [ "$day_of_year" -le 355 ]; then season="Autumn"; season_start=265; days_in_season=91 else season="Winter"; days_in_season=90 if [ "$day_of_year" -ge 356 ]; then season_start=356; else season_start=0; fi fi # Day within the season (1-based) if [ "$season" = "Winter" ] && [ "$day_of_year" -lt 80 ]; then day_of_season=$(( day_of_year + 365 - 355 )) elif [ "$season" = "Winter" ]; then day_of_season=$(( day_of_year - 355 )) else day_of_season=$(( day_of_year - season_start + 1 )) fi # Each season: three movements of ~30 days; Mvt 3 takes the remainder if [ "$day_of_season" -le 30 ]; then movement_num=1; days_in_movement=30 elif [ "$day_of_season" -le 60 ]; then movement_num=2; days_in_movement=30 else movement_num=3; days_in_movement=$(( days_in_season - 60 )) fi echo "Date=$yesterday Season=$season Mvt=$movement_num DayOfSeason=$day_of_season DaysInMvt=$days_in_movement" # ── Locate music file ───────────────────────────────────────────────────────── music_file=$(find "$music_base_dir" -type f -iname "*${season}*Mvt*${movement_num}*" | sort | head -n 1) if [ -z "$music_file" ] || [ ! -f "$music_file" ]; then echo "ERROR: Music file not found for $season Mvt $movement_num in $music_base_dir" exit 1 fi music_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$music_file") echo "Music: $(basename "$music_file") ($music_duration s)" # Each daily clip should cover music_duration / days_in_movement seconds so that # the full movement montage sums to exactly the music duration. target_duration_per_video=$(echo "scale=6; $music_duration / $days_in_movement" | bc) echo "Target duration per daily clip: $target_duration_per_video s" # ── Locate images ───────────────────────────────────────────────────────────── script_name=$(basename "$(dirname "$(realpath "$0")")" | cut -d'-' -f1) image_dir="$base_dir/$script_name/$yesterday" if [ ! -d "$image_dir" ]; then echo "WARNING: Image directory $image_dir not found for $yesterday — skipping." exit 0 fi total_images=$(find "$image_dir" -type f -name "*.jpg" | wc -l) if [ "$total_images" -lt 1 ]; then echo "WARNING: No images in $image_dir for $yesterday — skipping." exit 0 fi # ── Prepare output directory ────────────────────────────────────────────────── output_dir="$base_dir/movies/$script_name/$season/Mvt$movement_num" mkdir -p "$output_dir" # ── Build sorted image list ──────────────────────────────────────────────────── temp_file=$(mktemp --suffix=.txt) trap 'rm -f "$temp_file" "$temp_video"' EXIT find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do echo "file '$img'" done > "$temp_file" num_images=$(wc -l < "$temp_file") echo "Images: $num_images" # ── Step 1: Build raw video at default frame rate ───────────────────────────── temp_video="$output_dir/${yesterday}_Mvt${movement_num}-temp.mp4" echo "Creating raw video..." ffmpeg -loglevel warning \ -f concat -safe 0 -i "$temp_file" \ -c:v libx264 -pix_fmt yuv420p -crf 28 -vsync 2 -an \ -y "$temp_video" raw_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_video") echo "Raw video duration: $raw_duration s" # ── Step 2: Speed-adjust to target duration + normalise resolution ───────────── speed_factor=$(echo "scale=6; $raw_duration / $target_duration_per_video" | bc) echo "Speed factor: $speed_factor" final_file="$output_dir/${yesterday}_Mvt${movement_num}-Day${day_of_season}of${days_in_movement}-final.mp4" ffmpeg -loglevel warning \ -i "$temp_video" \ -vf "setpts=PTS/$speed_factor,\ scale=${TARGET_W}:${TARGET_H}:force_original_aspect_ratio=decrease,\ pad=${TARGET_W}:${TARGET_H}:(ow-iw)/2:(oh-ih)/2,setsar=1" \ -c:v libx264 -pix_fmt yuv420p -crf 26 \ -t "$target_duration_per_video" \ -y "$final_file" rm "$temp_video" adjusted_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_file") echo "Daily clip created: $final_file ($adjusted_duration s)"