Commit graph

14 commits

Author SHA1 Message Date
5d535d18d8 Switch to Socket.IO; fix two real memory leaks
All checks were successful
Publier l'image Docker / publish (push) Successful in 10s
Requested: Socket.IO instead of raw WebSockets, and explicit attention
to memory leaks. Auditing for the second turned up two real, concrete
leaks, independent of the transport library:

1. Server-side: a raw `ws` server has no idea a client is gone unless
   it gets a proper close frame - a sleeping laptop, a network drop, a
   killed tab never fire 'close', so the PartyMember's socket
   reference (and everything it closes over) sat in memory forever,
   and the party was never cleaned up by anything else either
   (removeIdleParties only looks at lastActivityUtc). Socket.IO's
   built-in heartbeat (pingInterval/pingTimeout) detects and closes
   these out on its own - a real, not just cosmetic, reason to prefer
   it here.

2. Client-side: router.js only ever did currentRoot.innerHTML = ''
   between routes, destroying DOM but never telling the outgoing
   view's JS to stop. Leaving the player any way other than its own
   "Quitter" button (a sidebar link, browser back) left the 3s
   heartbeat setInterval, the realtime connection, and the hls.js
   instance all running against a detached <video> indefinitely, for
   as long as the tab stayed open.

Fixes:
- server/src/socketHandler.js replaces wsHandler.js: same protocol
  (see its own top comment), over Socket.IO. Rooms (socket.join/
  io.to) replace manually iterating party.members to broadcast.
  Error event renamed to "errorMessage" - Socket.IO reserves "error".
- server/src/index.js: socket.io server instead of a raw
  WebSocketServer, same-origin (no CORS config needed).
- client/js/party.js: socket.io-client (window.io, loaded via
  <script> in index.html) instead of hand-rolled WebSocket + JSON
  framing - also gets automatic reconnection for free.
- router.js: route() handlers may now return a cleanup function;
  render() calls whatever the previous view returned before switching
  to the next route, regardless of how navigation happened.
- player.js returns a cleanup that clears the heartbeat, destroys
  hls, disconnects the socket, and removes its document-level click
  listener (now named, not inline-anonymous, specifically so it CAN
  be removed - an anonymous one added fresh per visit would itself
  have been a slow leak even with everything else fixed). The
  "Quitter" button is now just navigate('/home') - cleanup runs the
  same way regardless of how you leave.

Verified for real (see session history): a real Socket.IO client
connected to a really-running local server end to end. Then, with
document.addEventListener/removeEventListener and setInterval/
clearInterval instrumented, drove the router through repeated "join a
party, then navigate away without clicking Quitter" cycles - adds and
removes matched exactly every time (3/3 listeners over three cycles,
2/2 intervals over two), nothing left pending.

docs/architecture.md has the full writeup under "Realtime transport:
Socket.IO, and two real memory leaks".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:07:51 +02:00
02587632ff Fix tile sizing, move player options into a gear menu, surface player errors
All checks were successful
Publier l'image Docker / publish (push) Successful in 17s
1. Carousel/grid tiles had inconsistent widths - classic flexbox/grid
   "min-width: auto" pitfall: a long title (or an image's intrinsic
   size before load) could force a tile wider than its declared
   flex-basis/track size. Fixed with min-width: 0 on .tile. Verified:
   three tiles (one with a deliberately very long title) now measure
   exactly 160px each.

2. Player language/subtitle/quality controls were three always-visible
   dropdowns floating over the video. Moved behind a small gear-icon
   button in the action bar that opens a compact popup panel
   (closes on outside click or on selecting an option) - host-only,
   same as before.

3. Sidebar library entries no longer get an emoji prefix - plain names.

4. Player failures were silent (a caught error with just a code
   comment, or hls.js failing with nothing shown). Added: a loading
   notice while the stream loads, an on-screen error box (with a
   Réessayer button) for fatal hls.js errors and native <video> errors,
   and console.error logging of the technical detail. Confirmed
   working live during testing - a real hls.js fatal error (no live
   Emby stream available in the test environment) was caught and
   displayed correctly instead of failing silently. This is meant to
   turn "the player doesn't work" into an actual diagnosable error
   message next time it happens for real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 22:57:06 +02:00
f563f5b0a2 Emby-style UI: sidebar, carousels, series drill-down, episode queue, player tracks
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
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>
2026-09-13 22:44:49 +02:00
1891e87c83 Fix streaming: use Emby's PlaybackInfo instead of a hand-built m3u8 URL
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
Real error from the field: "Value cannot be null. (Parameter 'key')" -
Emby's own server (.NET) throwing an ArgumentNullException. The
/api/catalog/stream/:itemId/master.m3u8 route was hand-building
/Videos/{id}/master.m3u8?VideoCodec=h264&AudioCodec=aac directly,
missing whatever parameter (MediaSourceId/PlaySessionId/DeviceId are
the likely culprits) Emby's server needs for an internal lookup.

Fixed by calling GET /Items/{id}/PlaybackInfo?UserId=... first and
using the returned MediaSource's own TranscodingUrl - the same thing
real Emby clients do, rather than guessing the URL shape ourselves.
Falls back to a hand-built URL only if no TranscodingUrl comes back,
now at least carrying MediaSourceId/PlaySessionId/UserId from that
response instead of nothing.

docs/architecture.md updated: the real bug and fix, plus a known
limitation this doesn't yet handle (a direct-play source with no
TranscodingUrl at all - hls.js has nothing to fall back to for that
case yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:59:54 +02:00
07613c1cec Redesign home page: library tabs, poster tiles, ongoing parties list
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
- New backend endpoints: GET /api/catalog/libraries (Emby's
  /Users/{id}/Views), GET /api/catalog/libraries/:id/items (top-level,
  Recursive=false - movies as movies, TV as Series tiles), GET
  /api/parties (list every active party server-wide, for the home
  page's "watch parties en cours" section)
- Item DTO gained `type` and `canWatchParty` (only Movie/Episode/Video
  are directly streamable - a Series tile only gets "Voir sur Emby")
  and `embyUrl`, a deep link into Emby's own web client built from
  EMBY_SERVER_URL + serverId (now captured at login)
- Party creation now fetches and stores the item's name/poster so the
  home page's party list doesn't need a separate lookup per party
- client/js/views/browse.js rewritten: ongoing-parties section, a
  join-by-code fallback, one tab per library, and a tile grid with a
  hover-revealed two-button overlay (start party / view on Emby) - the
  first only shown for playable types
- Removed itemDetail.js/the /item/:id route (tiles act directly now,
  no intermediate detail page); player.js simplified to use
  party.itemName instead of a redundant item lookup
- app.css: full-width home layout (no max-width cap, as asked), CSS
  grid handling all the responsive reflow, :hover/:focus-within reveal
  for the tile overlay with an always-visible (hover: none) fallback
  for touch devices

Verified by actually driving a browser (mocked fetch standing in for
/api/parties and /api/catalog/* - real login still needs real
credentials): tab switching swaps content correctly, the overlay
reveals per-tile only, a Series tile correctly omits the watch-party
button, and the touch fallback shows both buttons inline without
hover. See docs/architecture.md for exactly what that does and doesn't
confirm (real Emby /Views and item-list response shapes are still
unverified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:54:14 +02:00
b6818655b1 Proxy all Emby access through our own backend; drop server-url field; use Emby's real theme colors
All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
Three changes requested:

1. Server proxies Emby, browser never talks to it directly. Login is
   now POST /api/auth/login {username,password} on our own backend;
   the backend calls Emby's AuthenticateByName server-side and mints
   its own opaque session (httpOnly cookie), storing
   {embyToken,userId,userName} in memory. The browser never sees or
   holds a real Emby access token. Every other call - search, item
   details, images, HLS streaming, party actions - goes to our own
   /api/* origin, authenticated by that cookie; server/src/embyProxy.js
   forwards to Emby server-side and streams the response back
   (including a playlist-rewrite pass for HLS .m3u8 so hls.js never
   gets handed a URL pointing at Emby directly). This also sidesteps
   the CORS problem the previous direct-to-Emby design had.

2. EMBY_SERVER_URL is backend-only config (.env.example); the login
   form no longer asks for a server address at all.

3. client/css/app.css now uses colors read directly off the live Emby
   server's own dark theme (--theme-primary-color-* etc, fetched via a
   JS probe against the running client, not guessed): accent green
   #52b54b/#6ccf65, near-black background #151216.

Tested for real: ran the server locally against the actual
EMBY_SERVER_URL and submitted intentionally-wrong credentials through
the UI - confirmed the full round trip (browser -> our backend -> real
Emby -> real 401 -> our error message) with no CORS issue, since it's
all server-to-server now. Login itself hasn't been exercised with real
credentials, so catalog/streaming remain unverified - see
docs/architecture.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:36:26 +02:00
64710fa779 Add Forgejo Actions pipeline to build/publish the Docker image
All checks were successful
Publier l'image Docker / publish (push) Successful in 28s
Mirrors the working mediaDrop pipeline (.forgejo/workflows/publish-docker.yml
there) on the same instance: push to master -> latest + short-sha tags,
push a vX.Y.Z tag -> X.Y.Z/X.Y/latest. REGISTRY_TOKEN secret already
present on this repo. No test job yet - nothing to test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:59:55 +02:00
d621ce7503 Untrack .claude/ (local tooling config, not project source)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:53:31 +02:00
6ff9aa29e2 Pivot to a standalone app - Emby plugin hit a hard wall
No plugin API exists for adding a button to Emby's player or item
detail page (confirmed by an Emby developer on their own forum, and
by research since). The only way to reach that would be patching
Emby's shipped web client files directly - outside the plugin,
breaks on every image update, needs manual reapplication. Not worth
the ongoing cost, per discussion.

New direction: a standalone app with its own player. Reads Emby's
catalog/streams directly from the browser (no plugin, no menu
integration fights); a small Node backend only coordinates party
membership and relays playback state over WebSocket - it never
touches a media byte. Auth is Emby-only, as chosen: the browser signs
in against Emby directly and this backend validates whatever token it
gets handed by asking Emby itself (GET /Users/{userId}), never seeing
a password.

Control model changed too: now that we own the player, host controls
playback and everyone else follows (event-driven play/pause/seek +
a 3s heartbeat, drift-corrected on the follower side) - the old
plugin's "everyone keeps their own controls, shared reference
position" was specifically a concession to nudging pre-existing Emby
app sessions we didn't control, which no longer applies.

- server/: Express + ws backend (party lifecycle, WebSocket protocol,
  Emby token validation)
- client/: plain-JS/ES-modules frontend (login, search, item detail,
  party player) - no build step
- Smoke-tested locally end to end against a stubbed Emby URL: server
  starts, routing/auth-gating works, error handling degrades
  gracefully (network failure, invalid token) - see the session
  transcript for the actual browser test run
- docs/architecture.md rewritten around the new design, including why
  the plugin was abandoned and a fresh "assumptions to verify" list
  (CORS between this app's origin and Emby's is the biggest unknown)
- src/WatchParty/ (the Emby plugin) kept as-is for reference, no
  longer the active direction

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:48:55 +02:00
f25f719e5f Fix the real crash: partyPage.html was missing data-controller
Traced the exact "Cannot read properties of undefined (reading
lifeCycleAbortSignal)" error by fetching the target server's actual
viewmanager.js and pluginmanager.js directly (both are unauthenticated
static JS) instead of guessing further from docs.

Root cause: Emby's modern ViewManager (v4.10.0.40) only creates a
view.controller - required before it calls any lifecycle method on
the view - when the view's root element has a data-controller
attribute resolving to an AMD module via
pluginManager.getConfigurationResourceUrl(). partyPage.html never had
one (only the legacy data-role="page" attribute), so
newView.controller stayed undefined and getResumeOptions crashed
reading a property off it. configPage.html (the admin settings page)
apparently survives without this because dashboard pages go through a
different/older loading path.

Fix: added data-controller="__plugin/WatchPartyRoomController" to
partyPage.html, and a third PluginPageInfo/embedded resource
(partyPageController.js) - a minimal AMD module for it to resolve to.
ViewManager mixes in baseview.js's onResume/onPause/lifeCycleAbortSignal
automatically since this controller doesn't define its own, so it only
needs to exist.

Also checked (live JS probe, not a guess): pluginmanager.js has a
hardcoded plugin-id allowlist (allowPluginPages), but it's bypassed
whenever appHost.supports('restrictedplugins') is falsy - confirmed
falsy here, so it isn't blocking anything on this server's web client.

docs/architecture.md updated with the full trace for posterity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:17:14 +02:00
d4a365ae5c Drop EnableInMainMenu=true - broke Emby's modern ViewManager
Real error from the field: navigating to the party page threw inside
Emby's own viewmanager.js (v4.10.0.40): TypeError, cannot read
properties of undefined (reading lifeCycleAbortSignal), at
getResumeOptions/dispatchViewBeforeShow. EnableInMainMenu=true was
never confirmed by any real example (only EnableInUserMenu=true was,
via the forum thread cited in Plugin.cs) - it very likely routes the
page through the newer ViewManager meant for full content views
(Home, Movies, ...), which expects a lifecycle contract this static
embedded-resource page doesn't provide. Reverting to exactly the
confirmed-working combination.

The raw page itself was confirmed served correctly beforehand (200,
correct content, via GET /web/configurationpage?name=WatchPartyRoom)
so the plugin/registration side was never the problem - just this one
flag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:01:45 +02:00
6f22f72532 Integrate Watch Party into Emby's own UI instead of a standalone app
Per user feedback: no separate control panel, no chat. Found that
PluginPageInfo has EnableInUserMenu/EnableInMainMenu (confirmed via
reflection on the real MediaBrowser.Model assembly, plus an Emby
community thread showing another plugin using EnableInUserMenu=true)
- a real, if barely documented, way to surface a plugin page to
regular users, not just admins, through the exact same
IHasWebPages/GetPages() mechanism as the admin settings page.

- Plugin.cs now registers a second page (partyPage.html) with
  EnableInUserMenu=true + EnableInMainMenu=true
- partyPage.html replaces the old standalone control panel: no login
  form, it runs inside the already-authenticated Emby web client and
  calls the REST API via the global ApiClient
- Removed web/control-panel/ (the standalone app, its own login flow,
  and the custom GET /WatchParty/app* static file routes in
  WatchPartyService.cs) - no longer needed
- docs/architecture.md and README.md updated: the new mechanism, and
  the honest limitation that comes with it (reaches Web/Electron
  clients, not official mobile/TV apps - those allowlist plugin pages
  client-side)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 19:44:31 +02:00
db815df7d5 Fix plugin to build clean against real Emby 4.9.1.90 SDK
Installed .NET 8 SDK (per-user, winget needed admin elevation this
machine doesn't have) and restored the real MediaBrowser.Server.Core /
MediaBrowser.Common packages, then fixed everything the compiler and
a bit of reflection surfaced:

- PlaybackProgressEventArgs/PlaybackStopEventArgs are in
  MediaBrowser.Controller.Library, not ...Session
- IAuthorizationContext/AuthorizationInfo are in MediaBrowser.Controller.Net
- Item/user/session ids (SessionInfo.Id/UserId, BaseItemDto.Id) are plain
  string on this SDK version, not Guid - Party/PartyMember updated
  accordingly (Party.Id stays a Guid; it's minted by this plugin, not Emby)
- Reading the calling user needed an extra step:
  IAuthorizationContext.GetAuthorizationInfo(Request).UserId is a `long`
  (internal row id); .User.Id is the one matching SessionInfo.UserId,
  but it's a Guid so it's reformatted with .ToString("N")

docs/architecture.md's "Assumptions to verify" now reflects what was
actually confirmed vs. what's still only compile-checked (HTTP status
codes, content-type headers, the JS auth header format, the dashboard
config page) pending a run against a real server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 14:40:17 +02:00
66af05f259 Scaffold Emby Watch Party plugin
Design: coordinate parties on top of Emby's existing session
remote-control commands (SendPlaystateCommand) rather than building a
new player. Each member keeps their own Emby app/controls; the server
periodically corrects drift and mirrors pause/unpause against one
reference member's reported position.

- src/WatchParty: plugin skeleton (BasePlugin, IServerEntryPoint,
  IService/[Route] API, in-memory party store, drift-correction loop)
- web/control-panel: plain HTML/JS/CSS companion page to create/join
  parties and resync, served by the plugin itself
- docs/architecture.md: full design, sources consulted, and an
  explicit list of SDK details that still need verifying against a
  real build (no .NET SDK / Emby SDK assemblies available in this
  environment to compile against)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 14:02:43 +02:00