All checks were successful
Publier l'image Docker / publish (push) Successful in 18s
Full rewrite, requested directly. Two independent decisions since TypeScript can't compile itself away at runtime - "no build step" and "TypeScript" are mutually exclusive, not a small addition to the existing setup: - Server: plain tsc (no bundling need - Node resolves its own requires). server/dist mirrors server/src 1:1. - Client: esbuild (does need bundling) - picked over a heavier tool (Vite, webpack) since this app's whole client is a couple thousand lines; its built-in CSS-from-JS bundling and a `text` loader for importing an HTML template as a string cover everything needed via a ~30-line hand-written build script (client/build.mjs), not a config file for something built for much bigger apps. Both projects compile independently (no shared-types package - considered and rejected as more structural commitment than this app's size justifies; server/src/types.ts and client/src/types.ts have their own overlapping DTO copies instead). server/'s `npm run dev` uses tsx for a compile-free dev loop; `npm run build` (tsc) is what server/Dockerfile's build stage actually runs. client/'s `npm run watch` is esbuild's own watch mode. Server: one singleton service class per feature, generalizing the PartyManager/MusicQueueService pattern that already existed, rather than route handlers with logic inline: - AuthService (from embyAuth.js), CatalogService (NEW - previously ~15 route handlers' worth of Emby queries/DTO shaping inlined directly in index.js), PartyService (renamed from PartyManager), MusicQueueService (ported as-is, already service-shaped), SocketGateway (from socketHandler.js), EmbyClient (from embyProxy.js - low-level plumbing, not called a "service" since it has no state/feature of its own). - index.ts is now thin wiring only: every route reads the session, calls one service method, returns the result. - Real error-handling pass, not just a port: Express 4 doesn't catch a rejected Promise from an async handler on its own - a new asyncRoute() wrapper + a single error-handling middleware replace what used to be zero handling for a request that outright throws. CatalogService methods throw a CatalogFetchError (own HTTP status - getRawItem now preserves Emby's real 404 instead of a blanket 502) rather than touching res directly. Client: one folder per view (src/views/<name>/<Name>.ts + .html + .css) instead of one .js file with the markup inline as a template literal. A view's static shell moved to its .html (esbuild's text loader); per-item dynamic content (tile grids, episode/track lists) stays as template -literal-returning functions in the .ts, same as before. Genuinely shared CSS (tiles, the presence avatar, the series/album detail-header pattern, track-list rows) lives under src/styles/, imported by whichever views need it; src/styles/base.css holds the global primitives, imported once by app.ts. Deliberately not pushed to zero duplication/renaming shared classes across views for tidiness alone - not worth the risk in a change already this large. Player.ts keeps its pre-existing internal shape (the /party/:id dispatcher + the video renderer in one file, importing renderMusicPlayer from the sibling folder) - that pattern predates this rewrite and only needed types, not restructuring. Two real things the port itself caught, not just carried over: - A type bug: porting SocketGateway's "members" broadcast, reusing PartyService's PublicMember interface would have type-checked fine but was the wrong (larger) shape - the socket broadcast never included isPaused/positionTicks, only the REST response did. Caught by comparing against the original JS line by line, fixed with a dedicated SocketMemberDto type. - A real Express+TypeScript friction point: noUncheckedIndexedAccess (enabled in both tsconfigs - genuinely useful elsewhere, e.g. PartyService.advanceQueue's array indexing) makes every req.params.xxx read a type error, since Express's index-signature typing doesn't know a matched route's required :name segment is always present. Resolved once with a param(req, name) helper rather than 15 routes' worth of scattered assertions. Verified: both projects type-check cleanly in strict mode (tsc --noEmit - not just "esbuild didn't complain", which it wouldn't even with type errors present, since it transpiles without checking). Server: every unit/integration test from the JS version (party lifecycle logging, the music queue's timer/vote/pause behavior incl. real setTimeout auto-advance, the socket integration through SocketGateway+PartyService+ MusicQueueService together, the library-type filter, the playback -override forcing) re-run against the compiled server/dist output - identical results; the compiled server was actually started and confirmed listening cleanly. Client: since esbuild bundles everything into one app.js (no more individually-importable view modules), testing moved to driving the real compiled bundle through its actual boot -> login -> router -> view chain (mocked fetch/Socket.IO/hls.js, real clicks/hash navigation) - arguably more realistic than the old per-module approach. Confirmed this way: login, the sidebar, home's carousels, a video party (presence, host controls, leave), the full music hierarchy, a music party (presence, vote-skip, pause toggle, the inline browse-and-add panel), and the series page - all through client/dist's actual output, no thrown errors anywhere. Not verified: an actual `docker build` of the new multi-stage Dockerfile - this dev environment has no Docker at all. The build stage runs the exact npm run build commands already confirmed working locally, just inside node:20-alpine - the main remaining risk is something environment -specific, not the build commands themselves, but a real build is still the first thing to try after this lands. docs/architecture.md has the full writeup (new "TypeScript and a service-oriented architecture" section, plus a corrected top-level "Components" overview that had gone stale even before this change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
195 lines
10 KiB
Markdown
195 lines
10 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 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://<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
|
|
- [ ] 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).
|