#!/bin/bash # daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to # SUNRISE_TARGET_SECS, burn the local sunrise time vertically on the right # side, and write the final video. Upload is handled by a separate systemd # service (sky-cam-sunrise-upload) triggered via OnSuccess= so the two jobs # have independent log entries and failure states. # # Resilience: the speed-adjusted video is saved permanently before the overlay # step. If overlay fails, the sped video survives at: # /-daily-sunrise-sped.mp4 # Re-run this script once the issue is resolved; the sped file is deleted # automatically when the overlay step succeeds. set -euo pipefail SCRIPT_DIR="$(dirname "$(realpath "$0")")" source "$SCRIPT_DIR/sky-cam.conf" export TZ="$TIMEZONE" CAM_NAME="${1:-$SUNRISE_CAM}" # ── Date / paths ────────────────────────────────────────────────────────────── current_date=$(date +%Y-%m-%d) output_date=$(date +%Y-%m) image_dir="$BASE_DIR/$CAM_NAME/$current_date" output_dir="$MOVIES_DIR/$CAM_NAME/$output_date/sunrise-only" mkdir -p "$output_dir" # ── Sunrise time ────────────────────────────────────────────────────────────── sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py") if [[ -z "$sunrise_time" ]]; then "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ "Could not retrieve sunrise time from sunrise.py" || true echo "Error: Failed to retrieve sunrise time." exit 1 fi echo "Sunrise (UTC): $sunrise_time" sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S") echo "Sunrise (local): $sunrise_time_local" # ── Capture window ──────────────────────────────────────────────────────────── time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); } sunrise_sec=$(time_to_seconds "$sunrise_time_local") start_sec=$((sunrise_sec - SUNRISE_PRE_MIN * 60)) end_sec=$((sunrise_sec + SUNRISE_POST_MIN * 60)) echo "Window: $((start_sec/3600)):$(printf '%02d' $(((start_sec%3600)/60))) → $((end_sec/3600)):$(printf '%02d' $(((end_sec%3600)/60)))" # ── Wait for capture window to finish ──────────────────────────────────────── # The timer fires early (default 03:00); we sleep internally so images from # the full window are available before we start encoding. midnight=$(date -d "today 00:00:00" +%s) now_day_sec=$(( $(date +%s) - midnight )) if [ "$now_day_sec" -lt "$end_sec" ]; then wait_sec=$(( end_sec - now_day_sec + 30 )) echo "Waiting ${wait_sec}s for capture window to finish (ends $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S'))..." sleep "$wait_sec" else echo "Capture window already closed at $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S') — processing available images" fi # ── Collect images in window ────────────────────────────────────────────────── if [ ! -d "$image_dir" ]; then "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ "Image directory not found: $image_dir" || true echo "Error: image directory not found: $image_dir" exit 1 fi temp_list=$(mktemp --suffix=.txt) raw_video="" temp_audio="" trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" 2>/dev/null || true' EXIT find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do img_sec=$(time_to_seconds "$(basename "$img" .jpg)") if [[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]]; then echo "file '$img'" fi done > "$temp_list" num_images=$(wc -l < "$temp_list") if [[ $num_images -lt 1 ]]; then "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ "No images found in sunrise window" || true echo "No images found in sunrise window." exit 1 fi echo "Images in window: $num_images" # ── Font detection (same fallback chain as montage-mvt.sh) ─────────────────── FONT="" if command -v fc-match &>/dev/null; then FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true) fi if [ -z "$FONT" ]; then for f in \ /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \ /usr/share/fonts/dejavu/DejaVuSans.ttf \ /usr/share/fonts/TTF/DejaVuSans.ttf \ /usr/share/fonts/truetype/freefont/FreeSans.ttf; do [ -f "$f" ] && { FONT="$f"; break; } done fi # ── Step 1: Raw video from images ───────────────────────────────────────────── raw_video=$(mktemp --suffix=.mp4) echo "Step 1/3: encoding raw video..." if ! ffmpeg -loglevel warning \ -f concat -safe 0 -i "$temp_list" \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \ -y "$raw_video"; then "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 1/3 (encode) $current_date" \ "ffmpeg raw encode from images failed — check: journalctl -u sky-cam-sunrise.service" || true exit 1 fi raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$raw_video") speed_factor=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc) echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x" # ── Step 2: Speed-adjust — saved permanently so overlay failure is recoverable ─ # Deleted automatically if step 3 succeeds. sped_video="$output_dir/$current_date-daily-sunrise-sped.mp4" echo "Step 2/3: speed-adjust → $sped_video" if ! ffmpeg -loglevel warning \ -i "$raw_video" \ -vf "setpts=PTS/${speed_factor}" \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \ -y "$sped_video"; then rm -f "$sped_video" "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \ "speed-adjust failed — no video saved" || true exit 1 fi sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video") echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)" # ── Build overlay filter (skipped if SUNRISE_OVERLAY_ENABLED=false) ────────── SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':') DT="" if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then SR_VERT=$(echo "$SR_TIME" | awk 'BEGIN{FS=""}{for(i=1;i<=NF;i++){printf "%s",$i; if(i/dev/null || true) if [ -n "$random_file" ]; then audio_src="$random_file" echo "Audio: library fallback $(basename "$audio_src")" fi fi fi fi # ── Step 3a: mix audio (video copied, not re-encoded) ──────────────────────── work_video="$sped_video" has_audio=false if [ -n "$audio_src" ]; then temp_audio=$(mktemp --suffix=.mp4) echo "Step 3a/3: mixing audio → temp" if ffmpeg -loglevel warning \ -i "$sped_video" \ -ss "$audio_offset" -t "$SUNRISE_TARGET_SECS" -i "$audio_src" \ -filter_complex "[1:a]afade=t=in:st=0:d=0.5,afade=t=out:st=${fade_out}:d=0.5[aout]" \ -map "0:v" -map "[aout]" \ -c:v copy -c:a aac -b:a 128k \ -y "$temp_audio"; then work_video="$temp_audio" has_audio=true echo "Audio: mixed OK" else rm -f "$temp_audio"; temp_audio="" echo "Audio mix failed — step 3b will be overlay-only" "$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \ "Audio could not be mixed — continuing with overlay only" || true [ "$audio_src" = "$cam_audio" ] && rm -f "$cam_audio" fi fi # ── Step 3b: burn overlay (or promote work_video directly if disabled) ──────── if $has_audio; then audio_out_flags=(-c:a copy); else audio_out_flags=(-an); fi step3b_ok=false if [ -n "$DT" ]; then echo "Step 3b/3: overlay → $final_video" if ffmpeg -loglevel warning \ -i "$work_video" \ -vf "${DT}" \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" \ "${audio_out_flags[@]}" \ -y "$final_video"; then step3b_ok=true else if $has_audio; then promote_label="audio-mixed"; else promote_label="speed-only"; fi mv "$work_video" "$final_video" [ "$work_video" != "$sped_video" ] && rm -f "$sped_video" temp_audio="" [ -f "$cam_audio" ] && rm -f "$cam_audio" echo "Overlay failed — promoting ${promote_label} video as upload target" "$SCRIPT_DIR/notify.sh" "WARNING: sunrise overlay failed $current_date" \ "Overlay failed — uploading ${promote_label} video (no timestamp)" || true step3b_ok=true # degraded but recoverable — upload still fires fi else echo "Step 3b/3: overlay disabled — promoting as final" mv "$work_video" "$final_video" [ "$work_video" != "$sped_video" ] && rm -f "$sped_video" temp_audio="" step3b_ok=true fi if $step3b_ok; then actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video") echo "Done: $final_video (${actual_dur}s, sunrise at ${SR_TIME})" rm -f "$sped_video" "$temp_audio" 2>/dev/null || true; temp_audio="" [ -f "$cam_audio" ] && rm -f "$cam_audio" "$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \ "$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME}" || true # ── Rolling retention: delete sunrise videos older than SUNRISE_RETENTION_DAYS ─ retain="${SUNRISE_RETENTION_DAYS:-10}" if [ "$retain" -gt 0 ]; then deleted_old=$(find "$MOVIES_DIR/$CAM_NAME" \ -path "*/sunrise-only/*-daily-sunrise.mp4" \ -mtime +"$retain" -print -delete 2>/dev/null | wc -l) [ "$deleted_old" -gt 0 ] && \ echo "Retention: deleted $deleted_old sunrise video(s) older than ${retain} days" fi fi