Skip to content

Repository files navigation

listen-together

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

Why a sidecar (and not a Navidrome plugin)

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.

Key properties

  • 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.

Public instance

An official, best-effort instance runs at party.alsogamer.com:

  • Sync server URL: https://party.alsogamer.com (clients derive wss://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.

Quick start

# 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}

Project layout

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

Documentation

Develop

make test     # go test ./...
make race     # with the race detector
make lint     # gofmt check + go vet

About

Synchronized-playback (listening party) server for Navidrome and any Subsonic-compatible server. Mirror of git.alsogamer.com/alsog/listen-together

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages