Merge pull request #8 from outis1one/claude/seasonal-sunrise-montage-nn268
Claude/seasonal sunrise montage nn268
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
+25
-6
@@ -14,6 +14,9 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|||||||
source "$SCRIPT_DIR/sky-cam.conf"
|
source "$SCRIPT_DIR/sky-cam.conf"
|
||||||
export TIMEZONE # make it visible to season_info.py subprocess
|
export TIMEZONE # make it visible to season_info.py subprocess
|
||||||
|
|
||||||
|
# Camera name: first argument overrides conf (systemd passes it via ExecStart).
|
||||||
|
CAM_NAME="${1:-$CAM_NAME}"
|
||||||
|
|
||||||
# ── Configuration ──────────────────────────────────────────────────────────────
|
# ── Configuration ──────────────────────────────────────────────────────────────
|
||||||
base_dir="$BASE_DIR"
|
base_dir="$BASE_DIR"
|
||||||
music_base_dir="$MUSIC_DIR"
|
music_base_dir="$MUSIC_DIR"
|
||||||
@@ -59,7 +62,7 @@ fi
|
|||||||
echo "Images found: $total_images"
|
echo "Images found: $total_images"
|
||||||
|
|
||||||
# ── Prepare output directory ──────────────────────────────────────────────────
|
# ── Prepare output directory ──────────────────────────────────────────────────
|
||||||
output_dir="$base_dir/movies/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
output_dir="$MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
# ── Build sorted image list ───────────────────────────────────────────────────
|
# ── Build sorted image list ───────────────────────────────────────────────────
|
||||||
@@ -76,7 +79,7 @@ done > "$temp_file"
|
|||||||
echo "Step 1/2: encoding raw video..."
|
echo "Step 1/2: encoding raw video..."
|
||||||
ffmpeg -loglevel warning \
|
ffmpeg -loglevel warning \
|
||||||
-f concat -safe 0 -i "$temp_file" \
|
-f concat -safe 0 -i "$temp_file" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -crf 28 -vsync 2 -an \
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_SEASONS_RAW" -vsync 2 -an \
|
||||||
-y "$temp_video"
|
-y "$temp_video"
|
||||||
|
|
||||||
raw_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_video")
|
raw_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_video")
|
||||||
@@ -87,20 +90,36 @@ speed_factor=$(echo "scale=6; $raw_duration / $target_per_clip" | bc)
|
|||||||
echo "Step 2/2: speed factor $speed_factor..."
|
echo "Step 2/2: speed factor $speed_factor..."
|
||||||
|
|
||||||
final_file="$output_dir/${yesterday}_Mvt${MVT_NUM}-Day${DAY_OF_MVT}of${DAYS_IN_MVT}-final.mp4"
|
final_file="$output_dir/${yesterday}_Mvt${MVT_NUM}-Day${DAY_OF_MVT}of${DAYS_IN_MVT}-final.mp4"
|
||||||
ffmpeg -loglevel warning \
|
if ! ffmpeg -loglevel warning \
|
||||||
-i "$temp_video" \
|
-i "$temp_video" \
|
||||||
-vf "setpts=PTS/$speed_factor" \
|
-vf "setpts=PTS/$speed_factor" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -crf 26 \
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_SEASONS_FINAL" \
|
||||||
-t "$target_per_clip" -an \
|
-t "$target_per_clip" -an \
|
||||||
-y "$final_file"
|
-y "$final_file"; then
|
||||||
|
rm -f "$final_file"
|
||||||
|
"$SCRIPT_DIR/notify.sh" "FAILED: $SEASON Mvt$MVT_NUM Day$DAY_OF_MVT speed-adjust ($yesterday)" \
|
||||||
|
"ffmpeg speed-adjust failed — re-run 4-seasons.sh $CAM_NAME while JPGs exist" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
adjusted=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_file")
|
adjusted=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_file")
|
||||||
echo "Daily clip: $final_file ($adjusted s)"
|
echo "Daily clip: $final_file ($adjusted s)"
|
||||||
|
|
||||||
|
# Warn if clip duration deviates more than 0.5 s from target (float drift check)
|
||||||
|
drift=$(echo "scale=3; $adjusted - $target_per_clip" | bc | sed 's/^-//')
|
||||||
|
if awk "BEGIN{exit !($drift > 0.5)}"; then
|
||||||
|
echo "WARNING: clip duration ${adjusted}s differs from target ${target_per_clip}s by ${drift}s"
|
||||||
|
"$SCRIPT_DIR/notify.sh" "WARNING: $SEASON Mvt$MVT_NUM Day$DAY_OF_MVT duration drift ($yesterday)" \
|
||||||
|
"Clip ${adjusted}s vs target ${target_per_clip}s (drift ${drift}s)" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$SCRIPT_DIR/notify.sh" "$SEASON Mvt$MVT_NUM Day$DAY_OF_MVT/$DAYS_IN_MVT saved ($yesterday)" \
|
||||||
|
"$(basename "$final_file") — ${adjusted}s" || true
|
||||||
|
|
||||||
# ── Auto-trigger montage on last day of movement ──────────────────────────────
|
# ── Auto-trigger montage on last day of movement ──────────────────────────────
|
||||||
if [ "$IS_LAST_DAY" = "true" ]; then
|
if [ "$IS_LAST_DAY" = "true" ]; then
|
||||||
echo "Last day of $SEASON Mvt $MVT_NUM — triggering montage compilation..."
|
echo "Last day of $SEASON Mvt $MVT_NUM — triggering montage compilation..."
|
||||||
# Pass $yesterday so montage-mvt.sh looks up the correct movement
|
# Pass $yesterday so montage-mvt.sh looks up the correct movement
|
||||||
# (running the next morning, "today" would already be the next movement).
|
# (running the next morning, "today" would already be the next movement).
|
||||||
"$SCRIPT_DIR/montage-mvt.sh" "$yesterday"
|
"$SCRIPT_DIR/montage-mvt.sh" "$yesterday" "$CAM_NAME"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+124
-115
@@ -1,152 +1,161 @@
|
|||||||
#!/bin/bash
|
#!/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:
|
||||||
|
# <output_dir>/<date>-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")")"
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
||||||
source "$SCRIPT_DIR/sky-cam.conf"
|
source "$SCRIPT_DIR/sky-cam.conf"
|
||||||
|
export TZ="$TIMEZONE"
|
||||||
|
|
||||||
# Get today's date in the format YYYY-MM-DD
|
# ── Date / paths ──────────────────────────────────────────────────────────────
|
||||||
current_date=$(date +%Y-%m-%d)
|
current_date=$(date +%Y-%m-%d)
|
||||||
output_date=$(date +%Y-%m)
|
output_date=$(date +%Y-%m)
|
||||||
|
|
||||||
# Define the directory where images are stored (using today's date)
|
|
||||||
image_dir="$BASE_DIR/$CAM_NAME/$current_date"
|
image_dir="$BASE_DIR/$CAM_NAME/$current_date"
|
||||||
|
|
||||||
# Set output directory
|
|
||||||
output_dir="$BASE_DIR/movies/$CAM_NAME/$output_date/sunrise-only"
|
output_dir="$BASE_DIR/movies/$CAM_NAME/$output_date/sunrise-only"
|
||||||
|
|
||||||
# Make sure the output directory exists
|
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
# Get the sunrise time for today using the sunrise.py script
|
# ── Sunrise time ──────────────────────────────────────────────────────────────
|
||||||
sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py")
|
sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py")
|
||||||
|
|
||||||
# Check if the sunrise time was fetched successfully
|
|
||||||
if [[ -z "$sunrise_time" ]]; then
|
if [[ -z "$sunrise_time" ]]; then
|
||||||
echo "Error: Failed to retrieve sunrise time."
|
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||||||
exit 1
|
"Could not retrieve sunrise time from sunrise.py" || true
|
||||||
|
echo "Error: Failed to retrieve sunrise time."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo "Sunrise (UTC): $sunrise_time"
|
||||||
|
|
||||||
# Output the sunrise time in UTC
|
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
|
||||||
echo "Sunrise time (UTC): $sunrise_time"
|
echo "Sunrise (local): $sunrise_time_local"
|
||||||
|
|
||||||
# Export timezone so date and subprocesses use the configured local time.
|
# ── Capture window ────────────────────────────────────────────────────────────
|
||||||
export TZ="$TIMEZONE"
|
time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); }
|
||||||
|
|
||||||
# Convert sunrise time from UTC to local time.
|
sunrise_sec=$(time_to_seconds "$sunrise_time_local")
|
||||||
sunrise_time_et=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
|
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)))"
|
||||||
|
|
||||||
# Output the converted sunrise time in Eastern Time
|
# ── Collect images in window ──────────────────────────────────────────────────
|
||||||
echo "Sunrise time (Eastern Time): $sunrise_time_et"
|
|
||||||
|
|
||||||
# Function to convert time to seconds since midnight
|
|
||||||
time_to_seconds() {
|
|
||||||
IFS='-' read -r h m s <<< "$1"
|
|
||||||
echo $((10#$h * 3600 + 10#$m * 60 + 10#$s))
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert sunrise time (in Eastern Time) to seconds since midnight
|
|
||||||
sunrise_seconds=$(time_to_seconds "$sunrise_time_et")
|
|
||||||
|
|
||||||
# Calculate the capture window around sunrise.
|
|
||||||
start_seconds=$((sunrise_seconds - SUNRISE_PRE_MIN * 60))
|
|
||||||
end_seconds=$((sunrise_seconds + SUNRISE_POST_MIN * 60))
|
|
||||||
|
|
||||||
# Output the start and end times in seconds for debugging
|
|
||||||
echo "Start seconds: $start_seconds"
|
|
||||||
echo "End seconds: $end_seconds"
|
|
||||||
echo "Sunrise seconds: $sunrise_seconds"
|
|
||||||
|
|
||||||
# Check if the image directory exists
|
|
||||||
if [ ! -d "$image_dir" ]; then
|
if [ ! -d "$image_dir" ]; then
|
||||||
echo "Error: The directory for images does not exist: $image_dir"
|
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||||||
exit 1
|
"Image directory not found: $image_dir" || true
|
||||||
|
echo "Error: image directory not found: $image_dir"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create a temporary file to store the list of image files
|
temp_list=$(mktemp --suffix=.txt)
|
||||||
temp_file=$(mktemp)
|
raw_video=""
|
||||||
|
trap 'rm -f "$temp_list" "$raw_video" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
# Find, sort, and process images in the specified directory
|
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do
|
||||||
# Sort files by filename, which will order them correctly by MM-DD-SS
|
img_sec=$(time_to_seconds "$(basename "$img" .jpg)")
|
||||||
find "$image_dir" -type f -name "*.jpg" | sort -n | while read -r image; do
|
if [[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]]; then
|
||||||
# Extract the time portion of the filename (e.g., 09-16-00 from 09-16-00.jpg)
|
echo "file '$img'"
|
||||||
image_time=$(basename "$image" .jpg) # MM-DD-SS
|
fi
|
||||||
|
done > "$temp_list"
|
||||||
|
|
||||||
# Convert image time to seconds since midnight
|
num_images=$(wc -l < "$temp_list")
|
||||||
image_seconds=$(time_to_seconds "$image_time")
|
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"
|
||||||
|
|
||||||
# Debug: Output the image time and its seconds since midnight
|
# ── Font detection (same fallback chain as montage-mvt.sh) ───────────────────
|
||||||
echo "Image: $image, Time: $image_time, Seconds: $image_seconds"
|
FONT=""
|
||||||
|
if command -v fc-match &>/dev/null; then
|
||||||
# Check if the image time falls within the specified range
|
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
|
||||||
if [[ $image_seconds -ge $start_seconds && $image_seconds -le $end_seconds ]]; then
|
fi
|
||||||
echo "Image $image_time ($image_seconds) is within the range [$start_seconds, $end_seconds]"
|
if [ -z "$FONT" ]; then
|
||||||
|
for f in \
|
||||||
# Add the image to the temporary file
|
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \
|
||||||
echo "file '$image'" >> "$temp_file"
|
/usr/share/fonts/dejavu/DejaVuSans.ttf \
|
||||||
fi
|
/usr/share/fonts/TTF/DejaVuSans.ttf \
|
||||||
done
|
/usr/share/fonts/truetype/freefont/FreeSans.ttf; do
|
||||||
|
[ -f "$f" ] && { FONT="$f"; break; }
|
||||||
# Check if any images were found in the time range
|
done
|
||||||
if [ ! -s "$temp_file" ]; then
|
|
||||||
echo "No images found within the specified time range."
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Calculate the number of images
|
# ── Step 1: Raw video from images ─────────────────────────────────────────────
|
||||||
num_images=$(wc -l < "$temp_file")
|
raw_video=$(mktemp --suffix=.mp4)
|
||||||
echo "Total images to be included in the video: $num_images"
|
echo "Step 1/3: encoding raw video..."
|
||||||
|
if ! ffmpeg -loglevel warning \
|
||||||
|
-f concat -safe 0 -i "$temp_list" \
|
||||||
|
-c:v libx264 -pix_fmt yuv420p -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
|
||||||
|
|
||||||
# Output video filename for the first movie (no speed adjustments)
|
raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$raw_video")
|
||||||
first_movie="$output_dir/$current_date-sunrise-day.mp4"
|
speed_factor=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc)
|
||||||
|
echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x"
|
||||||
|
|
||||||
# Create the first movie at the default frame rate (no speed adjustments)
|
# ── Step 2: Speed-adjust — saved permanently so overlay failure is recoverable ─
|
||||||
echo "Creating video at default frame rate..."
|
# Deleted automatically if step 3 succeeds.
|
||||||
ffmpeg -f concat -safe 0 -i "$temp_file" -c:v libx264 -pix_fmt yuv420p -crf 28 -vsync 2 -y "$first_movie"
|
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 -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
|
||||||
|
|
||||||
# Check the duration of the created video
|
sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video")
|
||||||
video_duration=$(ffmpeg -i "$first_movie" 2>&1 | grep "Duration" | cut -d ' ' -f 4 | sed s/,//)
|
echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)"
|
||||||
echo "Initial video duration: $video_duration"
|
|
||||||
|
|
||||||
# Get the video duration in seconds (from HH:MM:SS format)
|
# ── Build overlay filter ──────────────────────────────────────────────────────
|
||||||
IFS=':' read -r hours minutes seconds <<< $(echo $video_duration | cut -d '.' -f1) # Extract time
|
# Time displayed as stacked characters down the right side (e.g. 0/7/:/2/3),
|
||||||
total_seconds=$(($hours * 3600 + $minutes * 60 + $seconds)) # Convert to total seconds
|
# semi-transparent with a soft drop shadow so it reads on any background.
|
||||||
|
SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':')
|
||||||
|
# Build "H\nH\n:\nM\nM" for vertical stacking in drawtext
|
||||||
|
SR_VERT=$(echo "$SR_TIME" | awk 'BEGIN{FS=""}{for(i=1;i<=NF;i++){printf "%s",$i; if(i<NF)printf "\\n"}}')
|
||||||
|
|
||||||
target_duration=$SUNRISE_TARGET_SECS
|
DT="drawtext"
|
||||||
|
[ -n "$FONT" ] && DT="${DT}=fontfile='${FONT}'" || DT="${DT}"
|
||||||
|
DT="${DT}:text='${SR_VERT}'"
|
||||||
|
DT="${DT}:fontcolor=white@${SUNRISE_OVERLAY_OPACITY}"
|
||||||
|
DT="${DT}:fontsize=h/22"
|
||||||
|
DT="${DT}:line_spacing=4"
|
||||||
|
DT="${DT}:x=w-tw-18:y=(h-th)/2"
|
||||||
|
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
|
||||||
|
|
||||||
speed_up_factor=$(echo "scale=3; $total_seconds / $target_duration" | bc)
|
# ── Step 3: Overlay — sunrise time burned in ──────────────────────────────────
|
||||||
echo "Calculated speed-up factor: $speed_up_factor"
|
# If this fails, $sped_video survives at its permanent path for manual recovery.
|
||||||
|
|
||||||
# Output filename for the second movie (speed-adjusted)
|
|
||||||
final_video="$output_dir/$current_date-daily-sunrise.mp4"
|
final_video="$output_dir/$current_date-daily-sunrise.mp4"
|
||||||
|
echo "Step 3/3: overlay → $final_video"
|
||||||
# Apply the speed adjustment using the setpts filter
|
if ! ffmpeg -loglevel warning \
|
||||||
echo "Adjusting video speed to ${SUNRISE_TARGET_SECS}s..."
|
-i "$sped_video" \
|
||||||
ffmpeg -i "$first_movie" -filter:v "setpts=PTS/$speed_up_factor" -y "$final_video"
|
-vf "${DT}" \
|
||||||
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_SUNRISE" -an \
|
||||||
# Check the duration of the adjusted video
|
-y "$final_video"; then
|
||||||
adjusted_duration=$(ffmpeg -i "$final_video" 2>&1 | grep "Duration" | cut -d ' ' -f 4 | sed s/,//)
|
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise overlay $current_date" \
|
||||||
echo "Adjusted video duration: $adjusted_duration"
|
"Overlay failed — speed-only video saved: $(basename "$sped_video")" || true
|
||||||
|
exit 1
|
||||||
# Check for ffmpeg success
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Error: ffmpeg failed to create the final video."
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Inform user the video has been created
|
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video")
|
||||||
echo "Video created at $final_video"
|
echo "Done: $final_video (${actual_dur}s, sunrise at ${SR_TIME})"
|
||||||
|
|
||||||
# Clean up temporary files
|
rm -f "$sped_video"
|
||||||
rm "$temp_file"
|
"$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \
|
||||||
|
"$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME}" || true
|
||||||
# Run the sunrise_overlay.py script to add the sunrise overlay
|
|
||||||
python3 "$SCRIPT_DIR/sunrise_overlay.py" "$final_video"
|
|
||||||
|
|
||||||
# Run the sunrise2mm.py script to upload the video
|
|
||||||
python3 "$SCRIPT_DIR/sunrise2mm.py"
|
|
||||||
|
|
||||||
# Check if the Python script was successful
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Successfully uploaded the video to Mattermost."
|
|
||||||
else
|
|
||||||
echo "There was an issue uploading the video to Mattermost."
|
|
||||||
fi
|
|
||||||
|
|||||||
+5
-2
@@ -10,6 +10,9 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
||||||
source "$SCRIPT_DIR/sky-cam.conf"
|
source "$SCRIPT_DIR/sky-cam.conf"
|
||||||
|
|
||||||
|
# Camera name: first argument overrides conf (systemd passes it via ExecStart).
|
||||||
|
CAM_NAME="${1:-$CAM_NAME}"
|
||||||
|
|
||||||
# ── Configuration ──────────────────────────────────────────────────────────────
|
# ── Configuration ──────────────────────────────────────────────────────────────
|
||||||
base_dir="$BASE_DIR"
|
base_dir="$BASE_DIR"
|
||||||
# FULLDAY_FPS and RETENTION_DAYS come from sky-cam.conf
|
# FULLDAY_FPS and RETENTION_DAYS come from sky-cam.conf
|
||||||
@@ -17,7 +20,7 @@ base_dir="$BASE_DIR"
|
|||||||
# ── Date / paths ──────────────────────────────────────────────────────────────
|
# ── Date / paths ──────────────────────────────────────────────────────────────
|
||||||
yesterday=$(date --date="yesterday" +%Y-%m-%d)
|
yesterday=$(date --date="yesterday" +%Y-%m-%d)
|
||||||
image_dir="$base_dir/$CAM_NAME/$yesterday"
|
image_dir="$base_dir/$CAM_NAME/$yesterday"
|
||||||
output_dir="$base_dir/movies/$CAM_NAME/fullday"
|
output_dir="$MOVIES_DIR/$CAM_NAME/fullday"
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
# ── Purge old full-day videos ─────────────────────────────────────────────────
|
# ── Purge old full-day videos ─────────────────────────────────────────────────
|
||||||
@@ -60,7 +63,7 @@ echo "Creating: $output_file"
|
|||||||
|
|
||||||
ffmpeg -loglevel warning \
|
ffmpeg -loglevel warning \
|
||||||
-f concat -safe 0 -i "$temp_list" \
|
-f concat -safe 0 -i "$temp_list" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -crf 28 \
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_FULLDAY" \
|
||||||
-an -y "$output_file"
|
-an -y "$output_file"
|
||||||
|
|
||||||
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$output_file")
|
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$output_file")
|
||||||
|
|||||||
+73
-25
@@ -6,7 +6,8 @@
|
|||||||
# ./install.sh --system # installs to /etc/systemd/system (needs sudo)
|
# ./install.sh --system # installs to /etc/systemd/system (needs sudo)
|
||||||
#
|
#
|
||||||
# Run this once after editing sky-cam.conf, and again whenever sky-cam.conf
|
# Run this once after editing sky-cam.conf, and again whenever sky-cam.conf
|
||||||
# changes (e.g. SCRIPT_DIR moves). No other file needs editing.
|
# changes (e.g. SCRIPT_DIR moves, cameras added, schedules changed).
|
||||||
|
# No other file needs editing.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ write_service() {
|
|||||||
local unit="$1" description="$2" script="$3" extra="${4:-}"
|
local unit="$1" description="$2" script="$3" extra="${4:-}"
|
||||||
cat > "$UNIT_DIR/${unit}.service" <<EOF
|
cat > "$UNIT_DIR/${unit}.service" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Sky-Cam ($CAM_NAME) — $description
|
Description=Sky-Cam — $description
|
||||||
${extra}OnFailure=sky-cam-notify-failure@%n.service
|
${extra}OnFailure=sky-cam-notify-failure@%n.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -50,10 +51,10 @@ write_timer() {
|
|||||||
local unit="$1" description="$2" calendar="$3"
|
local unit="$1" description="$2" calendar="$3"
|
||||||
cat > "$UNIT_DIR/${unit}.timer" <<EOF
|
cat > "$UNIT_DIR/${unit}.timer" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Sky-Cam ($CAM_NAME) — $description
|
Description=Sky-Cam — $description
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnCalendar=$calendar
|
OnCalendar=*-*-* $calendar
|
||||||
Persistent=true
|
Persistent=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
@@ -73,32 +74,79 @@ ExecStart=$SCRIPT_DIR/notify.sh "sky-cam job failed: %i" "systemd unit %i failed
|
|||||||
EOF
|
EOF
|
||||||
echo " wrote sky-cam-notify-failure@.service"
|
echo " wrote sky-cam-notify-failure@.service"
|
||||||
|
|
||||||
# ── Three daily jobs ──────────────────────────────────────────────────────────
|
# ── Sunrise video — SUNRISE_CAM only ─────────────────────────────────────────
|
||||||
# Sunrise video → Mattermost (run after sunrise window ends)
|
# OnSuccess= triggers the upload service only when video creation succeeds.
|
||||||
write_service "sky-cam-sunrise" \
|
cat > "$UNIT_DIR/sky-cam-sunrise.service" <<EOF
|
||||||
"daily sunrise video → Mattermost" \
|
[Unit]
|
||||||
"daily_sunrise_video.sh" \
|
Description=Sky-Cam — $SUNRISE_CAM: daily sunrise video
|
||||||
"After=network-online.target\nWants=network-online.target\n"
|
After=network-online.target
|
||||||
write_timer "sky-cam-sunrise" "daily sunrise video" "*-*-* 09:00:00"
|
Wants=network-online.target
|
||||||
|
OnFailure=sky-cam-notify-failure@%n.service
|
||||||
|
OnSuccess=sky-cam-sunrise-upload.service
|
||||||
|
|
||||||
# Four Seasons daily clip (auto-triggers montage + year-end on last days)
|
[Service]
|
||||||
write_service "sky-cam-4seasons" \
|
Type=oneshot
|
||||||
"Four Seasons daily clip" \
|
ExecStart=$SCRIPT_DIR/daily_sunrise_video.sh
|
||||||
"4-seasons.sh" \
|
StandardOutput=journal
|
||||||
"After=network-online.target\nWants=network-online.target\n"
|
StandardError=journal
|
||||||
write_timer "sky-cam-4seasons" "Four Seasons daily clip" "*-*-* 01:00:00"
|
EOF
|
||||||
|
echo " wrote sky-cam-sunrise.service"
|
||||||
|
|
||||||
# Full-day timelapse with 10-day retention
|
cat > "$UNIT_DIR/sky-cam-sunrise-upload.service" <<EOF
|
||||||
write_service "sky-cam-fullday" \
|
[Unit]
|
||||||
"full-day timelapse" \
|
Description=Sky-Cam — $SUNRISE_CAM: upload daily sunrise to Mattermost
|
||||||
"fullday-video.sh"
|
After=network-online.target
|
||||||
write_timer "sky-cam-fullday" "full-day timelapse" "*-*-* 02:00:00"
|
Wants=network-online.target
|
||||||
|
OnFailure=sky-cam-notify-failure@%n.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=$SCRIPT_DIR/sunrise2mm.py
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
EOF
|
||||||
|
echo " wrote sky-cam-sunrise-upload.service"
|
||||||
|
|
||||||
|
write_timer "sky-cam-sunrise" "$SUNRISE_CAM: daily sunrise video" "$SCHEDULE_SUNRISE"
|
||||||
|
|
||||||
|
timers=(sky-cam-sunrise)
|
||||||
|
|
||||||
|
# ── Per-camera Four Seasons and full-day jobs ─────────────────────────────────
|
||||||
|
# Reads CAMERAS, SCHEDULE_SEASONS_<cam>, SCHEDULE_FULLDAY_<cam> from sky-cam.conf.
|
||||||
|
# Scripts receive the camera name as $1 so they know which camera to process.
|
||||||
|
for cam in "${CAMERAS[@]}"; do
|
||||||
|
sched_seasons_var="SCHEDULE_SEASONS_${cam}"
|
||||||
|
sched_fullday_var="SCHEDULE_FULLDAY_${cam}"
|
||||||
|
|
||||||
|
if [ -n "${!sched_seasons_var:-}" ]; then
|
||||||
|
write_service "sky-cam-seasons-${cam}" \
|
||||||
|
"${cam}: Four Seasons daily clip" \
|
||||||
|
"4-seasons.sh ${cam}" \
|
||||||
|
"After=network-online.target\nWants=network-online.target\n"
|
||||||
|
write_timer "sky-cam-seasons-${cam}" "${cam}: Four Seasons daily clip" \
|
||||||
|
"${!sched_seasons_var}"
|
||||||
|
timers+=("sky-cam-seasons-${cam}")
|
||||||
|
else
|
||||||
|
echo " WARNING: SCHEDULE_SEASONS_${cam} not set — skipping seasons timer for ${cam}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${!sched_fullday_var:-}" ]; then
|
||||||
|
write_service "sky-cam-fullday-${cam}" \
|
||||||
|
"${cam}: full-day timelapse" \
|
||||||
|
"fullday-video.sh ${cam}"
|
||||||
|
write_timer "sky-cam-fullday-${cam}" "${cam}: full-day timelapse" \
|
||||||
|
"${!sched_fullday_var}"
|
||||||
|
timers+=("sky-cam-fullday-${cam}")
|
||||||
|
else
|
||||||
|
echo " WARNING: SCHEDULE_FULLDAY_${cam} not set — skipping fullday timer for ${cam}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# ── Reload and enable ─────────────────────────────────────────────────────────
|
# ── Reload and enable ─────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
$SC daemon-reload
|
$SC daemon-reload
|
||||||
|
|
||||||
for timer in sky-cam-sunrise sky-cam-4seasons sky-cam-fullday; do
|
for timer in "${timers[@]}"; do
|
||||||
$SC enable --now "${timer}.timer" \
|
$SC enable --now "${timer}.timer" \
|
||||||
&& echo " enabled + started ${timer}.timer" \
|
&& echo " enabled + started ${timer}.timer" \
|
||||||
|| echo " WARNING: could not enable ${timer}.timer"
|
|| echo " WARNING: could not enable ${timer}.timer"
|
||||||
@@ -108,8 +156,8 @@ echo ""
|
|||||||
echo "Done. Check status with:"
|
echo "Done. Check status with:"
|
||||||
if $SYSTEM_MODE; then
|
if $SYSTEM_MODE; then
|
||||||
echo " sudo systemctl list-timers 'sky-cam-*'"
|
echo " sudo systemctl list-timers 'sky-cam-*'"
|
||||||
echo " sudo journalctl -u sky-cam-4seasons.service -f"
|
echo " sudo journalctl -u sky-cam-seasons-${CAMERAS[0]}.service -f"
|
||||||
else
|
else
|
||||||
echo " systemctl --user list-timers 'sky-cam-*'"
|
echo " systemctl --user list-timers 'sky-cam-*'"
|
||||||
echo " journalctl --user -u sky-cam-4seasons.service -f"
|
echo " journalctl --user -u sky-cam-seasons-${CAMERAS[0]}.service -f"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+53
-16
@@ -22,14 +22,20 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|||||||
source "$SCRIPT_DIR/sky-cam.conf"
|
source "$SCRIPT_DIR/sky-cam.conf"
|
||||||
export TIMEZONE # make it visible to season_info.py subprocess
|
export TIMEZONE # make it visible to season_info.py subprocess
|
||||||
|
|
||||||
|
# Args: [date] [camera-name]
|
||||||
|
# Camera name: second argument overrides conf (passed through from 4-seasons.sh).
|
||||||
|
CAM_NAME="${2:-$CAM_NAME}"
|
||||||
|
|
||||||
# ── Configuration ──────────────────────────────────────────────────────────────
|
# ── Configuration ──────────────────────────────────────────────────────────────
|
||||||
base_dir="$BASE_DIR"
|
base_dir="$BASE_DIR"
|
||||||
music_base_dir="$MUSIC_DIR"
|
music_base_dir="$MUSIC_DIR"
|
||||||
[ ! -d "$music_base_dir" ] && music_base_dir="$SCRIPT_DIR/music"
|
[ ! -d "$music_base_dir" ] && music_base_dir="$SCRIPT_DIR/music"
|
||||||
|
|
||||||
FADE_DUR=2.0 # video + audio fade in/out (seconds)
|
# Fade/overlay durations come from sky-cam.conf (MONTAGE_FADE_DUR,
|
||||||
ATTR_DUR=6 # attribution overlay duration (seconds from start)
|
# MONTAGE_ATTR_DUR, MONTAGE_ATTR_FADE). Alias for shorter local use.
|
||||||
ATTR_FADE=1 # attribution fade in / fade out duration
|
FADE_DUR="$MONTAGE_FADE_DUR"
|
||||||
|
ATTR_DUR="$MONTAGE_ATTR_DUR"
|
||||||
|
ATTR_FADE="$MONTAGE_ATTR_FADE"
|
||||||
|
|
||||||
# ── Season / movement info ────────────────────────────────────────────────────
|
# ── Season / movement info ────────────────────────────────────────────────────
|
||||||
DATE_ARG="${1:-}"
|
DATE_ARG="${1:-}"
|
||||||
@@ -39,12 +45,14 @@ eval "$(python3 "$SCRIPT_DIR/season_info.py" ${DATE_ARG:+"$DATE_ARG"})"
|
|||||||
echo "Season=$SEASON Mvt=$MVT_NUM Year=$ASTRO_YEAR (ref: ${DATE_ARG:-today})"
|
echo "Season=$SEASON Mvt=$MVT_NUM Year=$ASTRO_YEAR (ref: ${DATE_ARG:-today})"
|
||||||
|
|
||||||
# ── Locate files ──────────────────────────────────────────────────────────────
|
# ── Locate files ──────────────────────────────────────────────────────────────
|
||||||
video_dir="$base_dir/movies/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
video_dir="$MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
||||||
output_dir="$video_dir/montage"
|
output_dir="$video_dir/montage"
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
music_file=$(find "$music_base_dir" -type f -iname "*${SEASON}*Mvt*${MVT_NUM}*" | sort | head -n 1)
|
music_file=$(find "$music_base_dir" -type f -iname "*${SEASON}*Mvt*${MVT_NUM}*" | sort | head -n 1)
|
||||||
if [ -z "$music_file" ]; then
|
if [ -z "$music_file" ]; then
|
||||||
|
"$SCRIPT_DIR/notify.sh" "FAILED: montage $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \
|
||||||
|
"Music file not found in $music_base_dir" || true
|
||||||
echo "ERROR: Music file not found for $SEASON Mvt $MVT_NUM in $music_base_dir"
|
echo "ERROR: Music file not found for $SEASON Mvt $MVT_NUM in $music_base_dir"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -54,11 +62,28 @@ echo "Music: $(basename "$music_file") (${music_duration}s)"
|
|||||||
# ── Collect sorted daily clips ────────────────────────────────────────────────
|
# ── Collect sorted daily clips ────────────────────────────────────────────────
|
||||||
mapfile -d '' daily_clips < <(find "$video_dir" -maxdepth 1 -name "*-final.mp4" -print0 | sort -z)
|
mapfile -d '' daily_clips < <(find "$video_dir" -maxdepth 1 -name "*-final.mp4" -print0 | sort -z)
|
||||||
if [ "${#daily_clips[@]}" -eq 0 ]; then
|
if [ "${#daily_clips[@]}" -eq 0 ]; then
|
||||||
|
"$SCRIPT_DIR/notify.sh" "FAILED: montage $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \
|
||||||
|
"No *-final.mp4 clips found in $video_dir" || true
|
||||||
echo "ERROR: No *-final.mp4 clips in $video_dir"
|
echo "ERROR: No *-final.mp4 clips in $video_dir"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Found ${#daily_clips[@]} daily clips"
|
echo "Found ${#daily_clips[@]} daily clips"
|
||||||
|
|
||||||
|
# ── Validate clip duration sum against music ──────────────────────────────────
|
||||||
|
total_clip_dur=0
|
||||||
|
for clip in "${daily_clips[@]}"; do
|
||||||
|
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$clip")
|
||||||
|
total_clip_dur=$(echo "scale=3; $total_clip_dur + $d" | bc)
|
||||||
|
done
|
||||||
|
clip_drift=$(echo "scale=3; $total_clip_dur - $music_duration" | bc | sed 's/^-//')
|
||||||
|
echo "Sum of clips: ${total_clip_dur}s Music: ${music_duration}s Drift: ${clip_drift}s"
|
||||||
|
if awk "BEGIN{exit !($clip_drift > 2.0)}"; then
|
||||||
|
echo "WARNING: clip total differs from music by ${clip_drift}s — speed pass will compensate"
|
||||||
|
"$SCRIPT_DIR/notify.sh" \
|
||||||
|
"WARNING: $SEASON Mvt$MVT_NUM clip drift ${clip_drift}s ($ASTRO_YEAR)" \
|
||||||
|
"Clips sum ${total_clip_dur}s vs music ${music_duration}s" || true
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Source resolution ─────────────────────────────────────────────────────────
|
# ── Source resolution ─────────────────────────────────────────────────────────
|
||||||
VIDEO_W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "${daily_clips[0]}")
|
VIDEO_W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "${daily_clips[0]}")
|
||||||
VIDEO_H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "${daily_clips[0]}")
|
VIDEO_H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "${daily_clips[0]}")
|
||||||
@@ -96,21 +121,28 @@ ffmpeg -loglevel warning \
|
|||||||
-f concat -safe 0 -i "$concat_list" \
|
-f concat -safe 0 -i "$concat_list" \
|
||||||
-vf "scale=${VIDEO_W}:${VIDEO_H}:force_original_aspect_ratio=decrease,\
|
-vf "scale=${VIDEO_W}:${VIDEO_H}:force_original_aspect_ratio=decrease,\
|
||||||
pad=${VIDEO_W}:${VIDEO_H}:(ow-iw)/2:(oh-ih)/2,setsar=1" \
|
pad=${VIDEO_W}:${VIDEO_H}:(ow-iw)/2:(oh-ih)/2,setsar=1" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -crf 26 -an \
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_MONTAGE" -an \
|
||||||
-y "$temp_concat"
|
-y "$temp_concat"
|
||||||
|
|
||||||
# ── Step 2: Speed-adjust to match music duration exactly ──────────────────────
|
# ── Step 2: Speed-adjust — saved permanently so music/overlay failure is recoverable ─
|
||||||
|
# Deleted automatically if step 3 succeeds.
|
||||||
concat_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_concat")
|
concat_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_concat")
|
||||||
speed_factor=$(echo "scale=6; $concat_dur / $music_duration" | bc)
|
speed_factor=$(echo "scale=6; $concat_dur / $music_duration" | bc)
|
||||||
echo "Step 2/3: speed $speed_factor (raw=${concat_dur}s → music=${music_duration}s)"
|
echo "Step 2/3: speed $speed_factor (raw=${concat_dur}s → music=${music_duration}s)"
|
||||||
|
|
||||||
temp_sped=$(mktemp --suffix=.mp4); TMPFILES+=("$temp_sped")
|
sped_file="$output_dir/${MVT_START}_${SEASON}_Mvt${MVT_NUM}-Sped.mp4"
|
||||||
ffmpeg -loglevel warning \
|
if ! ffmpeg -loglevel warning \
|
||||||
-i "$temp_concat" \
|
-i "$temp_concat" \
|
||||||
-vf "setpts=PTS/$speed_factor" \
|
-vf "setpts=PTS/$speed_factor" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -crf 26 -an \
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_MONTAGE" -an \
|
||||||
-t "$music_duration" -y "$temp_sped"
|
-t "$music_duration" -y "$sped_file"; then
|
||||||
|
rm -f "$sped_file"
|
||||||
|
"$SCRIPT_DIR/notify.sh" "FAILED: montage step 2/3 (speed) $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \
|
||||||
|
"speed-adjust failed — concat temp lost; re-run montage-mvt.sh" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
rm "$temp_concat"
|
rm "$temp_concat"
|
||||||
|
echo "Speed-adjusted: $sped_file (saved — music+overlay still pending)"
|
||||||
|
|
||||||
# ── Step 3: Music + fades + attribution overlay ───────────────────────────────
|
# ── Step 3: Music + fades + attribution overlay ───────────────────────────────
|
||||||
# Attribution overlay: semi-transparent bar across the top for the first
|
# Attribution overlay: semi-transparent bar across the top for the first
|
||||||
@@ -146,16 +178,21 @@ DT="${DT}:alpha='${ALPHA}':enable='${ENABLE}'"
|
|||||||
|
|
||||||
output_file="$output_dir/${MVT_START}_${SEASON}_Mvt${MVT_NUM}-Montage.mp4"
|
output_file="$output_dir/${MVT_START}_${SEASON}_Mvt${MVT_NUM}-Montage.mp4"
|
||||||
|
|
||||||
ffmpeg -loglevel warning \
|
# If this fails, $sped_file survives for manual recovery (re-run step 3 only).
|
||||||
-i "$temp_sped" -i "$music_file" \
|
if ! ffmpeg -loglevel warning \
|
||||||
|
-i "$sped_file" -i "$music_file" \
|
||||||
-filter_complex \
|
-filter_complex \
|
||||||
"[0:v]fade=t=in:st=0:d=${FADE_DUR},fade=t=out:st=${fade_out_start}:d=${FADE_DUR},${DT}[vout];\
|
"[0:v]fade=t=in:st=0:d=${FADE_DUR},fade=t=out:st=${fade_out_start}:d=${FADE_DUR},${DT}[vout];\
|
||||||
[1:a]afade=t=in:st=0:d=${FADE_DUR},afade=t=out:st=${fade_out_start}:d=${FADE_DUR}[aout]" \
|
[1:a]afade=t=in:st=0:d=${FADE_DUR},afade=t=out:st=${fade_out_start}:d=${FADE_DUR}[aout]" \
|
||||||
-map "[vout]" -map "[aout]" \
|
-map "[vout]" -map "[aout]" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -c:a aac -b:a 192k \
|
-c:v libx264 -pix_fmt yuv420p -c:a aac -b:a "$AUDIO_BITRATE" \
|
||||||
-t "$music_duration" -y "$output_file"
|
-t "$music_duration" -y "$output_file"; then
|
||||||
rm "$temp_sped"
|
"$SCRIPT_DIR/notify.sh" "FAILED: montage step 3/3 (music+overlay) $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \
|
||||||
|
"Music/overlay failed — sped video saved: $(basename "$sped_file")" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$sped_file"
|
||||||
echo "Done: $output_file"
|
echo "Done: $output_file"
|
||||||
|
|
||||||
# ── Notify ────────────────────────────────────────────────────────────────────
|
# ── Notify ────────────────────────────────────────────────────────────────────
|
||||||
@@ -167,5 +204,5 @@ echo "Done: $output_file"
|
|||||||
# ── Trigger year-end join on last Autumn movement ────────────────────────────
|
# ── Trigger year-end join on last Autumn movement ────────────────────────────
|
||||||
if [ "$SEASON" = "Autumn" ] && [ "$MVT_NUM" = "3" ]; then
|
if [ "$SEASON" = "Autumn" ] && [ "$MVT_NUM" = "3" ]; then
|
||||||
echo "Last movement of astronomical year $ASTRO_YEAR — triggering year-end join..."
|
echo "Last movement of astronomical year $ASTRO_YEAR — triggering year-end join..."
|
||||||
"$SCRIPT_DIR/year-end-join.sh" "$ASTRO_YEAR"
|
"$SCRIPT_DIR/year-end-join.sh" "$ASTRO_YEAR" "$CAM_NAME"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+125
-63
@@ -20,13 +20,14 @@
|
|||||||
# Then edit ExecStart= in each .service file to match your SCRIPT_DIR, then:
|
# Then edit ExecStart= in each .service file to match your SCRIPT_DIR, then:
|
||||||
#
|
#
|
||||||
# systemctl --user daemon-reload
|
# systemctl --user daemon-reload
|
||||||
# systemctl --user enable --now sky-cam-sunrise.timer # 09:00 daily
|
# systemctl --user enable --now sky-cam-sunrise.timer
|
||||||
# systemctl --user enable --now sky-cam-4seasons.timer # 01:00 daily
|
# systemctl --user enable --now sky-cam-seasons-sunrise.timer
|
||||||
# systemctl --user enable --now sky-cam-fullday.timer # 02:00 daily
|
# systemctl --user enable --now sky-cam-fullday-sunrise.timer
|
||||||
|
# # one sky-cam-seasons-<cam>.timer and sky-cam-fullday-<cam>.timer per camera
|
||||||
#
|
#
|
||||||
# Check status:
|
# Check status:
|
||||||
# systemctl --user list-timers 'sky-cam-*'
|
# systemctl --user list-timers 'sky-cam-*'
|
||||||
# journalctl --user -u sky-cam-4seasons.service -f
|
# journalctl --user -u sky-cam-seasons-sunrise.service -f
|
||||||
#
|
#
|
||||||
# System-wide (replace --user with no flag, prepend sudo):
|
# System-wide (replace --user with no flag, prepend sudo):
|
||||||
# sudo cp systemd/*.service systemd/*.timer /etc/systemd/system/
|
# sudo cp systemd/*.service systemd/*.timer /etc/systemd/system/
|
||||||
@@ -37,44 +38,44 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# You run / schedule:
|
# You run / schedule:
|
||||||
# daily_sunrise_video.sh — daily at 09:00 (via systemd)
|
# daily_sunrise_video.sh — daily at SCHEDULE_SUNRISE (via systemd, SUNRISE_CAM only)
|
||||||
# grabs today's sunrise images, makes a 10-second
|
# grabs images around sunrise, makes a short video,
|
||||||
# video, uploads it to Mattermost
|
# uploads it to Mattermost
|
||||||
#
|
#
|
||||||
# 4-seasons.sh — daily at 01:00 (via systemd)
|
# 4-seasons.sh <cam> — daily at SCHEDULE_SEASONS_<cam> (via systemd, per camera)
|
||||||
# makes yesterday's daily clip sized to its share
|
# makes yesterday's daily clip sized to its share
|
||||||
# of the matching Vivaldi movement's music duration;
|
# of the matching Vivaldi movement's music duration;
|
||||||
# on the last day of a movement, auto-triggers
|
# on the last day of a movement, auto-triggers
|
||||||
# montage-mvt.sh
|
# montage-mvt.sh
|
||||||
#
|
#
|
||||||
# fullday-video.sh — daily at 02:00 (via systemd)
|
# fullday-video.sh <cam> — daily at SCHEDULE_FULLDAY_<cam> (via systemd, per camera)
|
||||||
# full-day timelapse at fixed fps; deletes files
|
# full-day timelapse at fixed fps; deletes videos
|
||||||
# older than 10 days automatically
|
# older than RETENTION_DAYS automatically
|
||||||
#
|
#
|
||||||
# install.sh — run once at setup, and again if this file changes
|
# install.sh — run once at setup, and again if this file changes
|
||||||
# generates and installs systemd units from conf
|
# generates and installs systemd units from conf
|
||||||
#
|
#
|
||||||
# stitch-cameras.py — run manually when needed
|
# stitch-cameras.py — run manually when needed
|
||||||
# stitches north + sunrise into a panorama
|
# stitches north + sunrise into a panorama
|
||||||
# usage: python3 stitch-cameras.py north.jpg sunrise.jpg prefix [focal]
|
# usage: python3 stitch-cameras.py north.jpg sunrise.jpg prefix [focal]
|
||||||
#
|
#
|
||||||
# Auto-triggered (do not run directly):
|
# Auto-triggered (do not run directly):
|
||||||
# montage-mvt.sh — called by 4-seasons.sh on last day of each movement
|
# montage-mvt.sh — called by 4-seasons.sh on last day of each movement
|
||||||
# concatenates all daily clips for the movement,
|
# concatenates all daily clips for the movement,
|
||||||
# speed-adjusts to match music, overlays attribution,
|
# speed-adjusts to match music, overlays attribution,
|
||||||
# sends completion notification; on last Autumn
|
# sends completion notification; on last Autumn
|
||||||
# movement also triggers year-end-join.sh
|
# movement also triggers year-end-join.sh
|
||||||
#
|
#
|
||||||
# year-end-join.sh — called by montage-mvt.sh at end of Autumn Mvt 3
|
# year-end-join.sh — called by montage-mvt.sh at end of Autumn Mvt 3
|
||||||
# concatenates all 12 movement montages into one
|
# concatenates all 12 movement montages into one
|
||||||
# Four Seasons year video, sends notification
|
# Four Seasons year video, sends notification
|
||||||
#
|
#
|
||||||
# Helpers (called by the scripts above, not run directly):
|
# Helpers (called by the scripts above, not run directly):
|
||||||
# notify.sh — sends notifications via ntfy / email / Mattermost
|
# notify.sh — sends notifications via ntfy / email / Mattermost
|
||||||
# season_info.py — outputs astronomical season/movement/year for a date
|
# season_info.py — outputs astronomical season/movement/year for a date
|
||||||
# sunrise.py — outputs today's sunrise time (used by daily_sunrise_video)
|
# sunrise.py — outputs today's sunrise time (used by daily_sunrise_video)
|
||||||
# sunrise2mm.py — uploads the sunrise video to Mattermost
|
# sunrise2mm.py — uploads the sunrise video to Mattermost
|
||||||
# sunrise_overlay.py — burns a timestamp overlay onto the sunrise video
|
# sunrise_overlay.py — burns a timestamp overlay onto the sunrise video
|
||||||
#
|
#
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# FILE DEPENDENCIES — who calls whom, and what to update if you rename a file
|
# FILE DEPENDENCIES — who calls whom, and what to update if you rename a file
|
||||||
@@ -92,20 +93,20 @@
|
|||||||
# and sky-cam-notify-failure@.service
|
# and sky-cam-notify-failure@.service
|
||||||
#
|
#
|
||||||
# daily_sunrise_video.sh
|
# daily_sunrise_video.sh
|
||||||
# ← called by systemd sky-cam-sunrise.timer (09:00)
|
# ← called by systemd sky-cam-sunrise.timer (SCHEDULE_SUNRISE)
|
||||||
# → if renamed: update install.sh (write_service call, line ~80)
|
# → if renamed: update install.sh (write_service call ~line 70)
|
||||||
# then re-run install.sh to regenerate the unit
|
# then re-run install.sh to regenerate the unit
|
||||||
# calls → sunrise.py, sunrise_overlay.py, sunrise2mm.py
|
# calls → sunrise.py, sunrise_overlay.py, sunrise2mm.py
|
||||||
#
|
#
|
||||||
# 4-seasons.sh
|
# 4-seasons.sh
|
||||||
# ← called by systemd sky-cam-4seasons.timer (01:00)
|
# ← called by systemd sky-cam-seasons-<cam>.timer (SCHEDULE_SEASONS_<cam>)
|
||||||
# → if renamed: update install.sh (write_service call, line ~86)
|
# → if renamed: update install.sh (write_service call ~line 85)
|
||||||
# then re-run install.sh
|
# then re-run install.sh
|
||||||
# calls → season_info.py, montage-mvt.sh
|
# calls → season_info.py, montage-mvt.sh
|
||||||
#
|
#
|
||||||
# fullday-video.sh
|
# fullday-video.sh
|
||||||
# ← called by systemd sky-cam-fullday.timer (02:00)
|
# ← called by systemd sky-cam-fullday-<cam>.timer (SCHEDULE_FULLDAY_<cam>)
|
||||||
# → if renamed: update install.sh (write_service call, line ~93)
|
# → if renamed: update install.sh (write_service call ~line 93)
|
||||||
# then re-run install.sh
|
# then re-run install.sh
|
||||||
#
|
#
|
||||||
# montage-mvt.sh
|
# montage-mvt.sh
|
||||||
@@ -149,55 +150,116 @@
|
|||||||
# Full path to the directory containing this file and all the scripts.
|
# Full path to the directory containing this file and all the scripts.
|
||||||
SCRIPT_DIR=/home/motion/drives/local-2tb/sunrise-scripts
|
SCRIPT_DIR=/home/motion/drives/local-2tb/sunrise-scripts
|
||||||
|
|
||||||
# Camera name — subfolder under BASE_DIR (images) and BASE_DIR/movies (videos).
|
|
||||||
# E.g. "sunrise" → images: $BASE_DIR/sunrise/YYYY-MM-DD/ videos: $BASE_DIR/movies/sunrise/
|
|
||||||
CAM_NAME=sunrise
|
|
||||||
|
|
||||||
# ── Storage ───────────────────────────────────────────────────────────────────
|
# ── Storage ───────────────────────────────────────────────────────────────────
|
||||||
BASE_DIR=/home/motion/drives/local-2tb
|
BASE_DIR=/home/motion/drives/local-2tb
|
||||||
|
MOVIES_DIR=$BASE_DIR/movies # override if videos live on a different drive
|
||||||
MUSIC_DIR=/home/motion/drives/local-2tb/music/4 Seasons
|
MUSIC_DIR=/home/motion/drives/local-2tb/music/4 Seasons
|
||||||
|
|
||||||
# ── Location & timezone ───────────────────────────────────────────────────────
|
# ── Location & timezone ───────────────────────────────────────────────────────
|
||||||
# Used by sunrise.py to calculate sunrise time each day.
|
# Used by sunrise.py to calculate sunrise time each day.
|
||||||
# TIMEZONE must be a valid IANA name: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
#
|
||||||
LATITUDE=38.123456
|
# To find your coordinates:
|
||||||
LONGITUDE=-97.654321
|
# Google Maps — right-click your location → the first item shown is "lat, lon"
|
||||||
|
# Or: maps.google.com, drop a pin, coordinates appear in the URL and sidebar
|
||||||
|
#
|
||||||
|
# TIMEZONE must be a valid IANA name. Look yours up at:
|
||||||
|
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
|
# (use the value in the "TZ identifier" column, e.g. America/Chicago)
|
||||||
|
#
|
||||||
|
LATITUDE=25.0000
|
||||||
|
LONGITUDE=-38.0000
|
||||||
TIMEZONE=America/New_York
|
TIMEZONE=America/New_York
|
||||||
|
|
||||||
|
# ── Cameras ───────────────────────────────────────────────────────────────────
|
||||||
|
# List every camera name here (space-separated inside the parentheses).
|
||||||
|
# Each name becomes a subfolder under BASE_DIR (images) and BASE_DIR/movies.
|
||||||
|
# To add a camera: add its name here, add its schedules below, re-run install.sh.
|
||||||
|
#
|
||||||
|
CAMERAS=(sunrise) # e.g. CAMERAS=(sunrise north west)
|
||||||
|
SUNRISE_CAM=sunrise # which camera faces east (gets the sunrise video job)
|
||||||
|
|
||||||
|
# ── Schedules ─────────────────────────────────────────────────────────────────
|
||||||
|
# SCHEDULE_SUNRISE when to run daily_sunrise_video.sh (SUNRISE_CAM only)
|
||||||
|
# SCHEDULE_SEASONS_<cam> when to run 4-seasons.sh for each camera
|
||||||
|
# SCHEDULE_FULLDAY_<cam> when to run fullday-video.sh for each camera
|
||||||
|
#
|
||||||
|
# Times are HH:MM:SS (24-hour). Space cameras at least 30 min apart.
|
||||||
|
# install.sh reads these and generates one systemd timer per camera per job.
|
||||||
|
#
|
||||||
|
SCHEDULE_SUNRISE=09:00:00
|
||||||
|
|
||||||
|
SCHEDULE_SEASONS_sunrise=01:00:00
|
||||||
|
SCHEDULE_FULLDAY_sunrise=02:00:00
|
||||||
|
|
||||||
|
# Uncomment and fill in for each camera you add to CAMERAS above:
|
||||||
|
#SCHEDULE_SEASONS_north=01:30:00
|
||||||
|
#SCHEDULE_FULLDAY_north=02:30:00
|
||||||
|
#
|
||||||
|
#SCHEDULE_SEASONS_west=02:00:00
|
||||||
|
#SCHEDULE_FULLDAY_west=03:00:00
|
||||||
|
|
||||||
# ── Sunrise video tuning ──────────────────────────────────────────────────────
|
# ── Sunrise video tuning ──────────────────────────────────────────────────────
|
||||||
# How many minutes before/after sunrise to include in the daily clip.
|
# How many minutes before/after sunrise to include in the daily clip.
|
||||||
SUNRISE_PRE_MIN=70
|
SUNRISE_PRE_MIN=70
|
||||||
SUNRISE_POST_MIN=10
|
SUNRISE_POST_MIN=10
|
||||||
# Target duration (seconds) for the speed-adjusted sunrise video.
|
# Target duration (seconds) for the speed-adjusted sunrise video.
|
||||||
SUNRISE_TARGET_SECS=10
|
SUNRISE_TARGET_SECS=10
|
||||||
|
# Opacity of the sunrise-time overlay (0.0 = invisible, 1.0 = fully opaque).
|
||||||
|
SUNRISE_OVERLAY_OPACITY=0.45
|
||||||
|
|
||||||
# ── Full-day timelapse tuning ─────────────────────────────────────────────────
|
# ── Full-day timelapse tuning ─────────────────────────────────────────────────
|
||||||
FULLDAY_FPS=5 # frame rate of the timelapse video
|
FULLDAY_FPS=5 # frame rate of the timelapse video
|
||||||
RETENTION_DAYS=10 # delete full-day videos older than this many days
|
RETENTION_DAYS=10 # delete full-day videos older than this many days
|
||||||
|
|
||||||
|
# ── Video encoding quality ────────────────────────────────────────────────────
|
||||||
|
# FFmpeg CRF: lower = higher quality / larger files. Typical range 18–30.
|
||||||
|
# 18 ≈ visually lossless 23 = ffmpeg default 28 = smaller/lower
|
||||||
|
# Different scripts use different defaults by design — intermediate/draft
|
||||||
|
# encodings use a higher CRF (smaller files, quality less critical), while
|
||||||
|
# final "polished" outputs use a lower CRF for better quality.
|
||||||
|
# Each is independent — changing one does NOT change the others.
|
||||||
|
#
|
||||||
|
CRF_SUNRISE=28 # daily_sunrise_video.sh — daily Mattermost upload
|
||||||
|
CRF_FULLDAY=28 # fullday-video.sh — full-day timelapse
|
||||||
|
CRF_SEASONS_RAW=28 # 4-seasons.sh — raw concat before speed-adjust
|
||||||
|
CRF_SEASONS_FINAL=26 # 4-seasons.sh — final daily clip (goes into montage)
|
||||||
|
CRF_MONTAGE=26 # montage-mvt.sh — concat and speed-adjust and final
|
||||||
|
AUDIO_BITRATE=192k # montage-mvt.sh — music track on movement montages
|
||||||
|
|
||||||
|
# ── Montage attribution overlay ───────────────────────────────────────────────
|
||||||
|
# A translucent bar across the top of each movement montage, naming the
|
||||||
|
# music source ("The Four Seasons — Antonio Vivaldi ...").
|
||||||
|
# Appears for MONTAGE_ATTR_DUR seconds from the start, with ATTR_FADE-second
|
||||||
|
# fade in and fade out. FADE_DUR controls the video/audio fade in/out at
|
||||||
|
# the very start and very end of the montage.
|
||||||
|
#
|
||||||
|
MONTAGE_FADE_DUR=2.0 # video + audio fade in/out (seconds)
|
||||||
|
MONTAGE_ATTR_DUR=6 # attribution overlay total duration (seconds)
|
||||||
|
MONTAGE_ATTR_FADE=1 # attribution fade-in and fade-out length (seconds)
|
||||||
|
|
||||||
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
|
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
|
||||||
mattermost_url=https://your-mattermost-server.example.com
|
mattermost_url=https://your-mattermost-server.example.com
|
||||||
access_token=your-access-token-here
|
access_token=your-access-token-here
|
||||||
channel_id=your-daily-upload-channel-id-here
|
channel_id=your-daily-upload-channel-id-here
|
||||||
|
|
||||||
# ── Notifications — montage / year-end complete + any job failure ─────────────
|
# ── Notifications — montage / year-end complete + any job failure ─────────────
|
||||||
# Notifications fire when a movement montage or the year video finishes.
|
# Notifications fire when a movement montage or the year video finishes,
|
||||||
# They also fire automatically via systemd OnFailure= if any daily job fails.
|
# and via systemd OnFailure= if any daily job fails.
|
||||||
# Enable one or more methods below.
|
# Uncomment and configure one or more methods to enable.
|
||||||
|
|
||||||
# ntfy — push notifications, zero signup required for self-hosted:
|
# ntfy — push notifications, zero signup for self-hosted or free cloud tier:
|
||||||
# https://docs.ntfy.sh/install/
|
# Self-hosted: https://docs.ntfy.sh/install/
|
||||||
# or use the free cloud tier at ntfy.sh (pick any topic name)
|
# Cloud: pick any topic name at ntfy.sh (no account needed)
|
||||||
NTFY_ENABLED=false
|
#NTFY_ENABLED=true
|
||||||
NTFY_URL=https://ntfy.sh/your-topic-here
|
#NTFY_URL=https://ntfy.sh/your-topic-here
|
||||||
|
|
||||||
# Email — requires the 'mail' command on the system (package: mailutils or s-nail).
|
# Email — requires the 'mail' command (package: mailutils or s-nail).
|
||||||
# For outbound SMTP configure /etc/ssmtp/ssmtp.conf or msmtp.
|
# Configure outbound SMTP via /etc/ssmtp/ssmtp.conf or msmtp.
|
||||||
EMAIL_ENABLED=false
|
#EMAIL_ENABLED=true
|
||||||
EMAIL_TO=you@example.com
|
#EMAIL_TO=you@example.com
|
||||||
EMAIL_FROM=skycam@localhost
|
#EMAIL_FROM=skycam@localhost
|
||||||
|
|
||||||
# Mattermost text post — reuses the URL and token above, but posts to this
|
# Mattermost text post — reuses mattermost_url and access_token above.
|
||||||
# channel (can be the same daily channel or a separate private/admin channel).
|
# Can be the same daily channel or a separate admin/private channel.
|
||||||
MM_NOTIFY_ENABLED=false
|
#MM_NOTIFY_ENABLED=true
|
||||||
MM_NOTIFY_CHANNEL_ID=your-notify-channel-id-here
|
#MM_NOTIFY_CHANNEL_ID=your-notify-channel-id-here
|
||||||
|
|||||||
Regular → Executable
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -45,7 +46,7 @@ today_date_str = today.strftime("%Y-%m-%d") # e.g., "2024-11-05"
|
|||||||
today_month_str = today.strftime("%Y-%m") # e.g., "2024-11"
|
today_month_str = today.strftime("%Y-%m") # e.g., "2024-11"
|
||||||
|
|
||||||
# Set the full folder path for today's video
|
# Set the full folder path for today's video
|
||||||
base_video_folder = os.path.join(config.get('BASE_DIR', ''), 'movies', config.get('CAM_NAME', 'sunrise'))
|
base_video_folder = os.path.join(config.get('MOVIES_DIR', ''), config.get('CAM_NAME', 'sunrise'))
|
||||||
output_dir = os.path.join(base_video_folder, today_month_str, "sunrise-only")
|
output_dir = os.path.join(base_video_folder, today_month_str, "sunrise-only")
|
||||||
|
|
||||||
# Check if the directory exists
|
# Check if the directory exists
|
||||||
|
|||||||
+24
-6
@@ -8,7 +8,7 @@
|
|||||||
# The 12 individual movement videos already carry their own attribution overlay
|
# The 12 individual movement videos already carry their own attribution overlay
|
||||||
# (first 6 seconds of each), so no extra card is appended here.
|
# (first 6 seconds of each), so no extra card is appended here.
|
||||||
#
|
#
|
||||||
# Output: $BASE_DIR/movies/$CAM_NAME/$ASTRO_YEAR/$CAM_NAME-$ASTRO_YEAR.mp4
|
# Output: $MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR/$CAM_NAME-$ASTRO_YEAR.mp4
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -17,11 +17,13 @@ source "$SCRIPT_DIR/sky-cam.conf"
|
|||||||
|
|
||||||
ASTRO_YEAR="${1:-}"
|
ASTRO_YEAR="${1:-}"
|
||||||
if [ -z "$ASTRO_YEAR" ]; then
|
if [ -z "$ASTRO_YEAR" ]; then
|
||||||
echo "Usage: $0 <ASTRO_YEAR> (e.g. $0 2025)"
|
echo "Usage: $0 <ASTRO_YEAR> [CAM_NAME] (e.g. $0 2025 north)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# Camera name: second argument overrides conf (passed through from montage-mvt.sh).
|
||||||
|
CAM_NAME="${2:-$CAM_NAME}"
|
||||||
|
|
||||||
year_dir="$BASE_DIR/movies/$CAM_NAME/$ASTRO_YEAR"
|
year_dir="$MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR"
|
||||||
|
|
||||||
echo "Assembling Four Seasons $ASTRO_YEAR from $year_dir ..."
|
echo "Assembling Four Seasons $ASTRO_YEAR from $year_dir ..."
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ concat_list=$(mktemp --suffix=.txt)
|
|||||||
trap 'rm -f "$concat_list"' EXIT
|
trap 'rm -f "$concat_list"' EXIT
|
||||||
|
|
||||||
missing=0
|
missing=0
|
||||||
|
total_expected_dur=0
|
||||||
for season in Winter Spring Summer Autumn; do
|
for season in Winter Spring Summer Autumn; do
|
||||||
for mvt in 1 2 3; do
|
for mvt in 1 2 3; do
|
||||||
mvt_dir="$year_dir/$season/Mvt$mvt/montage"
|
mvt_dir="$year_dir/$season/Mvt$mvt/montage"
|
||||||
@@ -39,11 +42,14 @@ for season in Winter Spring Summer Autumn; do
|
|||||||
echo "WARNING: missing montage for $season Mvt$mvt ($mvt_dir)"
|
echo "WARNING: missing montage for $season Mvt$mvt ($mvt_dir)"
|
||||||
missing=$((missing + 1))
|
missing=$((missing + 1))
|
||||||
else
|
else
|
||||||
echo " $season Mvt$mvt → $(basename "$f")"
|
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f")
|
||||||
|
total_expected_dur=$(echo "scale=3; $total_expected_dur + $d" | bc)
|
||||||
|
echo " $season Mvt$mvt → $(basename "$f") (${d}s)"
|
||||||
printf "file '%s'\n" "$f" >> "$concat_list"
|
printf "file '%s'\n" "$f" >> "$concat_list"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
echo "Expected total duration: ${total_expected_dur}s ($(echo "scale=1; $total_expected_dur/60" | bc) min)"
|
||||||
|
|
||||||
found=$(wc -l < "$concat_list")
|
found=$(wc -l < "$concat_list")
|
||||||
echo "$found of 12 movements found ($missing missing)"
|
echo "$found of 12 movements found ($missing missing)"
|
||||||
@@ -61,14 +67,26 @@ fi
|
|||||||
output_file="$year_dir/${CAM_NAME}-${ASTRO_YEAR}.mp4"
|
output_file="$year_dir/${CAM_NAME}-${ASTRO_YEAR}.mp4"
|
||||||
echo "Output: $output_file"
|
echo "Output: $output_file"
|
||||||
|
|
||||||
ffmpeg -loglevel warning \
|
if ! ffmpeg -loglevel warning \
|
||||||
-f concat -safe 0 -i "$concat_list" \
|
-f concat -safe 0 -i "$concat_list" \
|
||||||
-c copy -y "$output_file"
|
-c copy -y "$output_file"; then
|
||||||
|
"$SCRIPT_DIR/notify.sh" "FAILED: year-end join $CAM_NAME $ASTRO_YEAR" \
|
||||||
|
"ffmpeg concat failed — the 12 movement montages are still intact" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
total_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$output_file")
|
total_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$output_file")
|
||||||
total_min=$(echo "scale=1; $total_dur / 60" | bc)
|
total_min=$(echo "scale=1; $total_dur / 60" | bc)
|
||||||
echo "Done: $output_file (${total_min} min)"
|
echo "Done: $output_file (${total_min} min)"
|
||||||
|
|
||||||
|
# Sanity-check actual vs expected duration
|
||||||
|
dur_drift=$(echo "scale=3; $total_dur - $total_expected_dur" | bc | sed 's/^-//')
|
||||||
|
if awk "BEGIN{exit !($dur_drift > 2.0)}"; then
|
||||||
|
echo "WARNING: output ${total_dur}s differs from expected ${total_expected_dur}s by ${dur_drift}s"
|
||||||
|
"$SCRIPT_DIR/notify.sh" "WARNING: year-end duration drift $CAM_NAME $ASTRO_YEAR" \
|
||||||
|
"Output ${total_dur}s vs expected ${total_expected_dur}s (drift ${dur_drift}s)" || true
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Notify ────────────────────────────────────────────────────────────────────
|
# ── Notify ────────────────────────────────────────────────────────────────────
|
||||||
"$SCRIPT_DIR/notify.sh" \
|
"$SCRIPT_DIR/notify.sh" \
|
||||||
"Four Seasons $ASTRO_YEAR complete" \
|
"Four Seasons $ASTRO_YEAR complete" \
|
||||||
|
|||||||
Reference in New Issue
Block a user