# 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. 6. Music works differently, on purpose: pick a track (browsing Artist → Album → Track) to start a listening party. There's no host for playback — anyone can add more tracks to the shared queue at any time, anyone can pause/resume, and skipping the current track needs a majority vote from whoever's currently listening. The player has its own inline "Ajouter des morceaux" browser, so building the queue doesn't mean leaving the page. Everything — catalog, images, video, audio, 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. ## Status Login, and the rendering/navigation logic for every page (sidebar, home, library, series, artist/album, both players), have been exercised end to end (login against the real Emby server with intentionally-wrong credentials; the pages against mocked API responses; the music queue's timer/vote/pause logic against the real server modules directly - no Emby involved there at all). 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, artists/ albums/tracks, `PlaybackInfo`'s media streams) and actual HLS playback (video or audio) are still unverified. The TypeScript rewrite itself is separately verified: both projects type-check in strict mode, the compiled server passes the same unit/integration tests the pre-rewrite JS did (run against the compiled output, not the source), and the compiled+bundled client was driven through every page and both players end to end (mocked network/Socket.IO) in a real browser. **Not verified**: an actual `docker build` of the new multi-stage Dockerfile - this dev environment has no Docker - so treat the first real build as the thing to double-check, even though it mirrors exactly the `npm run build` commands already confirmed working locally for both projects. ## Project layout Both `server/` and `client/` are TypeScript now (see [docs/architecture.md](docs/architecture.md#typescript-and-a-service-oriented-architecture) for the why and the migration itself) - each its own project with its own `package.json`/`tsconfig.json`/build step; `server/src` and `client/src` are the only source of truth, `server/dist`/`client/dist` are generated (gitignored) and never hand-edited. ``` server/ Node.js + Express + Socket.IO backend: proxies ALL Emby access (auth, catalog, images, HLS streaming) and coordinates party/room state + realtime sync. The browser only ever talks to this. TypeScript, compiled by tsc to server/dist. src/index.ts Express app + route wiring only - thin, delegates to the services below src/config.ts env-derived config (EMBY_SERVER_URL, PORT) src/types.ts shared interfaces (Party, ItemDto, the Socket.IO event maps, ...) src/Logger.ts party/stream lifecycle logging src/emby/EmbyClient.ts low-level Emby HTTP + response proxying (incl. HLS) src/services/AuthService.ts server-side login + session store src/services/CatalogService.ts all Emby browsing/streaming + DTO shaping src/services/PartyService.ts in-memory party lifecycle (video queue, host handoff) src/services/MusicQueueService.ts participative music queue - see docs/architecture.md src/realtime/SocketGateway.ts Socket.IO protocol (see comments in the file) client/ Frontend: TypeScript, esbuild-bundled to client/dist (one app.js + one app.css - see client/build.mjs). Talks only to server/ - never to Emby directly. Each view is its own folder colocating its .ts/.html/.css. src/app.ts entry point - registers routes, boots the app src/router.ts tiny hash router (unchanged in spirit - see docs) src/types.ts shared interfaces (client-side copies of the server's DTO shapes - see docs/architecture.md) src/api/embyApi.ts calls our own /api/catalog/* + auth routes src/api/party.ts our own backend's party REST + Socket.IO client src/tiles.ts + tiles.css poster-tile rendering shared by home/library/artist src/styles/ base.css (global reset/vars/primitives) + small shared component styles (presence, detail header, track list) src/shell/Shell.* persistent sidebar + #mainContent the router swaps src/views/login/Login.* sign-in form src/views/home/Home.* live parties carousel + per-library "recently added" src/views/library/Library.* one library's tile grid (or artist grid, for music) src/views/series/Series.* Series detail + season tabs + episode list src/views/artist/Artist.* one artist's albums src/views/album/Album.* one album's tracks src/views/player/Player.* the /party/:id route's dispatcher + the video player src/views/musicPlayer/MusicPlayer.* the music half - now-playing, queue, inline browse+add src/WatchParty/ The earlier Emby-plugin attempt (C#, unrelated to the TypeScript above). 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 Both projects need `npm install` once; the server needs the client already built at least once (it serves `client/dist` as static files). ```bash cd client && npm install && npm run build && cd .. cd server && npm install && npm run build EMBY_SERVER_URL=https://your-emby-server npm start ``` Then open `http://localhost:3000`. For active development, `client`'s `npm run watch` rebuilds on save, and `server`'s `npm run dev` runs the server straight from TypeScript via `tsx` (no separate compile step needed while iterating) - run both in separate terminals. ## Deploying Build the Docker image from the **repo root** (it needs both `server/` and `client/`) - the Dockerfile compiles both TypeScript projects itself in a build stage, so nothing needs building beforehand: ```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://: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 - [x] Music: dedicated player, Artist/Album/Track browsing, participative queue (free add, vote-to-skip, no host) — see docs/architecture.md - [x] Full TypeScript rewrite (client + server), component-style client views (.ts/.html/.css each), server-side singleton services per feature — see docs/architecture.md - [ ] Try a full party end-to-end with two real accounts (still the main gap) - [ ] 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).