# 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 - **Nightly moon-track timelapse** — every east night frame where the moon is visible is cropped around the moon and stitched into a short mp4 (the moon roughly held still while clouds and stars drift past) - **Monthly moon-phase close-ups** — one composite per phase (full moon, first quarter, third quarter), framed as if east took it through a 65× telephoto. The sky/halo/parallactic-angle/timing are real-from-east; the lunar surface texture is borrowed from a cached high-res reference (no software can recover detail your camera didn't capture). Posted to Mattermost. --- ## Quick start ### 1. Install dependencies ```bash sudo apt install ffmpeg bc fonts-dejavu curl python3-pip pip3 install suntime pytz requests pip3 install skyfield Pillow numpy scipy # moon jobs (moon-track + moon-phase-monthly) ``` | 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 | | `python3` + `skyfield` | Moon phases + altitude/azimuth + parallactic angle (offline after first ephemeris download) | | `python3` + `Pillow numpy scipy` | Moon detection + phase compositing | ### 2. Download ```bash bash <(curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh) cd sky-cam ``` Or with a custom install directory: ```bash curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh | bash -s -- /opt/sky-cam cd /opt/sky-cam ``` ### 3. Configure ```bash 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///.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_` | When to run the Four Seasons daily clip — processes **yesterday's** images; runs after midnight | ### 4. Set up credentials ```bash ./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: ```bash # 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 ```bash # 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 (long-running systemd service, one per camera) │ ffmpeg pulls one frame every CAPTURE_INTERVAL seconds │ Writes: BASE_DIR//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//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 (runs at SCHEDULE_SEASONS_, 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 (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.mp4` — `year-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](https://ntfy.sh) | `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=` | 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_` 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: ```bash # 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: ```bash 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): ```bash ./daily_sunrise_video.sh ``` **Re-run a daily seasons clip** for yesterday: ```bash ./4-seasons.sh east ``` **Re-run a daily seasons clip** for a specific past date: ```bash ./4-seasons.sh east 2026-04-19 ``` **Backfill multiple past dates** (useful after a camera outage or a fresh install with existing images): ```bash 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: ```bash ./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): ```bash ./montage-mvt.sh east # uses today's date ./montage-mvt.sh east 2025-06-15 # specific date ``` **Rebuild the year-end video**: ```bash ./year-end-join.sh 2025 east ``` **Moon jobs**: ```bash # Re-run last night's moon-track timelapse for east ./moon-track.sh east # Back-fill moon-track for a specific past night ./moon-track.sh east 2026-04-15 # Auto mode — runs whichever phase composite is due today (no-ops otherwise) ./moon-phase-monthly.sh # Force a single phase, picking the most recent occurrence ./moon-phase-monthly.sh --phase full ./moon-phase-monthly.sh --phase first-quarter ./moon-phase-monthly.sh --phase third-quarter # Build but skip the Mattermost post ./moon-phase-monthly.sh --phase full --no-upload # Dry run — find best frame, log it, build nothing ./moon-phase-monthly.sh --phase third-quarter --dry-run # Back-fill a specific event by exact UTC moment ./moon-phase-monthly.sh --phase full --target 2026-04-01T15:51:00Z # Inspect any moon-related stats for a frame python3 moon_detect.py BASE_DIR/east/2026-04-29/21-07-00.jpg --debug /tmp/dbg.png python3 moon_phase.py info 2026-04-29T21:07:00Z ``` **Posting schedule**: | Job | When the timer fires | When the artifact actually appears | |---|---|---| | Sunrise video | `SCHEDULE_SUNRISE` (default 03:00 local) | A few minutes after sunrise + `SUNRISE_POST_MIN` | | Moon-track timelapse | `SCHEDULE_MOON_TRACK` (default 02:30 local) | ~5 min after the timer, covers the previous night | | 🌕 Full Moon composite | `SCHEDULE_MOON_PHASE` (default 09:30 local) | 3 days after exact full moon (configurable: `MOON_FULL_POST_DELAY_DAYS`) | | 🌓 First Quarter composite | same timer | 2 days after exact first quarter (configurable: `MOON_QUARTER_POST_DELAY_DAYS`) — best-effort, see geometry note below | | 🌗 Third Quarter composite | same timer | 2 days after exact third quarter | The moon-phase timer fires every day; the script no-ops on days that aren't a post-day for any phase, so you'll see exactly three posts per lunar cycle in Mattermost (sometimes only two if first quarter detection fails — see geometry note in `sky-cam.conf`). **Check capture status**: ```bash 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: ```bash journalctl --user -u sky-cam-watchdog-east.service -f ``` **Check logs**: ```bash journalctl --user -u sky-cam-sunrise.service journalctl --user -u sky-cam-seasons-east.service ``` --- ## Keeping sky-cam up to date ### Install git ```bash sudo apt install git ``` ### First-time clone (if you don't have the repo yet) ```bash 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: ```bash git pull origin main ``` Then re-run install if any schedules or scripts changed: ```bash ./install.sh ``` ### Check what changed since your last pull ```bash 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):** ```bash 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 ` and `git stash drop`. **Option B — discard your local edits completely:** ```bash 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 ```bash 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 ```bash git log --oneline # compact list of commits git log --oneline -20 # last 20 only git show # full diff for one commit ``` ### Switch to a specific branch (e.g. a development branch) ```bash git fetch origin git checkout claude/seasonal-sunrise-montage-nn268 ``` To switch back to main: ```bash git checkout main git pull origin main ``` ### Undo the last commit (before pushing) ```bash 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.