discover · compare · collections · requirements · contribute · feed · watching · stats · help with your tokens
vladimirperovic/owntunedashboard · source
Snapshot 0 · 2026-11-04T12:47:28Z · JavaScript · MIT
Content fingerprint
84e5842ed3ee043c903bdcd0aa27a314924c600b8933f1567b3c1b7928d9a6a0GitHub description at capture
Polished, AirPlay-first web dashboard for OwnTone — live radio, scheduler, multi-room, queue and library browsing. No build step.
README at capture
# OwnTone Dashboard
A polished, AirPlay-first web dashboard for [OwnTone](https://github.com/owntone/owntone-server)
(forked-daapd) — built for HomePods, multi-room listening and beautiful live radio.
OwnTone ships a minimal built-in web UI. This dashboard replaces it with a
premium experience: live radio cards with health probes, a persistent listening
history, schedules with sunrise ramps, night-safe volume, multi-room AirPlay
control, queue management, folder browsing and a zero-dependency Python
companion service.
**No build step. No Node on your server. No database.** Static files + nginx +
one small Python service.
## Features
- 📻 **Live radio cards** — server-side stream health probes (LIVE / OFFLINE),
per-station quality labels, favorites, drag & drop, live "now playing" text
- 🎛 **Full player** — artwork with mood accent, track chips (codec / rate / size),
seek, queue drawer with drag-reorder and swipe-delete
- 🌙 **Night-safe volume** — manual playback capped (default max 8 %) between
configurable hours, including a per-schedule opt-in cap
- ⏰ **Scheduler** — weekly rules, radio or playlists, AirPlay output, volume,
sunrise ramp (raise volume over N minutes), optional stop time, fallback
station if the stream is dead, "Play now"
- 😴 **Sleep timer** — server-side fade-out over the last 3 minutes, works with
the dashboard closed
- 🔊 **Multi-room AirPlay** — select and tune every speaker independently,
group volume, room presets ("scenes"), optional browser output alongside
AirPlay
- 🗂 **Library browsing** — recently added albums, genre & artist chips,
folder browser with per-track "play next" / "add to queue"
- 📝 **Playlist editor** — create and edit plain `.m3u` playlists from the UI
- 📊 **Insights** — 30-day play chart, top stations & artists, live activity feed
- 🖥 **Screensaver** — fullscreen blurred artwork + clock after 60 s idle
- 🔔 **Desktop notifications** for scheduled runs and timer events
- ⌨️ **Keyboard shortcuts** — space, arrows, M/N/P/R, `?` for the legend
- 🏠 **HomeKit bridge recipe** — expose every station to Siri (see below)
- 🧩 **Companion service** — pure Python stdlib: schedules, history, stats,
radio health, playlist & station file management
| Radio mode (dark) | Music mode (light) | Mobile |
| ------------------------------------ | ------------------------------------- | -------------------------------------- |
|  |  |  |
## Quick start
Requirements: a host running OwnTone, nginx and Python 3.
```sh
git clone https://github.com/vladimirperovic/owntunedashboard.git /opt/owntone-dashboard
# 1. nginx site (serves the dashboard on :3690, proxies /api and /scheduler)
sudo cp deploy/nginx.conf /etc/nginx/sites-available/owntone-dashboard
sudo ln -sf ../sites-available/owntone-dashboard /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 2. companion service (schedules, history, stats, radio health)
sudo install -m0644 deploy/owntone-dashboard-scheduler.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now owntone-dashboard-scheduler.service
```
Open `http://<your-host>:3690`. If OwnTone is not reachable the dashboard runs
in **Preview mode** with demo data — the same mode powering the
[live demo](https://vladimirperovic.github.io/owntunedashboard/).
Full walkthrough, including the manual checks that automated tests cannot
cover: [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md). Your own hosts stay out of
git via `deploy/deploy.local.conf` (see `deploy/deploy.local.conf.example`).
> The companion API is **unauthenticated**. It binds to localhost, but the nginx
> site proxies it to the whole network. Fine on a home LAN, not fine anywhere
> else — see [`docs/KNOWN-ISSUES.md`](docs/KNOWN-ISSUES.md).
## Configuration
Shipped defaults and the asset loader live in [`config.js`](config.js). On an
installed host, put browser overrides in `/etc/owntone-dashboard/config.json`
and custom images in `/etc/owntone-dashboard/artwork/`. The common installer
links these into each release as `site-config.json` and `site-assets/`, so an
update preserves them. For a development checkout, edit `site-config.json`.
Only browser-safe settings belong in this JSON file; it is publicly readable.
Optional `scheduler.env`, `update-api.env` and `installer.env` files in the same
external directory configure services and are not served by the dashboard.
See the [migration and configuration instructions](docs/DEPLOYMENT.md#operator-configuration).
## Radio stations
Drop one `.m3u` file per station into a folder whose path contains `/Radio/`
(configurable) and refresh the OwnTone library — or use the built-in
**Manage stations** dialog, which writes the files and rescans for you.
## HomeKit / Siri
Pair the dashboard's stations with Apple Home using any HomeKit HTTP-switch
bridge (e.g. [homebridge](https://homebridge.io) +
[homebridge-http-switch](https://www.npmjs.com/package/homebridge-http-switch)):
- ON → `POST http://<host>:3690/scheduler/stations/<slug>/play`
- OFF → `POST http://<host>:3690/scheduler/playback/stop`
- state → `GET http://<host>:3690/scheduler/stations/<slug>/status`
- list stations → `GET http://<host>:3690/scheduler/stations`
Name a switch per station (plus a short alias you would actually say) and ask
Siri to turn it on. Switch state is read from OwnTone's live player, so it stays
correct after a service restart and after a scheduled run. A `POST /scheduler/stations/random/play` endpoint
is included for a "Shuffle Radio" switch.
## macOS menu bar
`tools/menubar/owntone.10s.sh` is a ready-made
[SwiftBar](https://swiftbar.swiftbar.app) plugin: current track in the menu
bar with play/pause, next/previous and volume presets.
## Radio station detection
A playlist counts as a station when its file path contains `radioPathHint`
(`/Radio/` by default). `radioNameHints` in [`config.js`](config.js) adds
whole-word name fallbacks for libraries where stations are not all in one
folder — keep that list short, since anything in it can misread an album
playlist as a station. The companion service reads the same two settings from
`OWNTONE_RADIO_PATH_HINT` and `OWNTONE_RADIO_NAME_HINTS`; keep them in step.
## Development
```sh
npm install
npx playwright install chromium
```
| Command | What it checks |
| ---------------------------------------------------------- | ---------------------------------------- |
| `npm run lint` | ESLint over every script |
| `npm run format` / `npm run format:check` | Prettier |
| `npx playwright test` | Desktop and mobile UI, against demo mode |
| `python3 -m unittest discover -s scheduler -p 'test_*.py'` | Companion service logic |
| `ruff check scheduler/` | Python lint |
CI runs all of them on every push and pull request.
UI tests start their own server on `127.0.0.1:4185` and refuse to reuse an
existing server. Set `OWNTONE_TEST_PORT=4186` when that port is occupied or when
running another browser suite in parallel.
For a local DOM observer measurement, start `PORT=4184 node tests/static-server.js`
in one terminal, then run `node tools/measure-ui.cjs` in another. The script
counts observer callbacks in preview mode; it does not measure production CPU
time or network latency.
### How the front end is put together
No build step — plain `<script>` files loaded in order by
[`config.js`](config.js), which is also where the `?v=BUILD` cache buster
lives.
- [`shared.js`](shared.js) is the common layer: config, the request helpers for
both back ends, `escapeHtml`, the toast, the event bus, the icon set, the
night-safe rule, and `startPlayback()` — the single entry point every
playback path goes through.
- [`app.js`](app.js) owns the player, the library and the radio grid, and
announces `owntone:ready` and `owntone:library-updated`. Player consumers use
`owntone:player-updated`, emitted synchronously after `renderPlayer()` completes,
and `owntone:artwork-updated`, emitted when the current artwork finishes loading
successfully. Player events carry a detached, deeply frozen playback snapshot;
artwork events signal image completion. Modules synchronize once on mount as well, since
player and artwork events are not replayed to late subscribers.
- [`app-state.js`](app-state.js) provides the private owner state, a live read-only
view and detached snapshots. Feature modules read `window.OWNTONE_APP.state`
or `getSnapshot()` and invoke actions such as `selectPhysicalOutputs()` and
`setPhysicalOutputVolume()`. They cannot mutate shared state directly. Timer
handles, drag flags and pairing-dialog state stay local to their owners.
See the [architecture review](docs/ARCHITECTURE-2026-09-08.md) for the implemented
optimizations, measurements and remaining frontend/backend priorities.
CSS uses cascade layers, declared at the top of
[`styles.css`](styles.css):
```css
@layer base, components, system, features, polish;
```
A later layer wins over an earlier one regardless of selector specificity, so
overriding a base rule does not need `!important` — there are none in the
stylesheets.
## Disclaimer
This is an independent community project and is **not** affiliated with or
endorsed by the OwnTone project. "OwnTone" is used solely to describe
compatibility.
## License
[MIT](LICENSE)