diff --git a/4-seasons.sh b/4-seasons.sh index 8502960..ceb6c4e 100755 --- a/4-seasons.sh +++ b/4-seasons.sh @@ -14,6 +14,9 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")" source "$SCRIPT_DIR/sky-cam.conf" 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 ────────────────────────────────────────────────────────────── base_dir="$BASE_DIR" music_base_dir="$MUSIC_DIR" @@ -102,5 +105,5 @@ if [ "$IS_LAST_DAY" = "true" ]; then echo "Last day of $SEASON Mvt $MVT_NUM — triggering montage compilation..." # Pass $yesterday so montage-mvt.sh looks up the correct 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 diff --git a/fullday-video.sh b/fullday-video.sh index e3bb633..c5418f0 100755 --- a/fullday-video.sh +++ b/fullday-video.sh @@ -10,6 +10,9 @@ set -euo pipefail SCRIPT_DIR="$(dirname "$(realpath "$0")")" source "$SCRIPT_DIR/sky-cam.conf" +# Camera name: first argument overrides conf (systemd passes it via ExecStart). +CAM_NAME="${1:-$CAM_NAME}" + # ── Configuration ────────────────────────────────────────────────────────────── base_dir="$BASE_DIR" # FULLDAY_FPS and RETENTION_DAYS come from sky-cam.conf diff --git a/install.sh b/install.sh index 0dfb66d..ec7b455 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,8 @@ # ./install.sh --system # installs to /etc/systemd/system (needs sudo) # # 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 @@ -33,7 +34,7 @@ write_service() { local unit="$1" description="$2" script="$3" extra="${4:-}" cat > "$UNIT_DIR/${unit}.service" < "$UNIT_DIR/${unit}.timer" <, SCHEDULE_FULLDAY_ 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 ───────────────────────────────────────────────────────── echo "" $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" \ && echo " enabled + started ${timer}.timer" \ || echo " WARNING: could not enable ${timer}.timer" @@ -108,8 +128,8 @@ echo "" echo "Done. Check status with:" if $SYSTEM_MODE; then 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 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 diff --git a/montage-mvt.sh b/montage-mvt.sh index eac3436..0b9001c 100755 --- a/montage-mvt.sh +++ b/montage-mvt.sh @@ -22,6 +22,10 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")" source "$SCRIPT_DIR/sky-cam.conf" 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 ────────────────────────────────────────────────────────────── base_dir="$BASE_DIR" music_base_dir="$MUSIC_DIR" @@ -167,5 +171,5 @@ echo "Done: $output_file" # ── Trigger year-end join on last Autumn movement ──────────────────────────── if [ "$SEASON" = "Autumn" ] && [ "$MVT_NUM" = "3" ]; then 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 diff --git a/sky-cam.conf b/sky-cam.conf index 57b1d4a..7feaf7f 100644 --- a/sky-cam.conf +++ b/sky-cam.conf @@ -20,13 +20,14 @@ # Then edit ExecStart= in each .service file to match your SCRIPT_DIR, then: # # systemctl --user daemon-reload -# systemctl --user enable --now sky-cam-sunrise.timer # 09:00 daily -# systemctl --user enable --now sky-cam-4seasons.timer # 01:00 daily -# systemctl --user enable --now sky-cam-fullday.timer # 02:00 daily +# systemctl --user enable --now sky-cam-sunrise.timer +# systemctl --user enable --now sky-cam-seasons-sunrise.timer +# systemctl --user enable --now sky-cam-fullday-sunrise.timer +# # one sky-cam-seasons-.timer and sky-cam-fullday-.timer per camera # # Check status: # 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): # sudo cp systemd/*.service systemd/*.timer /etc/systemd/system/ @@ -37,44 +38,44 @@ # ----------------------------------------------------------------------------- # # You run / schedule: -# daily_sunrise_video.sh — daily at 09:00 (via systemd) -# grabs today's sunrise images, makes a 10-second -# video, uploads it to Mattermost +# daily_sunrise_video.sh — daily at SCHEDULE_SUNRISE (via systemd, SUNRISE_CAM only) +# grabs images around sunrise, makes a short video, +# uploads it to Mattermost # -# 4-seasons.sh — daily at 01:00 (via systemd) -# makes yesterday's daily clip sized to its share -# of the matching Vivaldi movement's music duration; -# on the last day of a movement, auto-triggers -# montage-mvt.sh +# 4-seasons.sh — daily at SCHEDULE_SEASONS_ (via systemd, per camera) +# makes yesterday's daily clip sized to its share +# of the matching Vivaldi movement's music duration; +# on the last day of a movement, auto-triggers +# montage-mvt.sh # -# fullday-video.sh — daily at 02:00 (via systemd) -# full-day timelapse at fixed fps; deletes files -# older than 10 days automatically +# fullday-video.sh — daily at SCHEDULE_FULLDAY_ (via systemd, per camera) +# full-day timelapse at fixed fps; deletes videos +# older than RETENTION_DAYS automatically # -# install.sh — run once at setup, and again if this file changes -# generates and installs systemd units from conf +# install.sh — run once at setup, and again if this file changes +# generates and installs systemd units from conf # -# stitch-cameras.py — run manually when needed -# stitches north + sunrise into a panorama -# usage: python3 stitch-cameras.py north.jpg sunrise.jpg prefix [focal] +# stitch-cameras.py — run manually when needed +# stitches north + sunrise into a panorama +# usage: python3 stitch-cameras.py north.jpg sunrise.jpg prefix [focal] # # Auto-triggered (do not run directly): -# montage-mvt.sh — called by 4-seasons.sh on last day of each movement -# concatenates all daily clips for the movement, -# speed-adjusts to match music, overlays attribution, -# sends completion notification; on last Autumn -# movement also triggers year-end-join.sh +# montage-mvt.sh — called by 4-seasons.sh on last day of each movement +# concatenates all daily clips for the movement, +# speed-adjusts to match music, overlays attribution, +# sends completion notification; on last Autumn +# movement also triggers year-end-join.sh # -# year-end-join.sh — called by montage-mvt.sh at end of Autumn Mvt 3 -# concatenates all 12 movement montages into one -# Four Seasons year video, sends notification +# year-end-join.sh — called by montage-mvt.sh at end of Autumn Mvt 3 +# concatenates all 12 movement montages into one +# Four Seasons year video, sends notification # # Helpers (called by the scripts above, not run directly): -# notify.sh — sends notifications via ntfy / email / Mattermost -# season_info.py — outputs astronomical season/movement/year for a date -# sunrise.py — outputs today's sunrise time (used by daily_sunrise_video) -# sunrise2mm.py — uploads the sunrise video to Mattermost -# sunrise_overlay.py — burns a timestamp overlay onto the sunrise video +# notify.sh — sends notifications via ntfy / email / Mattermost +# season_info.py — outputs astronomical season/movement/year for a date +# sunrise.py — outputs today's sunrise time (used by daily_sunrise_video) +# sunrise2mm.py — uploads the sunrise video to Mattermost +# 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 @@ -92,20 +93,20 @@ # and sky-cam-notify-failure@.service # # daily_sunrise_video.sh -# ← called by systemd sky-cam-sunrise.timer (09:00) -# → if renamed: update install.sh (write_service call, line ~80) +# ← called by systemd sky-cam-sunrise.timer (SCHEDULE_SUNRISE) +# → if renamed: update install.sh (write_service call ~line 70) # then re-run install.sh to regenerate the unit # calls → sunrise.py, sunrise_overlay.py, sunrise2mm.py # # 4-seasons.sh -# ← called by systemd sky-cam-4seasons.timer (01:00) -# → if renamed: update install.sh (write_service call, line ~86) +# ← called by systemd sky-cam-seasons-.timer (SCHEDULE_SEASONS_) +# → if renamed: update install.sh (write_service call ~line 85) # then re-run install.sh # calls → season_info.py, montage-mvt.sh # # fullday-video.sh -# ← called by systemd sky-cam-fullday.timer (02:00) -# → if renamed: update install.sh (write_service call, line ~93) +# ← called by systemd sky-cam-fullday-.timer (SCHEDULE_FULLDAY_) +# → if renamed: update install.sh (write_service call ~line 93) # then re-run install.sh # # montage-mvt.sh @@ -149,10 +150,6 @@ # Full path to the directory containing this file and all the 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 ─────────────────────────────────────────────────────────────────── BASE_DIR=/home/motion/drives/local-2tb MUSIC_DIR=/home/motion/drives/local-2tb/music/4 Seasons @@ -160,10 +157,38 @@ MUSIC_DIR=/home/motion/drives/local-2tb/music/4 Seasons # ── Location & timezone ─────────────────────────────────────────────────────── # 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 -LONGITUDE=-97.654321 +LATITUDE=25.0000 +LONGITUDE=-38.0000 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_ when to run 4-seasons.sh for each camera +# SCHEDULE_FULLDAY_ 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 ────────────────────────────────────────────────────── # How many minutes before/after sunrise to include in the daily clip. SUNRISE_PRE_MIN=70 @@ -181,23 +206,23 @@ access_token=your-access-token-here channel_id=your-daily-upload-channel-id-here # ── Notifications — montage / year-end complete + any job failure ───────────── -# Notifications fire when a movement montage or the year video finishes. -# They also fire automatically via systemd OnFailure= if any daily job fails. -# Enable one or more methods below. +# Notifications fire when a movement montage or the year video finishes, +# and via systemd OnFailure= if any daily job fails. +# Uncomment and configure one or more methods to enable. -# ntfy — push notifications, zero signup required for self-hosted: -# https://docs.ntfy.sh/install/ -# or use the free cloud tier at ntfy.sh (pick any topic name) -NTFY_ENABLED=false -NTFY_URL=https://ntfy.sh/your-topic-here +# ntfy — push notifications, zero signup for self-hosted or free cloud tier: +# Self-hosted: https://docs.ntfy.sh/install/ +# Cloud: pick any topic name at ntfy.sh (no account needed) +#NTFY_ENABLED=true +#NTFY_URL=https://ntfy.sh/your-topic-here -# Email — requires the 'mail' command on the system (package: mailutils or s-nail). -# For outbound SMTP configure /etc/ssmtp/ssmtp.conf or msmtp. -EMAIL_ENABLED=false -EMAIL_TO=you@example.com -EMAIL_FROM=skycam@localhost +# Email — requires the 'mail' command (package: mailutils or s-nail). +# Configure outbound SMTP via /etc/ssmtp/ssmtp.conf or msmtp. +#EMAIL_ENABLED=true +#EMAIL_TO=you@example.com +#EMAIL_FROM=skycam@localhost -# Mattermost text post — reuses the URL and token above, but posts to this -# channel (can be the same daily channel or a separate private/admin channel). -MM_NOTIFY_ENABLED=false -MM_NOTIFY_CHANNEL_ID=your-notify-channel-id-here +# Mattermost text post — reuses mattermost_url and access_token above. +# Can be the same daily channel or a separate admin/private channel. +#MM_NOTIFY_ENABLED=true +#MM_NOTIFY_CHANNEL_ID=your-notify-channel-id-here diff --git a/year-end-join.sh b/year-end-join.sh index 6817063..4fda07a 100755 --- a/year-end-join.sh +++ b/year-end-join.sh @@ -17,9 +17,11 @@ source "$SCRIPT_DIR/sky-cam.conf" ASTRO_YEAR="${1:-}" if [ -z "$ASTRO_YEAR" ]; then - echo "Usage: $0 (e.g. $0 2025)" + echo "Usage: $0 [CAM_NAME] (e.g. $0 2025 north)" exit 1 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"