Skip to content

decriptor/Roon.Extensions.DJ

Repository files navigation

RoonDJ

CI License: MIT

A Roon extension that lets house guests view the live play queue and add music to it from their phones — a collaborative party DJ. The host configures everything from Roon's own Extensions settings; guests just open a URL on the Wi-Fi.

  • Now Playing card + live queue that updates on every phone in real time, with a toggle between Up Next and Recently Played
  • Search your full Roon library (local + streaming) — tracks, albums, and artists (tap an artist to browse their music), with type/artist filters and result counts
  • Add tracks or albums to the queue; live "Added by …" attribution
  • Host-controlled zone, access mode, what can be added, where adds land, and limits
  • Mobile-first, accessible UI (WCAG AA, keyboard-operable, screen-reader friendly)
  • One-tap QR join — show /join on any screen and guests scan to open the app

Screenshots

Now Playing + queue Search (artist filter) Artist drill-in
Now playing Search Artist

Architecture

One Node.js + TypeScript process is the Roon extension, a Fastify + Socket.IO server, and the host of the React web app. All Roon access is behind a RoonGateway interface, so a FakeRoonGateway drives the unit tests and full Playwright e2e with no hardware.

shared/   TypeScript types shared by server + web
server/   Roon extension + Fastify API + Socket.IO + composition root
web/      React + Vite guest app
e2e/      Playwright specs (run the server against the fake gateway)

See docs/superpowers/specs/ and docs/superpowers/plans/ for the design and plan.

Prerequisites

  • Node.js 22+ (an .nvmrc pins the version; run nvm use)
  • A Roon Core on the same LAN (for real use)

npm install pulls the node-roon-api* dependencies straight from RoonLabs' public GitHub repositories (the official open-source SDK), so install needs network access to GitHub. They are commit-pinned for reproducible installs.

Install & build

npm install
npm run build        # builds shared, server, and web

Run (production)

npm run build
npm start            # starts the server on PORT (default 8080)

For unattended/party use, run npm start under a process supervisor that restarts on exit (a systemd unit with Restart=always, or pm2) so a crash brings the server back automatically.

On start it prints the LAN URLs to share with guests. Then, in Roon:

  1. Open Roon → Settings → Extensions.
  2. Enable "Roon DJ".
  3. Click its Settings and choose the party zone plus any options (access mode, what guests can add, where adds land, per-guest limit, etc.).
  4. Share the URL with guests — easiest is to open the built-in QR page at http://<host>:8080/join on any screen (TV, tablet, your phone) and have guests scan it. The startup log prints both the app URL and the /join URL.

Settings are live — changing them in Roon updates guest behavior immediately.

Pairing with a Core writes config.json, which holds live Roon pairing secrets (tokens). Never commit or share it — it is gitignored. See config.example.json for the shape.

Host settings

Setting Options Default
Party zone your Roon zones first zone
Guest access Open · Passcode · Name required Open
Party passcode text empty
What guests can add Tracks · Tracks+Albums · Tracks+Albums+Playlists Tracks+Albums
Where adds land End of queue · Play next End of queue
Max adds per guest 0 = unlimited 0
Show Now Playing to guests Yes/No Yes

Develop

Two terminals:

# terminal 1 — API + Roon extension (real Core), watch mode
npm run dev -w @roon-extensions/dj-server

# terminal 2 — Vite dev server with HMR (proxies /api and /socket.io to :8080)
npm run dev -w @roon-extensions/dj-web      # http://localhost:5173

To develop the UI without a Roon Core, run the fake-backed full stack:

npm run build -w @roon-extensions/dj-shared && npm run build -w @roon-extensions/dj-web
ROONDJ_FAKE=1 PORT=8080 node --import tsx/esm server/src/fakeServer.ts
# open http://localhost:8080

Test

npm test             # unit tests — server + web (Vitest)
npm run e2e          # Playwright e2e + axe accessibility (Desktop Chrome, Pixel 5, iOS Safari)

The e2e suite boots the stack against FakeRoonGateway, so it needs no Roon hardware and is CI-friendly. The CI badge at the top of this README reflects live status; see .github/workflows/ for the exact jobs.

Verified

  • ✅ A unit-test suite (Vitest) — server-side coverage of access modes, add rules, caps, art cache, queue relay, routes, realtime, search/browse, and text cleanup (with enforced coverage thresholds), plus web-side tests (jsdom + Testing Library) for the time formatting and search-row behavior.
  • Playwright e2e across Desktop Chrome, Mobile (Pixel 5), and iOS Safari (WebKit): queue render, search + add, browse drill-in + focus management, live cross-client updates, passcode/name gates, per-guest cap, reconnect + no-zone states, and zero axe accessibility violations with keyboard navigation. CI runs the full unit + e2e suites on every push — check the badge above for current pass/fail.
  • ✅ Validated end-to-end against a live Roon Core in both Chromium and WebKit, including over plain-HTTP LAN access (a non-secure context — note the app uses crypto.getRandomValues, not the secure-context-only crypto.randomUUID).
  • ℹ️ The real RoonGateway (in server/src/roon/) talks to a live Roon Core. On first run, smoke-test: enable in Roon → pick a zone → open /join on a phone → search/add a track and an artist → confirm it lands and shows on a second device. A build id is shown in the app header to confirm clients are on the latest build.

Privacy & data

Guest display names are optional — they only appear in name-required mode, or if a guest chooses to type one. When present, a name lives only in bounded in-memory caches: it is shown to other guests as "added by <name>" attribution and in the in-memory history, and it is gone when the server restarts. Guest data is never written to disk, sent off-box, or tracked. There are no analytics, no telemetry, no cookies, and no third-party network calls — all data is in-process and LAN-local, and the server does not log guest IPs or queries.

A guest's own browser keeps their chosen name and a random session id in localStorage (and the party passcode in sessionStorage) on their device, so they don't have to re-enter them. The only thing the host writes to disk is Roon's pairing state in config.json (the Core connection tokens — see below); no guest data is persisted there.

Deploying / updating

After rebuilding the web app, restart the server. The static file server indexes the built asset set at startup, so a rebuild while the server is running keeps serving the stale bundle until you restart. (This only affects deploy actions — it never happens during a live party.)

License

MIT — see LICENSE. RoonDJ is original work; the Roon integration is isolated to the server/src/roon/ directory, which uses the official open-source Roon SDK (node-roon-api*, Apache-2.0). No proprietary Roon Labs code is included. See THIRD-PARTY-NOTICES.md. "Roon" is a trademark of Roon Labs LLC; this is an independent extension, not affiliated with Roon Labs.

Roadmap (v2)

  • Host approval/moderation flow (a live /host view for pending adds)
  • Vote-to-skip and richer guest powers
  • Full reorder/remove (constrained by Roon's public API)
  • Verify/expand playlist add-as-target and per-artist top-tracks

The HostSettings model already carries fields for moderation and guest powers; they're not yet surfaced in the Roon settings UI to avoid exposing inert controls.

About

Roon DJ — a collaborative party-queue web app and Roon extension. Guests scan a QR and add music to the live queue from their phones.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors