Big UI pass towards looking/behaving like Emby. Deferred: music (needs
its own player + a real-time collaborative playlist - different enough
subsystem to be its own pass; sidebar/routing already treat it as a
first-class collectionType, page just says "not yet" for now).
Layout:
- client/js/shell.js: persistent sidebar (one entry per Emby library,
from GET /api/catalog/libraries which now also returns
collectionType) + a #mainContent the router swaps on navigation.
router.js made idempotent (start() safe to call more than once) so
a login -> shell boot can happen twice in one page lifetime (logout
then back in) without piling up duplicate hashchange listeners.
- app.js's boot() renders either the bare login screen or the full
shell depending on session state; login.js calls it again on
success instead of navigate() (no router running yet at that point).
Browsing, by collection type:
- views/home.js (route /home): a carousel of every active party
server-wide, a join-by-code fallback, then one "recently added"
carousel per non-music library (GET /api/catalog/libraries/:id/recent,
Emby's Items/Latest).
- views/library.js (route /library/:id): dispatches on collectionType -
movies/tvshows get the tile grid (Series tiles link into /series/:id
instead of starting a party directly), music gets an honest
placeholder.
- views/series.js (route /series/:id): season tabs + episode list
(new GET /api/catalog/series/:id/seasons and .../episodes, Emby's
/Shows/{id}/Seasons and /Episodes).
- tiles.js: poster-tile rendering shared by home.js and library.js so
they can't drift apart.
Episode queue / "next episode":
- POST /api/parties now builds a queue when starting from an episode
(fetches the rest of the series, from that episode onward) -
partyManager.js gained queue/queueIndex/advanceQueue.
- New WS message {type:"next"} (host-only) advances the queue and
broadcasts {type:"itemChanged", itemId, itemName, ...}; every client
reloads the player against the new item. player.js shows "Épisode
suivant" when hasNext, and auto-sends next() on the video's `ended`
event.
Player: language/subtitles/quality:
- New GET /api/catalog/item/:id/playback-options exposes PlaybackInfo's
MediaStreams as audio/subtitle track lists.
- master.m3u8 now forwards AudioStreamIndex/SubtitleStreamIndex/
MaxStreamingBitrate to PlaybackInfo; player.js has three <select>s
(host-only) that reload the stream at the same position on change.
Subtitles end up burned into the transcode (via SubtitleStreamIndex),
not a toggleable track - simpler than sidecar WebVTT for this pass.
"Quality" is a MaxStreamingBitrate ceiling, not true adaptive bitrate.
Verified by driving a browser against mocked /api/* responses (see
session history): sidebar renders/highlights correctly, home carousels
render (parties + one row per non-music library, music correctly
skipped), library grid dispatch works, series page season-tab
switching + episode list render and the season/episode navigation
flow works end to end. Real Emby response shapes for all of this
(Views' CollectionType, Seasons/Episodes, Items/Latest, PlaybackInfo's
MediaStreams) and the queue/track-picker protocol are NOT yet
confirmed against a live server - see docs/architecture.md.
Removed client/js/views/browse.js, superseded by home.js + library.js.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.8 KiB
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 for the full design, including why this replaced an earlier Emby-plugin attempt (still in src/WatchParty/ for reference — see below).
How it works
- Sign in with your Emby account (just username/password — the server address is fixed server-side, see Deploying below).
- A sidebar lists your Emby libraries; the home page shows every ongoing watch party plus a "recently added" row per library, Emby-style.
- Pick a movie/episode tile and hit "Lancer une watch party" (or drill into a Series' seasons/episodes first) — you become the host — or join one with a 6-character code.
- 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.
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
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/wsHandler.js WebSocket protocol (see comments in the file)
src/partyManager.js in-memory room state
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 + WebSocket 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
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/):
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
- Design + scaffold the standalone app
- Proxy all Emby access server-side; Emby-themed UI (sidebar, carousels)
- TV show navigation (Series → Seasons → Episodes) + episode auto-queue
- Player language/subtitle/quality pickers
- 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.