115 lines
4.7 KiB
Markdown
115 lines
4.7 KiB
Markdown
# Arcana FX
|
|
|
|
A dark-fantasy soundboard for D&D nights. The frontend shows a tile grid of
|
|
audio effects; pressing a tile asks the backend to play that file through a
|
|
configured Home Assistant `media_player`. Future panels for light control are
|
|
present but disabled.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌────────────┐ /api/play ┌──────────────────────┐ POST media_player/play_media
|
|
│ Frontend │ ─────────────▶│ Arcana FX backend │ ───────────────────────────────▶ Home Assistant
|
|
│ (static) │ │ (Node 20 + Express) │ │
|
|
└────────────┘ │ │◀──── fetches /media ─────────────────┘
|
|
│ Filestash client │ ◀── share proof cookie + cat ── Filestash
|
|
└──────────────────────┘
|
|
```
|
|
|
|
- **Backend** (`src/`) is the only component that holds the Home Assistant
|
|
token and the Filestash proof cookie. It exposes a deliberately narrow API.
|
|
- **Frontend** (`public/`) is plain static HTML/CSS/JS — no build step.
|
|
- Home Assistant pulls audio from `PUBLIC_BASE_URL/media?path=...`, which the
|
|
backend streams from Filestash on demand. Nothing is preloaded.
|
|
|
|
## API surface
|
|
|
|
| Method | Path | Description |
|
|
|-------:|------------------|------------------------------------------------------------|
|
|
| GET | `/health` | Liveness probe + cache stats |
|
|
| GET | `/api/config` | Public config (media player entity, light-control stubs) |
|
|
| GET | `/api/effects` | Recursive list of audio effects (`?refresh=1` to force) |
|
|
| POST | `/api/play` | Body `{ "path": "/Combat/Sword.mp3" }` |
|
|
| POST | `/api/stop` | Stops the configured media player |
|
|
| GET | `/media?path=…` | Intranet proxy streaming the file from Filestash |
|
|
|
|
`path` must be a share-relative path that was returned by `/api/effects`.
|
|
Arbitrary Filestash paths, HA entities, or HA services cannot be reached
|
|
through this API.
|
|
|
|
## Configuration
|
|
|
|
Two layers:
|
|
|
|
- `config/config.json` — non-secret, version-controlled. Filestash share,
|
|
allowed extensions, allowed media player entity, cache TTL, light-control
|
|
placeholders.
|
|
- `.env` — secrets and per-deployment values. Copy from `.env.example`:
|
|
|
|
```
|
|
HA_URL=http://192.168.33.33:8123
|
|
HA_TOKEN=... # long-lived access token, never commit
|
|
PUBLIC_BASE_URL=http://<lan-ip>:8091 # URL HA uses to fetch /media
|
|
PORT=8091
|
|
ALLOWED_ORIGINS= # optional CORS allow-list
|
|
```
|
|
|
|
`PUBLIC_BASE_URL` must be reachable **from Home Assistant**, not just from
|
|
your browser. On a typical home LAN it is `http://<docker-host-lan-ip>:8091`.
|
|
|
|
The allowed media player entity is `media_player.audio_test_media_player`,
|
|
hardcoded in `config/config.json`. Requests for any other entity are rejected
|
|
with `403`.
|
|
|
|
## Run with Docker Compose
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
# edit .env: set HA_TOKEN and PUBLIC_BASE_URL
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Then open `http://<host>:8091`.
|
|
|
|
## Run locally (no Docker)
|
|
|
|
Requires Node.js 20+.
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
# edit .env
|
|
npm install
|
|
npm run check # syntax check on backend sources
|
|
npm start
|
|
```
|
|
|
|
## Filestash flow
|
|
|
|
The backend obtains a proof cookie on first request:
|
|
|
|
1. `POST /api/share/<id>/proof` with `X-Requested-With: XmlHttpRequest`.
|
|
Response sets the proof cookie and tells us the share's base path (e.g.
|
|
`/DnD/Audio/`).
|
|
2. `GET /api/files/ls?path=<absolute>&share=<id>` to walk directories.
|
|
3. `GET /api/files/cat?path=<absolute>&share=<id>` to stream a file.
|
|
|
|
Recursive listing is cached in memory (`cacheTtlSeconds` in
|
|
`config/config.json`, default 10 minutes). `/api/effects?refresh=1` forces
|
|
a rescan.
|
|
|
|
## Security notes
|
|
|
|
- The HA token lives only in the backend process environment. It is never
|
|
sent to the frontend.
|
|
- Only the configured `media_player` entity is allowed. The HA client refuses
|
|
any other entity id at the client layer.
|
|
- Effect paths must match a known effect from the cached share scan, must
|
|
start with `/`, and must not contain `..` or NUL bytes.
|
|
- `.env` is git-ignored; only `.env.example` is committed.
|
|
|
|
## Future work
|
|
|
|
The "Light Control" panel renders disabled placeholders (`config/config.json
|
|
> lightControl.placeholders`). Wiring real switches/lights will be a new
|
|
narrow set of endpoints, on the same principle as the audio side.
|