watchparty/README.md
Nicolas 0c439259ac
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
Add server-side logging of the party lifecycle
Requested now that the app is actually running on real infrastructure:
visibility from docker logs/journalctl into what's happening (parties
created/destroyed, who joined/left, play/pause, pause requests) without
needing to reproduce a bug through the UI first.

server/src/logger.js: a deliberately tiny partyLog(message) - plain
console.log, prefixed [watchparty:party], ISO timestamp - plus a
who(userId, userName) formatter shared by every call site. No logging
library; this app runs as a single container, not behind a log
aggregator, so a consistent grep-able prefix is what actually helps here.

Covers the party's business lifecycle end to end, logged at the source so
every caller (REST and socket) is covered without repeating the check:
- partyManager.js: create, join, leave (also covers a host handoff or the
  party being destroyed, both fall out of leave), advanceQueue,
  removeIdleParties.
- socketHandler.js: playstate and requestPause, since those two mutate
  member state directly rather than through a PartyManager method.

The one thing that needed real care: the host's player re-sends its
CURRENT play/pause state every 3 seconds regardless of whether anything
changed (the heartbeat), on top of firing per actual play/pause/seeked
event. Logging every "playstate" message as it arrives would mean one log
line every 3 seconds per active party, forever. The handler instead
compares incoming isPaused against the member's previous value and only
logs on an actual flip - so a party generates exactly one line per real
pause or resume, never one per heartbeat tick.

Verified directly against the real shipped modules, not reimplemented
mocks: a scripted create/join/advanceQueue/leave/idle-sweep sequence
against the real partyManager.js produced exactly the expected line at
every step; a fake io/socket pair driving the real socketHandler.js
through start -> 5 unchanged heartbeats -> pause -> 3 unchanged
heartbeats -> resume logged exactly 3 playstate lines (not 9) at exactly
the right points, and a throttled repeat requestPause produced no second
line - all assertions passed, counted programmatically.

docs/architecture.md has the full writeup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 00:53:55 +02:00

132 lines
6.1 KiB
Markdown

# Watch Party for Emby
A standalone web app for watching the same title, in sync, with other users
of the same Emby server. It reads your Emby catalog and streams your Emby
media, but it's its own app with its own player — not an Emby plugin.
See [docs/architecture.md](docs/architecture.md) for the full design,
including why this replaced an earlier Emby-plugin attempt (still in
[src/WatchParty/](src/WatchParty/) for reference — see below).
## How it works
1. Sign in with your Emby account (just username/password — the server
address is fixed server-side, see Deploying below).
2. A sidebar lists your Emby libraries (movies, TV, music, music videos —
anything else Emby might have, like photos or a boxset collection, isn't
shown); the home page shows every ongoing
watch party live (host, how long it's been running, member count) plus a
"recently added" row per library, Emby-style. No join codes — if a party
is running, it's already on the list, updating in real time.
3. Pick a movie/episode tile and hit "Lancer une watch party" (or drill into
a Series' seasons/episodes first) — you become the host — or hit
"Rejoindre" on any party already listed on the home page.
4. The host's play/pause/seek drives everyone else's player automatically;
starting from an episode queues the rest of the series so the host can
just hit "Épisode suivant" instead of restarting a party each time. If
the host leaves, the party hands off to another member automatically
(their player unlocks live, no reload needed) — only closing once
everyone's gone.
5. Everyone present shows up as a small avatar overlay on the video (their
initials, on the player itself). If you're not the host, clicking your
own avatar asks the host to pause — everyone sees the request, including
the host, without needing chat.
Everything — catalog, images, video, sync — flows through this app's own
backend, which proxies whatever it needs from Emby server-side. The browser
never talks to Emby directly and never holds an Emby access token; it only
ever has a session cookie for this app.
Music isn't part of this yet — it needs its own player and a collaborative
playlist, different enough to be its own pass. The sidebar already lists a
Music library if you have one; the page just says so for now.
## Status
Login, and the rendering/navigation logic for the sidebar/home/library/series
pages, have been exercised end to end (login against the real Emby server
with intentionally-wrong credentials; the pages against mocked API
responses). See [docs/architecture.md](docs/architecture.md#assumptions-to-verify)
for exactly what that does and doesn't confirm — the real Emby response
shapes this now depends on (library `CollectionType`, seasons/episodes,
`PlaybackInfo`'s media streams) and actual HLS playback are still unverified.
## Project layout
```
server/ Node.js backend: proxies ALL Emby access (auth, catalog,
images, HLS streaming) and coordinates party/room state +
realtime sync. The browser only ever talks to this.
src/index.js Express app + REST endpoints
src/embyAuth.js server-side login against Emby + session cookies
src/embyProxy.js generic Emby request/response proxying (incl. HLS)
src/socketHandler.js Socket.IO protocol (see comments in the file)
src/partyManager.js in-memory room state
src/logger.js party lifecycle logging (docker logs/journalctl-friendly)
client/ Static frontend (plain JS, ES modules, no build step).
Talks only to server/ - never to Emby directly.
js/shell.js persistent sidebar + #mainContent the router swaps
js/tiles.js poster-tile rendering shared by home.js and library.js
js/embyApi.js calls our own /api/* routes (auth, catalog, playback options)
js/party.js our own backend's party REST + Socket.IO client
js/views/ one file per screen: login, home, library, series, player
src/WatchParty/ The earlier Emby-plugin attempt. Builds fine and installs,
but Emby has no supported extension point for a player
button, and even menu integration required reverse-
engineering undocumented internals - see
docs/architecture.md for the full story. Kept for
reference; not the active direction.
docs/architecture.md full design doc for the standalone app
```
## Running locally
```bash
cd server
npm install
EMBY_SERVER_URL=https://your-emby-server npm start
```
Then open `http://localhost:3000`.
## Deploying
Build the Docker image from the **repo root** (it needs both `server/` and
`client/`):
```bash
docker build -f server/Dockerfile -t watchparty .
docker run -d --name watchparty -p 3000:3000 -e EMBY_SERVER_URL=https://theater.media.kyuno.fr watchparty
```
Then put it behind your existing reverse proxy like everything else
(`theater.media.kyuno.fr`, `git.dev.kyuno.fr`, ...), e.g.
`watchparty.kyuno.fr` → `http://<container>:3000`.
## Roadmap
- [x] Design + scaffold the standalone app
- [x] Proxy all Emby access server-side; Emby-themed UI (sidebar, carousels)
- [x] TV show navigation (Series → Seasons → Episodes) + episode auto-queue
- [x] Player language/subtitle/quality pickers
- [x] Socket.IO instead of raw WebSockets; fixed two real memory leaks
(dead-connection cleanup server-side, view teardown on navigation
client-side) — see docs/architecture.md
- [x] Live "watch parties en cours" list (host, elapsed time, member count),
live host handoff, no join codes
- [x] Presence overlay (avatars) + "request pause" in the player
- [x] Server-side logging of the party lifecycle (create/join/leave/destroy,
host handoff, play/pause, pause requests) — see docs/architecture.md
- [ ] Try a full party end-to-end with two real accounts (still the main gap)
- [ ] Music: dedicated player, artist/album browsing, collaborative playlist
- [ ] Tune transcoding target / support direct play when possible
No chat, no OIDC — deliberately out of scope (Emby-only auth was the
explicit choice; see docs/architecture.md).
## License
MIT — see [LICENSE](LICENSE).