A small, standalone synchronized-playback ("listening party") server for Navidrome and any other Subsonic-compatible music server. One person hosts a room; everyone else's player follows their play / pause / seek / track changes in near-real-time.
The reference client is a fork of Feishin, but the protocol is plain WebSocket + JSON, so any client (web page, CLI, mobile) can join a room.
Feishin A (host) ──ws──┐
├──► listen-together ──(validate creds)──► Navidrome /rest/ping
Feishin B (follower)─ws─┤ rooms (in-mem)
any custom client ──ws──┘ host transport authority
roomState broadcast + clock sync
Navidrome's plugin system only allows plugins to be outbound WebSocket clients — they cannot host a server that many clients connect into. A sync coordinator must accept inbound connections and broadcast authoritative state, so it can't be a plugin. This is a tiny separate service instead, decoupled from Navidrome internals so any client can speak its protocol and it works against any Subsonic server. See docs/ARCHITECTURE.md.
- Auth via your own server. Clients send their own Subsonic credentials,
validated against their own server's
/rest/ping. Reuses existing accounts; works with Navidrome and any Subsonic server. - Minimal, privacy-preserving sync. Only
trackId, queue (list of IDs), index, position, and play/pause are shared. Stream URLs are never shared — each client builds its own from its own session. - One host per room holds transport authority; followers are read-only and can request / be handed control. Host is reassigned automatically on leave.
- Clock sync via
ping/pong: followers hard-seek when drift exceeds the threshold (~250 ms target). - Ephemeral in-memory rooms — no database.
An official, best-effort instance runs at party.alsogamer.com:
- Sync server URL:
https://party.alsogamer.com(clients derivewss://party.alsogamer.com/ws). In Feishin: Settings → Playback → Listen Together. - Locked to one music server: it only accepts accounts on the maintainer's own Navidrome server, so it's ready to use if you already have an account there. To run Listen Together against any other Subsonic/Navidrome server, self-host (below).
- Everyone in a room is on that same server — only track ids and positions are synced, never stream URLs or audio.
It's a live instance rather than a throwaway, but still best-effort: rooms are ephemeral and there are no hard uptime guarantees.
# local
make run # or: LT_PORT=4040 go run ./cmd/listen-together
# docker
docker compose -f docker-compose.example.yml up --build| Env | Default | Meaning |
|---|---|---|
LT_PORT |
4040 |
HTTP/WS listen port |
LT_ALLOWED_SERVERS |
(none) | Comma-separated allowlist of server base URLs. Empty = any server accepted (open relay; fine locally, not for production). |
LT_ALLOWED_ORIGINS |
(none) | Comma-separated allowlist of browser http(s) origins for the WS upgrade. Empty = any origin. Only http(s) origins are gated; native/desktop clients (no Origin, null, or a non-web scheme like file:// for an Electron app) are always allowed, so this can't lock out desktop clients. |
LT_MAX_ROOMS |
0 |
Cap on concurrent rooms. 0 = unlimited. |
LT_MAX_MEMBERS_PER_ROOM |
0 |
Cap on members per room. 0 = unlimited. |
LT_STATS_TOKEN |
(none) | If set, enables GET /stats protected by this bearer token. Empty = endpoint disabled. |
LT_AUTH_TIMEOUT |
0 |
Seconds a connection has to authenticate before it's dropped. 0 = built-in default (20s). |
Endpoints: GET /ws (WebSocket), GET /healthz, and GET /stats (when LT_STATS_TOKEN is set).
/stats returns JSON load counters; pass the token as ?token=… or Authorization: Bearer …:
curl -H "Authorization: Bearer $LT_STATS_TOKEN" http://localhost:4040/stats
# {"rooms":2,"members":5,"clients":5}cmd/listen-together/ entrypoint, env config, HTTP wiring
internal/protocol/ wire types: envelope, events, payloads (no deps)
internal/auth/ Subsonic-ping credential validation + cache
internal/room/ ephemeral session state (WebSocket-agnostic, id-based)
internal/hub/ WebSocket transport: clients, dispatch, broadcast
docs/ architecture, protocol, client guide, deployment
- docs/ARCHITECTURE.md — components, data flow, design decisions
- docs/PROTOCOL.md — full WebSocket protocol reference + examples
- docs/CLIENT_GUIDE.md — how to build a client (clock sync, queue resolution, echo suppression); Feishin integration notes
- docs/DEPLOYMENT.md — env, Docker, TLS/reverse proxy, security, scaling
- docs/TROUBLESHOOTING.md — common WebSocket / auth / proxy problems and fixes
make test # go test ./...
make race # with the race detector
make lint # gofmt check + go vet