Files
sky-cam/install.sh
T
Claude 74a74570fc Move location/music/ntfy config to .env; add paths to notifications
sky-cam.conf:
- MUSIC_DIR now uses override pattern (MUSIC_DIR="${MUSIC_DIR:-$SCRIPT_DIR/music}")
  so the real path can be set in .env without hardcoding it
- LATITUDE/LONGITUDE use override pattern; set real coords in .env to keep
  location private and out of git
- NTFY_URL now uses override pattern with empty default; set in .env
- NTFY_ENABLED moved out of comments into active config with false default

sunrise.py:
- Reads .env after sky-cam.conf so .env values override (same source order
  as sky-cam.conf itself)
- Handles ${VAR:-default} bash syntax when parsing sky-cam.conf values

All completion notifications now include the full output file path so you
can tell at a glance where the file landed:
- daily_sunrise_video.sh: "filename — Xs, sunrise at HH:MM | /full/path"
- 4-seasons.sh: "filename — Xs | /full/path"
- montage-mvt.sh: "filename | checks | /full/path | verify: ..."
- year-end-join.sh: "filename — Xmin | /full/path" (also adds [cam] to title)

install.sh .env.example now includes LATITUDE, LONGITUDE, MUSIC_DIR, NTFY_URL

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
2026-04-22 13:30:02 +00:00

327 lines
12 KiB
Bash
Executable File

#!/bin/bash
# install.sh — generate and install sky-cam systemd units from sky-cam.conf.
#
# Usage:
# ./install.sh # installs to ~/.config/systemd/user (no root needed)
# ./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, cameras added, schedules changed).
# No other file needs editing.
set -euo pipefail
HERE="$(dirname "$(realpath "$0")")"
source "$HERE/sky-cam.conf"
# ── Install target ────────────────────────────────────────────────────────────
SYSTEM_MODE=false
[ "${1:-}" = "--system" ] && SYSTEM_MODE=true
if $SYSTEM_MODE; then
UNIT_DIR="/etc/systemd/system"
SC="sudo systemctl"
else
UNIT_DIR="$HOME/.config/systemd/user"
SC="systemctl --user"
fi
mkdir -p "$UNIT_DIR"
echo "Installing systemd units to $UNIT_DIR ..."
# ── Ensure all scripts are executable ────────────────────────────────────────
chmod +x "$HERE"/*.sh "$HERE"/*.py 2>/dev/null || true
# ── Pre-create data directories for all cameras ───────────────────────────────
echo "Creating data directories..."
for cam in "${CAMERAS[@]}"; do
mkdir -p "$BASE_DIR/$cam"
mkdir -p "$MOVIES_DIR/$cam"
echo " $BASE_DIR/$cam"
echo " $MOVIES_DIR/$cam"
done
mkdir -p "$BASE_DIR/$SUNRISE_CAM"
echo ""
# ── Helper: write a oneshot service unit ─────────────────────────────────────
write_service() {
local unit="$1" description="$2" script="$3" extra="${4:-}"
cat > "$UNIT_DIR/${unit}.service" <<EOF
[Unit]
Description=Sky-Cam — $description
${extra}OnFailure=sky-cam-notify-failure@%n.service
[Service]
Type=oneshot
ExecStart=$SCRIPT_DIR/$script
StandardOutput=journal
StandardError=journal
EOF
echo " wrote ${unit}.service"
}
# ── Helper: write a timer unit ───────────────────────────────────────────────
write_timer() {
local unit="$1" description="$2" calendar="$3"
cat > "$UNIT_DIR/${unit}.timer" <<EOF
[Unit]
Description=Sky-Cam — $description
[Timer]
OnCalendar=*-*-* $calendar
Persistent=true
[Install]
WantedBy=timers.target
EOF
echo " wrote ${unit}.timer"
}
# ── Failure notification template ────────────────────────────────────────────
cat > "$UNIT_DIR/sky-cam-notify-failure@.service" <<EOF
[Unit]
Description=Sky-Cam notify on failure for %i
[Service]
Type=oneshot
ExecStart=$SCRIPT_DIR/notify.sh "sky-cam job failed: %i" "systemd unit %i failed — check: journalctl -u %i"
EOF
echo " wrote sky-cam-notify-failure@.service"
# ── Sunrise video — SUNRISE_CAM only ─────────────────────────────────────────
# OnSuccess= triggers the upload service only when video creation succeeds.
cat > "$UNIT_DIR/sky-cam-sunrise.service" <<EOF
[Unit]
Description=Sky-Cam — $SUNRISE_CAM: daily sunrise video
After=network-online.target
Wants=network-online.target
OnFailure=sky-cam-notify-failure@%n.service
OnSuccess=sky-cam-sunrise-upload.service
[Service]
Type=oneshot
ExecStart=$SCRIPT_DIR/daily_sunrise_video.sh $SUNRISE_CAM
StandardOutput=journal
StandardError=journal
EOF
echo " wrote sky-cam-sunrise.service"
cat > "$UNIT_DIR/sky-cam-sunrise-upload.service" <<EOF
[Unit]
Description=Sky-Cam — $SUNRISE_CAM: upload daily sunrise to Mattermost
After=network-online.target
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 continuous capture services ────────────────────────────────────
# capture.sh replaces MotionEye / any NVR for periodic JPEG capture.
# Generated when CAM_RTSP_<cam> is set; Type=simple (long-running, not oneshot).
capture_services=()
for cam in "${CAMERAS[@]}"; do
rtsp_var="CAM_RTSP_${cam}"
if [ -n "${!rtsp_var:-}" ]; then
cat > "$UNIT_DIR/sky-cam-capture-${cam}.service" <<EOF
[Unit]
Description=Sky-Cam — ${cam}: continuous RTSP frame capture
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=$SCRIPT_DIR/capture.sh ${cam}
Restart=on-failure
RestartSec=30
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
echo " wrote sky-cam-capture-${cam}.service"
capture_services+=("sky-cam-capture-${cam}")
cat > "$UNIT_DIR/sky-cam-watchdog-${cam}.service" <<EOF
[Unit]
Description=Sky-Cam — ${cam}: capture watchdog (stall detection)
After=sky-cam-capture-${cam}.service
Wants=sky-cam-capture-${cam}.service
[Service]
Type=simple
ExecStart=$SCRIPT_DIR/capture-watchdog.sh ${cam}
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
echo " wrote sky-cam-watchdog-${cam}.service"
capture_services+=("sky-cam-watchdog-${cam}")
else
echo " WARNING: CAM_RTSP_${cam} not set — skipping capture service for ${cam}"
echo " (set CAM_RTSP_${cam}=rtsp://... in sky-cam.conf to enable)"
fi
done
# ── Sunrise audio capture ─────────────────────────────────────────────────────
# Uses the same schedule as the sunrise video — both start early and wait
# internally for the right moment, so they don't interfere.
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
write_service "sky-cam-audio-capture" \
"$SUNRISE_CAM: sunrise audio capture" \
"sunrise-audio-capture.sh"
write_timer "sky-cam-audio-capture" \
"$SUNRISE_CAM: sunrise audio capture" \
"$SCHEDULE_SUNRISE"
timers+=("sky-cam-audio-capture")
fi
# ── Ambient audio recording services ─────────────────────────────────────────
# One long-running service per camera in AMBIENT_CAMS.
# Enabled alongside the capture services so install.sh restarts them on update.
if [ "${AMBIENT_ENABLED:-false}" = "true" ]; then
for cam in "${AMBIENT_CAMS[@]}"; do
rtsp_var="CAM_RTSP_${cam}"
if [ -n "${!rtsp_var:-}" ]; then
cat > "$UNIT_DIR/sky-cam-ambient-${cam}.service" <<EOF
[Unit]
Description=Sky-Cam — ${cam}: ambient audio recorder
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=$SCRIPT_DIR/ambient-record.sh ${cam}
Restart=on-failure
RestartSec=30
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
echo " wrote sky-cam-ambient-${cam}.service"
capture_services+=("sky-cam-ambient-${cam}")
else
echo " WARNING: CAM_RTSP_${cam} not set — skipping ambient service for ${cam}"
fi
done
fi
# ── Per-camera Four Seasons jobs ─────────────────────────────────────────────
# Reads CAMERAS and SCHEDULE_SEASONS_<cam> from sky-cam.conf.
# Script receives the camera name as $1 so it knows which camera to process.
for cam in "${CAMERAS[@]}"; do
sched_seasons_var="SCHEDULE_SEASONS_${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
done
# ── Generate .env.example ────────────────────────────────────────────────────
env_example="$HERE/.env.example"
{
echo "# sky-cam credentials — copy to .env and fill in real values."
echo "# .env is gitignored and never checked in."
echo ""
echo "# RTSP stream URLs — one line per camera (variable name = CAM_RTSP_<camname>)"
echo "#"
echo "# Use single quotes around the whole value to prevent shell interpretation."
echo "# Special characters in the PASSWORD must be URL-encoded (number-row reference):"
echo "#"
echo "# ! %21 @ %40 # %23 \$ %24 % %25 ^ %5E"
echo "# & %26 * %2A ( %28 ) %29 - safe _ safe"
echo "# + %2B = %3D ~ %7E \` %60 space %20"
echo "#"
echo "# Also critical (break URL parsing if unencoded): : %3A / %2F"
echo "#"
echo "# Example: password my@p\$ss:word! -> my%40p%24ss%3Aword%21"
echo ""
for cam in "${CAMERAS[@]}"; do
echo "CAM_RTSP_${cam}='rtsp://admin:PASSWORD@192.168.1.XXX:554/stream1'"
done
echo ""
echo "# Location — used by sunrise.py to calculate sunrise time each day."
echo "# Right-click your location in Google Maps to get coordinates."
echo "LATITUDE=0.0000"
echo "LONGITUDE=0.0000"
echo ""
echo "# Music directory — path to your Four Seasons music files."
echo "# Files must be named so that the season and Mvt number appear in the filename,"
echo "# e.g. 'Vivaldi_Spring_Mvt1.mp3'. Defaults to a 'music' subfolder next to the scripts."
echo "# MUSIC_DIR=/path/to/your/music/4 Seasons"
echo ""
echo "# Mattermost upload"
echo "mattermost_url=https://your-mattermost-server.example.com"
echo "access_token=your-access-token-here"
echo "channel_id=your-daily-upload-channel-id-here"
echo ""
echo "# Notifications"
echo "# ntfy — set NTFY_ENABLED=true in sky-cam.conf, then set your topic URL here:"
echo "#NTFY_URL=https://ntfy.sh/your-topic"
echo "#EMAIL_TO=you@example.com"
echo "#MM_NOTIFY_CHANNEL_ID=your-notify-channel-id-here"
} > "$env_example"
echo " wrote .env.example (cp .env.example .env then fill in real values)"
# ── User-session prerequisites (non-system installs only) ────────────────────
if ! $SYSTEM_MODE; then
# Make systemctl --user reachable when run outside a login session
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
# Start user services at boot even without an interactive login
loginctl enable-linger "$USER" \
&& echo " loginctl enable-linger: ok" \
|| echo " WARNING: loginctl enable-linger failed (may need to run manually)"
fi
# ── Reload and enable ─────────────────────────────────────────────────────────
echo ""
$SC daemon-reload
for svc in "${capture_services[@]}"; do
if $SC enable "${svc}.service" && $SC restart "${svc}.service"; then
echo " enabled + started ${svc}.service"
else
echo " WARNING: could not enable/start ${svc}.service"
fi
done
for timer in "${timers[@]}"; do
$SC enable --now "${timer}.timer" \
&& echo " enabled + started ${timer}.timer" \
|| echo " WARNING: could not enable ${timer}.timer"
done
echo ""
echo "Done. Check status with:"
if $SYSTEM_MODE; then
echo " sudo systemctl list-timers 'sky-cam-*'"
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-seasons-${CAMERAS[0]}.service -f"
fi