# 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

```bash
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
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/<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 |
| `SCHEDULE_FULLDAY_<cam>` | When to run the full-day timelapse — also processes yesterday; schedule after SEASONS |

### 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
```

---

## 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
        │
        └─ fullday-video.sh <cam>  (runs at SCHEDULE_FULLDAY_<cam>, processes yesterday)
             Encode all of yesterday's JPEGs at FULLDAY_FPS
             Delete videos older than RETENTION_DAYS
```

### 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=<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:

```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
```

**Rebuild a movement montage** (e.g. to retry audio after a failure):
```bash
./montage-mvt.sh                        # uses today's movement
./montage-mvt.sh 2025-06-15 east        # specific date + camera
```

**Rebuild the year-end video**:
```bash
./year-end-join.sh 2025 east
```

**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
journalctl --user -u sky-cam-fullday-east.service
```
