#!/bin/bash # moon-track.sh — nightly: detect the moon in each east night frame, crop a # fixed-size box around it, stitch the cropped sequence into an mp4 that holds # the moon roughly centred while clouds, halo and stars drift past. # # Run once per day from systemd around 02:00, after the night the moon was up. # Processes YESTERDAY's frames by default; pass YYYY-MM-DD to back-fill. # # Output: $MOVIES_DIR//moon-track/YYYY/YYYY-MM-DD-moon-track.mp4 set -euo pipefail SCRIPT_DIR="$(dirname "$(realpath "$0")")" source "$SCRIPT_DIR/sky-cam.conf" export TZ="$TIMEZONE" CAM_NAME="${1:-${SUNRISE_CAM:-east}}" DATE_ARG="${2:-}" if [ -n "$DATE_ARG" ]; then target_date=$(date --date="$DATE_ARG" +%Y-%m-%d) else target_date=$(date --date="yesterday" +%Y-%m-%d) fi if [ "${MOON_TRACK_ENABLED:-true}" != "true" ]; then echo "MOON_TRACK_ENABLED=false — skipping" exit 0 fi image_dir="$BASE_DIR/$CAM_NAME/$target_date" if [ ! -d "$image_dir" ]; then echo "no images for $CAM_NAME on $target_date — skipping" exit 0 fi year="${target_date:0:4}" out_dir="$MOVIES_DIR/$CAM_NAME/moon-track/$year" mkdir -p "$out_dir" final="$out_dir/${target_date}-moon-track.mp4" # Crop size around the detected moon, in source pixels. At 4K with a ~38 px # moon, 480 px gives a comfortable surround with halo and any clouds visible. CROP_PX="${MOON_TRACK_CROP_PX:-480}" FPS="${MOON_TRACK_FPS:-12}" CRF="${MOON_TRACK_CRF:-24}" work_dir=$(mktemp -d) trap 'rm -rf "$work_dir"' EXIT export SCRIPT_DIR echo "Scanning $image_dir for night frames with the moon visible..." python3 - "$image_dir" "$work_dir" "$CROP_PX" <<'PY' import os, sys sys.path.insert(0, os.environ['SCRIPT_DIR']) from PIL import Image from moon_detect import detect_moon image_dir = sys.argv[1] work_dir = sys.argv[2] crop_px = int(sys.argv[3]) half = crop_px // 2 frames = sorted(f for f in os.listdir(image_dir) if f.endswith('.jpg')) n = len(frames) written = 0 for i, name in enumerate(frames): p = os.path.join(image_dir, name) det = detect_moon(p) if det is None: continue cx, cy = det.centroid_xy im = Image.open(p) w, h = im.size left = max(0, min(int(cx - half), w - crop_px)) top = max(0, min(int(cy - half), h - crop_px)) crop = im.crop((left, top, left + crop_px, top + crop_px)) out = os.path.join(work_dir, f'{written:06d}.jpg') crop.save(out, quality=88) written += 1 if (i + 1) % 200 == 0 or i + 1 == n: print(f' scanned {i + 1}/{n} frames, kept {written}', flush=True) print(f'moon-detected frames: {written}') PY count=$(find "$work_dir" -maxdepth 1 -name '*.jpg' | wc -l) if [ "$count" -lt 10 ]; then echo "only $count moon-visible frames on $target_date — skipping video build" exit 0 fi echo "encoding $count frames into $final ..." ffmpeg -loglevel warning -y \ -framerate "$FPS" \ -i "$work_dir/%06d.jpg" \ -c:v libx264 -pix_fmt yuv420p -preset "${ENCODE_PRESET:-slow}" \ -crf "$CRF" \ "$final" echo "moon-track: $final ($(du -h "$final" | cut -f1))" # Retention sweep RETENTION="${MOON_TRACK_RETENTION_DAYS:-90}" if [ "$RETENTION" -gt 0 ]; then find "$MOVIES_DIR/$CAM_NAME/moon-track" -type f -name '*-moon-track.mp4' \ -mtime +"$RETENTION" -delete 2>/dev/null || true fi