Commit 11aa227
internal/manifest/types.go
@@ -11,7 +11,6 @@ const (
StatusInProgress ClipStatus = "in_progress"
StatusComplete ClipStatus = "complete"
StatusFailed ClipStatus = "failed"
- StatusSkipped ClipStatus = "skipped"
)
// ClipEntry represents a single video clip from an event.
internal/protect/client.go
@@ -22,7 +22,6 @@ var (
ErrServerError = errors.New("server error")
ErrCameraNotFound = errors.New("camera not found")
ErrMultipleCameras = errors.New("multiple cameras match name")
- ErrNotLoggedIn = errors.New("not logged in: call Login() first")
)
// Client is a UniFi Protect API client.
@@ -30,7 +29,6 @@ type Client struct {
baseURL string
apiPath string // "/proxy/protect/api" or "/api"
csrfToken string
- loggedIn bool
httpClient *http.Client
}
@@ -136,7 +134,6 @@ func (c *Client) Login(ctx context.Context, username, password string) error {
slog.Debug("obtained CSRF token")
}
- c.loggedIn = true
slog.Debug("login successful")
return nil
}
README.md
@@ -8,9 +8,10 @@ UniFi Protect Video Summary - Create ~10-minute timelapses from a day's recordin
# Build
go build ./cmd/upvs
-# Set credentials (or use --api-key flag)
+# Set credentials (or use flags)
export UPVS_HOST=https://192.168.1.1
-export UPVS_API_KEY=your-api-key-here
+export UPVS_USERNAME=your-username
+export UPVS_PASSWORD=your-password
# Run full pipeline for a single day
./upvs run --camera "Front Door" --date 2024-01-15 --out ./output
@@ -21,14 +22,18 @@ export UPVS_API_KEY=your-api-key-here
./upvs render --camera "Front Door" --date 2024-01-15 --out ./output
```
-## API Key
+## Authentication
-Generate an API key in UniFi OS: **Settings > Control Plane > Integrations**
+The tool authenticates using a local UniFi OS account (username/password).
+Create a dedicated account in UniFi OS for API access.
-Pass via:
-- `--api-key` flag
-- `--api-key-file` flag (path to file containing key)
-- `UPVS_API_KEY` environment variable
+Pass credentials via:
+- `--username` and `--password` flags
+- `--password-file` flag (path to file containing password)
+- `UPVS_USERNAME` and `UPVS_PASSWORD` environment variables
+
+**Note:** Prefer `--password-file` or environment variables over `--password` flag
+to avoid exposing credentials in process lists.
## Output
@@ -40,8 +45,23 @@ output/
โโโ metadata/day.json # Speed calculation
```
+## Options
+
+```
+Global Flags:
+ --host UniFi Protect URL (env: UPVS_HOST)
+ --username Username (env: UPVS_USERNAME)
+ --password Password (env: UPVS_PASSWORD)
+ --password-file Path to file containing password
+ --camera Camera ID or name
+ --out Output directory
+ --tls-insecure Skip TLS verification (for self-signed certs)
+ --direct-api Use /api path (direct NVR connection, not UniFi OS)
+ --verbose Enable debug logging
+```
+
## Requirements
- Go 1.23+
- FFmpeg (in PATH)
-- UniFi Protect with API key access
+- UniFi Protect with local account access
SPEC.md
@@ -14,26 +14,34 @@ Non-goals: multi-camera, multi-day batching, motion-only exports, UI automation,
## 2. Authentication
-### API Key Authentication (Recommended)
+### Session-Based Authentication
-UniFi Protect supports API key authentication for programmatic access:
+UniFi Protect uses cookie-based session authentication:
-- Keys are generated in UniFi OS: **Settings > Control Plane > Integrations**
-- Pass the key via `X-API-Key` HTTP header on all requests
-- No session management, cookies, or login flow required
-- Stateless - each request is independently authenticated
+1. **Login:** `POST /api/auth/login` with JSON body:
+ ```json
+ {"username": "...", "password": "...", "rememberMe": false, "token": ""}
+ ```
+
+2. **Session Cookie:** The response sets a session cookie used for subsequent requests
+
+3. **CSRF Token:** The login response includes an `X-Csrf-Token` header that must be
+ sent with subsequent requests
```
-GET /proxy/protect/api/bootstrap
-X-API-Key: <your-api-key>
+POST /api/auth/login
+Content-Type: application/json
+
+{"username": "protect-api", "password": "secret", "rememberMe": false, "token": ""}
```
### Implementation Notes
-- API keys provide full access to the Protect API
-- Keys do not expire but can be revoked in the UI
+- Create a dedicated local account in UniFi OS for API access
+- Use a cookie jar to persist session cookies across requests
+- Store and send the CSRF token from the login response
- TLS verification should be enabled; use `--tls-insecure` only for self-signed certs
-- Never log or persist the API key
+- Never log or persist credentials
### Documentation References
@@ -49,7 +57,8 @@ X-API-Key: <your-api-key>
| Input | Description |
|-------|-------------|
| `host` | UniFi Protect URL, e.g. `https://192.168.1.1` |
-| `api_key` | API key from UniFi OS |
+| `username` | Local UniFi OS account username |
+| `password` | Account password (or use `password_file`) |
| `camera` | Camera ID or name (must resolve uniquely) |
| `date` | Target day: `YYYY-MM-DD` |
| `out_dir` | Output root directory |
@@ -63,6 +72,7 @@ X-API-Key: <your-api-key>
| `crf` | `23` | FFmpeg quality (0-51, lower=better) |
| `preset` | `medium` | FFmpeg encoding preset |
| `tls_insecure` | `false` | Skip TLS verification |
+| `direct_api` | `false` | Use `/api` path (direct NVR, not UniFi OS) |
| `max_workers` | `4` | Download concurrency |
| `retry_limit` | `3` | Retries per download |
@@ -180,7 +190,8 @@ Returns system information including all cameras.
**Pagination:**
- Request with `limit=100` and `orderDirection=ASC`
-- If response contains `limit` events, fetch next page using `after=<last-event-id>`
+- If response contains `limit` events, fetch next page using the last event's
+ `end` timestamp + 1ms as the new `start` parameter
- Continue until fewer than `limit` events returned
**Day Window:**
@@ -354,7 +365,7 @@ The pipeline is designed to be restartable:
| Status | Action |
|--------|--------|
-| 401/403 | Fail immediately - invalid API key |
+| 401/403 | Fail immediately - invalid credentials |
| 404 | Mark clip as failed, continue with others |
| 429 | Retry with backoff |
| 5xx | Retry with backoff |
@@ -362,7 +373,7 @@ The pipeline is designed to be restartable:
### Sentinel Errors
Define inspectable errors for programmatic handling:
-- `ErrUnauthorized`: Invalid API key
+- `ErrUnauthorized`: Invalid credentials
- `ErrCameraNotFound`: Camera doesn't exist
- `ErrMultipleCameras`: Ambiguous camera name
- `ErrMaxAttemptsExceeded`: Retries exhausted
@@ -382,13 +393,15 @@ If no events found:
upvs [global flags] <command>
Global Flags:
- --host UniFi Protect URL (env: UPVS_HOST)
- --api-key API key (env: UPVS_API_KEY)
- --api-key-file Path to file containing API key
- --camera Camera ID or name
- --out Output directory
- --tls-insecure Skip TLS verification
- --verbose Enable debug logging
+ --host UniFi Protect URL (env: UPVS_HOST)
+ --username Username (env: UPVS_USERNAME)
+ --password Password (env: UPVS_PASSWORD)
+ --password-file Path to file containing password
+ --camera Camera ID or name
+ --out Output directory
+ --tls-insecure Skip TLS verification
+ --direct-api Use /api path (direct NVR, not UniFi OS)
+ --verbose Enable debug logging
Commands:
scan Enumerate events for a day
@@ -436,7 +449,8 @@ Render Flags:
## 11. Security
-- API key must not be logged or written to disk
+- Credentials must not be logged or written to disk
+- Prefer `--password-file` or env vars over `--password` flag (avoids process list exposure)
- Use `log/slog` structured logging (no credential fields)
- TLS verification enabled by default
- `--tls-insecure` must be explicitly set and logged as warning