Skip to content

api: OpenAPI spec, self-hosted Swagger UI, uniform CORS, and an SSE live-state stream - #58

Draft
vynulldev wants to merge 5 commits into
mainfrom
api-spec
Draft

vynulldev wants to merge 5 commits into
mainfrom
api-spec

Conversation

@vynulldev

Copy link
Copy Markdown
Owner

What & why

Vynull is a headless daemon, and the HTTP API is the real contract every client builds against. The bundled web UI, the now-playing overlay, and MPRIS are all just clients of it. This PR makes that explicit and gives anyone the pieces needed to build a separate frontend (a native app, a phone remote, an alternative UI) without reading the Go handlers.

Four parts, each a self-contained commit:

  1. An OpenAPI 3 spec (docs/api/openapi.yaml). Full coverage of the API, ~56 paths across status/players, the library (tracks, per-track edits, playlists, tags), playback, analysis (waveforms, cues, beatgrids, artwork, audio), import/export, settings/menu, overlay, device link, diagnostics, and the file browser. Schemas are transcribed from the actual Go response structs. It also states the posture a frontend author needs up front: no auth (trusted LAN; bind 0.0.0.0 for cross-device), the live-update options, and the raw-bytes endpoints.

  2. A self-hosted Swagger UI, served alongside the spec. New endpoints, always on so even a headless daemon with --web off documents itself:

    • GET /api/openapi.yaml the spec
    • GET /api/docs a browsable Swagger UI (including "Try it out" against the live API)
    • /api/swagger/* its assets

    Swagger UI is vendored (swagger-ui-dist 5.17.14, Apache-2.0, under docs/api/swagger/) and embedded, so the docs page pulls nothing from a CDN and works offline on the link-local network, matching how the rest of the UI bundles its fonts and assets. This adds about 1.6 MB to the binary.

  3. Uniform CORS. A middleware wraps the API mux so every endpoint sends permissive CORS (Access-Control-Allow-Origin: *, methods, Content-Type) and answers preflight OPTIONS uniformly, instead of the former per-handler patchwork where some endpoints set it and most POST routes had no preflight at all. Since Vynull is a trusted-LAN tool with no auth, this only makes the already-open posture consistent so a browser frontend on another origin works. The old per-handler CORS lines remain correct (the middleware sets the same header, so no duplication, verified) and are now redundant; a later cleanup can drop them.

  4. An SSE live-state stream (GET /api/events). A Server-Sent Events stream pushing live deck + now-playing state, so a frontend gets real-time updates instead of polling /api/players. Each message is an event: state carrying {players, nowplaying} (the same shapes those endpoints return), sent on change, effectively a few Hz while a deck is active and a heartbeat when idle. A client interpolates the playhead between events with beat_age_ms.

Net effect: a frontend author gets a browsable contract, open CORS, audio streaming (from #56), and a real-time state stream. That is the foundation the "separate frontends" idea needs.

Hardware testing

  • Tested on: N/A, this does not affect deck behaviour. Everything here is additive to the HTTP API (the spec, the docs endpoints, a CORS header middleware, and a new read-only SSE stream). Decks talk Pro DJ Link / dbserver / NFS, none of which this touches. Verified at the HTTP layer: all doc endpoints serve and the docs page references only local assets (no external calls); CORS headers and OPTIONS preflight are present on every endpoint with no duplicate headers; the SSE stream is text/event-stream and primes the client on connect.

Checklist

  • go build ./..., go vet ./..., and go test ./... pass
  • gofmt -l . is clean
  • New source files carry an SPDX header (GPL-3.0-or-later); vendored Swagger UI is Apache-2.0 (docs/api/swagger/LICENSE)
  • Tested on real hardware (deck + firmware noted above), or this change doesn't affect deck behaviour
  • I agree my contribution is licensed under the project's GPLv3

Vynull is a headless daemon and the API is the real contract every client
builds against (the web UI, the overlay, and MPRIS are all just clients).
Document it so a separate frontend, native or otherwise, can be built
against a contract instead of reading the Go handlers.

docs/api/openapi.yaml covers the full surface: ~54 paths / 73 operations
across status/players, the library (tracks, edits, playlists, tags),
playback, analysis (waveforms, cues, beatgrids, artwork), import/export,
settings/menu, overlay, device link, diagnostics, and the file browser.
Schemas are transcribed from the Go response structs. It also states the
posture for frontend authors: no auth (trusted LAN; bind 0.0.0.0 for
cross-device), poll-based live updates (beat_age_ms for playhead
interpolation), partial CORS, and the raw-bytes endpoints (artwork,
waveform-png, preview). Validated: YAML parses, every schema ref resolves.
Add GET|HEAD /api/audio/{trackID} to the OpenAPI spec: serves a library
track's audio by ID with Range/seek support, native formats direct and
others transcoded to WAV, and the X-Audio-Source header. It's the audio
primitive a separate frontend (native app, phone remote) can build on.
Make the HTTP API self-describing. New endpoints, always on (not gated on
--web) so even a headless daemon documents itself:

  GET /api/openapi.yaml   the spec
  GET /api/docs           a browsable Swagger UI
  /api/swagger/*          its assets

Swagger UI is vendored (swagger-ui-dist 5.17.14, Apache-2.0, in
docs/api/swagger/) and embedded, so the docs page pulls nothing from a CDN
and works offline on the link-local network, matching the rest of the UI.
The spec lives in docs/api/ as its canonical home; a small apidocs package
there embeds it and the assets. Adds ~1.6MB to the binary.

Verified live: all four endpoints serve, and the docs page references only
local /api/swagger/ assets (no external calls).
Wrap the API mux in a CORS middleware so every endpoint sends permissive
CORS (Access-Control-Allow-Origin: *, methods, Content-Type) and answers
preflight OPTIONS uniformly — instead of the former per-handler patchwork
where some endpoints set it and some (e.g. /api/status) didn't, and most
POST endpoints had no preflight at all. This lets a browser frontend on
another origin call the whole API.

Vynull is a trusted-LAN tool with no auth, so this only makes the already
open posture consistent. Existing per-handler CORS lines remain correct
(the middleware sets the same header via Set, so no duplication — verified)
and are now redundant; they can be removed in a later cleanup. Spec's CORS
note updated to match.
Add GET /api/events, a Server-Sent Events stream pushing live deck +
now-playing state, so a frontend gets real-time updates instead of polling
/api/players at ~1 Hz. Each message is an 'event: state' carrying
{players, nowplaying} (the same shapes those endpoints return), sent on
change — effectively ~4 Hz while a deck is active (beat_age_ms keeps
moving) and nothing while idle, with a heartbeat to hold the connection.
A client interpolates the playhead between events with beat_age_ms, so a
few per second render smoothly. CORS applies via the mux middleware.

Documented in the spec; the 'live updates' note now points at the stream.
Verified live: text/event-stream, initial snapshot primed on connect.
@vynulldev vynulldev added documentation Improvements or additions to documentation enhancement New feature or request labels Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant