Claude 7c5a703222 Fix sky-cam-audio-capture.service spurious failure on no-op retention
The retention block ended with `[ test ] && echo`, which under `set -e`
returns exit 1 when `deleted` is 0 — the normal case on most days. As
the script's last command, that bash exit propagated to systemd, which
marked the unit failed and fired the OnFailure ntfy even though the
audio file was written successfully.

Add `|| true` and an explicit `exit 0` so a no-op retention pass is
treated as success.
2026-04-29 18:33:03 +00:00
2026-04-22 22:24:28 -04:00
2026-04-23 09:29:00 -04:00

sky-cam

Automated sky / timelapse camera scripts that produce:

  • Daily sunrise clip — a speed-adjusted video of the sunrise window, uploaded to Mattermost each morning
  • Four Seasons timelapse — daily clips sized to each Vivaldi movement's music duration, assembled automatically into per-movement montages (with music + attribution overlay) and a full-year video
  • Full-day timelapse — a fixed-fps timelapse of every image captured that day, kept for a configurable retention window

Quick start

1. Install dependencies

sudo apt install ffmpeg bc fonts-dejavu curl python3-pip
pip3 install suntime pytz requests
Package Purpose
ffmpeg + ffprobe Encoding, RTSP capture, audio recording, duration probing
bc Floating-point arithmetic for speed factors
fonts-dejavu Text overlays (sunrise time, attribution)
python3 + suntime pytz Astronomical sunrise calculation — pure math, no internet, works indefinitely
python3 + requests Mattermost upload

2. Download

bash <(curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh)
cd sky-cam

Or with a custom install directory:

curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh | bash -s -- /opt/sky-cam
cd /opt/sky-cam

3. Configure

nano sky-cam.conf

SCRIPT_DIR and BASE_DIR are auto-detected — you only need to fill in settings specific to your setup:

Setting What it is
BASE_DIR Root where camera images live — override if storing on a separate drive (BASE_DIR/<cam>/<date>/<HH-MM-SS>.jpg)
MOVIES_DIR Where finished videos are written (default: BASE_DIR/movies)
MUSIC_DIR Directory containing the 12 Vivaldi Four Seasons MP3 files
CAMERAS Space-separated list of camera names, e.g. (east north south west)
SUNRISE_CAM Which camera faces east and gets the sunrise job
LATITUDE / LONGITUDE / TIMEZONE Your location for sunrise calculation
CAPTURE_INTERVAL Seconds between captured frames (default: 10)
SCHEDULE_SUNRISE When to start the sunrise job — default 03:00, script waits internally until the capture window closes
SCHEDULE_SEASONS_<cam> When to run the Four Seasons daily clip — processes yesterday's images; runs after midnight

4. Set up credentials

./install.sh           # generates .env.example alongside systemd units
cp .env.example .env
$EDITOR .env

.env holds all sensitive values and is never committed to git:

# RTSP stream URL — one per camera (variable name matches camera name)
CAM_RTSP_east=rtsp://admin:password@192.168.1.100:554/stream1

# Mattermost upload
mattermost_url=https://your-mattermost.example.com
access_token=your-token
channel_id=your-channel-id

# Notifications (uncomment what you use)
#NTFY_URL=https://ntfy.sh/your-topic
#EMAIL_TO=you@example.com

Re-run ./install.sh any time sky-cam.conf changes (schedules, cameras, etc.).

5. Verify

# Confirm cameras are capturing
systemctl --user status sky-cam-capture-east.service
ls BASE_DIR/east/$(date +%Y-%m-%d)/   # images should appear within CAPTURE_INTERVAL seconds

# Check all timers are scheduled
systemctl --user list-timers 'sky-cam-*'

# Test sunrise calculation
python3 sunrise.py

# export XDG_RUNTIME_DIR so system schedules will show

export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user list-timers 'sky-cam-sunrise*'

# Add that export to your ~/.bashrc so it's always set in your shell:

echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc
source ~/.bashrc


How it works

Camera RTSP stream
        │
        ├─ capture.sh <cam>  (long-running systemd service, one per camera)
        │    ffmpeg pulls one frame every CAPTURE_INTERVAL seconds
        │    Writes: BASE_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.jpg
        │    Restarts at midnight for the new date directory; auto-reconnects after 30s on loss
        │
        ├─ sunrise-audio-capture.sh  (starts at 03:00, waits internally for sunrise)
        │    Calculates today's sunrise, then records exactly SUNRISE_TARGET_SECS of audio
        │    centred on the moment of sunrise (odd second goes to post-sunrise)
        │    Writes: BASE_DIR/<cam>/YYYY-MM-DD/sunrise-audio.m4a
        │    Deleted automatically after being mixed into the sunrise video
        │
Camera JPEGs + audio
        │
        ├─ daily_sunrise_video.sh  (starts at SCHEDULE_SUNRISE, waits for capture window)
        │    Wakes at SCHEDULE_SUNRISE, sleeps until (sunrise + SUNRISE_POST_MIN)
        │    Step 1: encode raw video from sunrise-window JPEGs
        │    Step 2: speed-adjust to SUNRISE_TARGET_SECS  → saved permanently
        │    Step 3a: mix audio (camera mic → library fallback → no audio)
        │    Step 3b: burn sunrise time overlay (optional, set SUNRISE_OVERLAY_ENABLED=false to skip)
        │    Each step degrades independently — upload always fires
        │    OnSuccess → sunrise2mm.py uploads to Mattermost
        │
        ├─ 4-seasons.sh <cam>  (runs at SCHEDULE_SEASONS_<cam>, processes yesterday)
        │    Step 1: encode all of yesterday's JPEGs into raw video
        │    Step 2: speed-adjust to music_duration / days_in_movement
        │    Last day of movement → triggers montage-mvt.sh
        │         Step 1: concatenate all daily clips
        │         Step 2: speed-adjust to exactly match music  → saved permanently
        │         Step 3: mix music + fades + attribution overlay → Montage.mp4
        │         Last movement of Autumn → triggers year-end-join.sh
        │         On completion → notify with verify-mvt.sh command
        └─ verify-mvt.sh <year> <season> <mvt> <cam>  (run manually after notification)
             Review montage, approve to delete source JPEG folders

Resilience

Every pipeline saves an intermediate file before the riskiest step, so a partial failure always leaves something uploadable:

Sunrise video — four-tier fallback, upload always fires:

Audio Overlay What gets uploaded
Final video with audio + timestamp
Audio-mixed video, no timestamp — overlay failure notified
Video with timestamp, no audio — audio failure notified
Speed-only video — both failures notified

Montage: if music + overlay (step 3) fails, the speed-adjusted silent video is promoted to *-Montage.mp4year-end-join.sh still includes the movement and you get a warning notification.


Notifications

notify.sh sends alerts through any combination of:

Channel Config key(s)
ntfy NTFY_ENABLED=true, NTFY_URL=https://ntfy.sh/your-topic
Email EMAIL_ENABLED=true, EMAIL_TO=you@example.com
Mattermost text post MM_NOTIFY_ENABLED=true, MM_NOTIFY_CHANNEL_ID=<channel-id>

Notifications fire for:

  • Sunrise video ready (with audio/overlay status), upload success/failure
  • Each daily seasons clip saved
  • Montage complete (or degraded if audio/overlay failed)
  • Year-end Four Seasons video complete
  • Any step failure, with the surviving file path named

Camera audio (optional)

If your camera has a microphone, sky-cam records exactly SUNRISE_TARGET_SECS of natural-speed audio centred on the actual sunrise moment and mixes it into the timelapse video.

  1. Set AUDIO_ENABLED=true in sky-cam.conf
  2. Set CAM_RTSP_<cam> in .env for the sunrise camera
  3. Re-run ./install.sh to generate the sky-cam-audio-capture.timer

The recording is split evenly around sunrise (e.g. 5 s before + 5 s after for a 10 s clip; odd second goes to post-sunrise). It is deleted automatically after mixing.

Audio fallback library (optional)

If the camera has no mic, or audio capture fails, daily_sunrise_video.sh picks a random ambient sound from sunrise-sounds/ instead. The library is organised into 11 weather/season folders (clear-spring, rain, thunder, windy, etc.) — populate it once with:

# Get a free API key at https://freesound.org/apiv2/apply/
python3 download-sunrise-sounds.py --api-key YOUR_KEY

Default: ~275 CC-licensed 128 kbps MP3 previews (~25 per category). Re-run any time to top up:

python3 download-sunrise-sounds.py --api-key YOUR_KEY --per-category 40

Attribution data for every file is written to sunrise-sounds/manifest.json.

Audio priority order:

  1. Camera mic recording (sunrise-audio.m4a) — real ambient sound at actual sunrise
  2. Random file from sunrise-sounds/ library
  3. No audio — overlay-only video (always produced regardless)

Manual operations

Re-run today's sunrise (e.g. after a config fix):

./daily_sunrise_video.sh

Re-run a daily seasons clip for yesterday:

./4-seasons.sh east

Re-run a daily seasons clip for a specific past date:

./4-seasons.sh east 2026-04-19

Backfill multiple past dates (useful after a camera outage or a fresh install with existing images):

for d in 2026-04-15 2026-04-16 2026-04-17 2026-04-18 2026-04-19; do
    ./4-seasons.sh east "$d"
done

Replace the date list with whatever range you need. Each run produces one *-final.mp4 in the movement's output directory. Once all days for a movement are present you can build the montage manually:

./montage-mvt.sh east 2026-04-19   # date of the last day of that movement

Rebuild a movement montage (e.g. to retry audio after a failure):

./montage-mvt.sh east                   # uses today's date
./montage-mvt.sh east 2025-06-15        # specific date

Rebuild the year-end video:

./year-end-join.sh 2025 east

Check capture status:

systemctl --user status sky-cam-capture-east.service
journalctl --user -u sky-cam-capture-east.service -f

The capture watchdog runs alongside each camera and sends a notification (via notify.sh) if no new frame arrives within CAPTURE_STALE_SECS seconds, and a second notification when capture resumes. Check watchdog logs with:

journalctl --user -u sky-cam-watchdog-east.service -f

Check logs:

journalctl --user -u sky-cam-sunrise.service
journalctl --user -u sky-cam-seasons-east.service

Keeping sky-cam up to date

Install git

sudo apt install git

First-time clone (if you don't have the repo yet)

git clone https://github.com/outis1one/sky-cam.git
cd sky-cam

Pull the latest changes from main

After merging a pull request on GitHub, or whenever you want to update your running copy:

git pull origin main

Then re-run install if any schedules or scripts changed:

./install.sh

Check what changed since your last pull

git log --oneline origin/main ^HEAD   # commits on remote not yet on your machine
git diff HEAD origin/main             # full diff of incoming changes
git fetch origin && git status        # fetch first, then show your local state

You have local edits and want to pull anyway

Option A — save your changes first (recommended):

git stash                # temporarily shelve your local edits
git pull origin main     # pull updates
git stash pop            # re-apply your edits on top

If stash pop reports a conflict, open the file and look for the <<<<<< markers — edit to resolve, then git add <file> and git stash drop.

Option B — discard your local edits completely:

git fetch origin
git reset --hard origin/main    # WARNING: your local edits are gone permanently

Use this only when you are sure you do not need your local changes.

Check what you have changed locally

git status              # which files are modified / untracked
git diff                # show the actual changes (unstaged)
git diff --staged       # show changes already staged with git add

Look at the history

git log --oneline               # compact list of commits
git log --oneline -20           # last 20 only
git show <commit-hash>          # full diff for one commit

Switch to a specific branch (e.g. a development branch)

git fetch origin
git checkout claude/seasonal-sunrise-montage-nn268

To switch back to main:

git checkout main
git pull origin main

Undo the last commit (before pushing)

git reset HEAD~1        # undo commit, keep the file changes

Credentials and .env are never in git

.env is listed in .gitignore and will never be overwritten by a pull. sky-cam.conf is in git — if you edited it locally, a pull may conflict. Keep your machine-specific values in .env and leave sky-cam.conf for settings you want to track.

S
Description
Mirror of outis1one/sky-cam from GitHub
Readme
60 MiB
Languages
Shell 55.2%
Python 44.8%