# Architecture ## Goals - Watch the same title, in sync, with other users of the same Emby server. - Use Emby as the media source (catalog + streaming) without requiring changes to Emby itself or its official clients. - Access gated to people with an Emby account on this server — no anonymous use, no separate identity system. ## Non-goals - Chat/reactions — deliberately out of scope. - OIDC/SSO — an OIDC provider (LemonLDAP::NG) is available in principle, but the explicit choice was Emby-only auth: simpler, no extra infra dependency, and everyone already has an Emby account. - Full library navigation (Shows → Seasons → Episodes) in v1 — search-based catalog only, to keep the first version achievable. Revisit if browsing turns out to matter more than searching in practice. ## Why this replaced an Emby plugin The first attempt (still in [`src/WatchParty/`](../src/WatchParty/), kept for reference) was a real Emby Server plugin: sync via `ISessionManager`'s existing remote-control commands, a settings page via `IHasWebPages`, and — after a fair amount of reverse-engineering — a real user-facing page integrated into Emby's own web client menu (`PluginPageInfo.EnableInUserMenu`, undocumented but confirmed working; required a `data-controller` AMD module to satisfy Emby's modern `ViewManager`, found by reading the target server's actual `viewmanager.js`). That plugin builds and installs correctly. What it hit a hard wall on: **a button inside the video player itself**, or on an item's detail page. An Emby developer confirmed on their own forum that plugins have no API for this ("It's currently not possible for a plugin to do this"), and nothing found in research since suggests that's changed. The only way to get a button into Emby's own player would be patching Emby's shipped web client files directly inside the Docker container — outside the plugin entirely, guaranteed to break on every Emby image update, and requiring manual reapplication each time. That trade-off wasn't worth it. Building a standalone app sidesteps the question entirely: it's our own player, so there's no "how do I add a button to someone else's UI" problem in the first place, and sync can be tighter than nudging Emby's remote-control commands ever allowed (see below). Full trace of the plugin investigation (real errors, real fixes, sources consulted) is in the git history of this file and of `src/WatchParty/` — worth reading if a future Emby SDK update ever makes in-player integration possible and this gets revisited. ## Components ``` server/ Node.js + Express + Socket.IO, TypeScript (compiled by tsc). Proxies 100% of Emby access (catalog, images, HLS/audio streaming) and owns party/room state + the realtime sync channel - see "Auth and Emby access" right below for why it's everything, not just party coordination, and "TypeScript and a service-oriented architecture" further down for the current file-by-file layout (one singleton service class per feature). client/ Frontend, TypeScript, esbuild-bundled to one app.js + one app.css (client/dist) - no framework. Talks only to server/, never to Emby directly (api/embyApi.ts and api/party.ts are both just fetch/Socket.IO wrappers around this app's own /api/* routes). One folder per view, each colocating its own .ts/.html/.css - see "TypeScript and a service-oriented architecture" further down. ``` Both projects predate this exact shape - see git history and "TypeScript and a service-oriented architecture" further down for the plain-JS -> TypeScript rewrite itself, including why (a deliberate, requested choice, not a default) and what changed structurally. ### Auth and Emby access: the server proxies everything Revised after the first version: the browser used to call Emby directly (catalog, images, streaming) and only used `server/` for party coordination. That hit real CORS problems in practice (Emby/its reverse proxy don't answer cross-origin preflights the way that needed), and separately wasn't the wanted shape anyway — the browser holding a real Emby access token at all was more exposure than necessary. Current design: - The browser sends a username/password **once**, to `POST /api/auth/login` on **our own backend** — never to Emby directly. - Our backend (`server/src/embyAuth.js`) calls Emby's `POST /Users/AuthenticateByName` itself, using `EMBY_SERVER_URL` from its own environment (not something the user ever types — see `.env.example`). On success it mints its own opaque session id, stores `{embyToken, userId, userName}` server-side (in-memory, like party state), and hands the browser only that session id, as an `httpOnly` cookie (`wp_session`). The browser never sees or holds Emby's real access token. - Every other request — catalog search, item details, images, HLS streaming, party actions — goes to **our own origin only** (`/api/...`), authenticated by that same cookie. `server/src/embyProxy.js` is what actually forwards these to Emby server-side and streams the response back, attaching the session's real Emby token itself. - The `/api/catalog/stream/:itemId/master.m3u8` and `.../raw` routes proxy HLS specifically: the master playlist is fetched from Emby server-side, and every URI line in it is rewritten to point back through `.../raw` (whether Emby wrote it as relative or absolute) — otherwise hls.js would follow the playlist's own URLs straight to Emby the moment it tried to fetch a segment, defeating the whole point. See `rewritePlaylist` in `embyProxy.js`. - Images (``) and video (`hls.js`/`