- server/test/*.test.ts: formalizes this project's many ad-hoc scratchpad test scripts into a committed suite, one file per service (musicQueueService, partyService, socketGateway, catalogService), using Node's built-in node:test + node:assert/strict - zero new dependencies. Runs straight against TypeScript source via `tsx --test` (npm test), no compile step. - server/tsconfig.test.json: extends the base config, overrides rootDir to "." and adds test/**/*.ts to include, so `npm run typecheck` covers the test files too without pulling them into dist/ (npm run build still uses the unmodified base tsconfig, scoped to src/ only). - server/package.json: typecheck now runs against tsconfig.test.json; new test script. - .forgejo/workflows/publish-docker.yml: new `test` job (node:20-alpine container - the publish job's docker:27-cli has no Node) running server typecheck+test and client typecheck+build; publish now depends on it via needs: test. Removed the stale "no test suite yet" comment. - README.md, docs/architecture.md: documented the test suite, how to run it locally, and the new CI job/gating. Verified locally: server typecheck, server test (25/25 passing), server build, client typecheck, and client build all pass individually - the same commands the new CI job runs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
212 lines
11 KiB
Markdown
212 lines
11 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.
|
|
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 server has a real automated test suite
|
|
(`server/test/*.test.ts`, Node's built-in test runner, see Running
|
|
locally below) covering the party lifecycle, the music queue's
|
|
timer/vote/pause behavior, the socket protocol, and the catalog/streaming
|
|
overrides, and the compiled+bundled client was driven through every page
|
|
and both players end to end (mocked network/Socket.IO) in a real browser.
|
|
CI (Forgejo Actions) runs that same typecheck+test+build sequence on
|
|
every push and gates the Docker image publish on it passing — see
|
|
[docs/architecture.md](docs/architecture.md#continuous-integration).
|
|
**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.
|
|
|
|
Server tests (`server/test/*.test.ts`, Node's built-in test runner, no
|
|
extra dependencies or compile step):
|
|
|
|
```bash
|
|
cd server && npm test
|
|
```
|
|
|
|
`server`'s `npm run typecheck` also covers `server/test/`, not just `src/`
|
|
(see `tsconfig.test.json`).
|
|
|
|
## 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://<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
|
|
- [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
|
|
- [x] Automated server test suite (`node:test`) + CI job gating the Docker
|
|
publish on typecheck+test+build passing — 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).
|