From 6f22f7253295b10ff2fb9065fb264e5191fa690e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 13 Sep 2026 19:44:31 +0200 Subject: [PATCH] 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 --- README.md | 44 ++-- docs/architecture.md | 61 ++++-- src/WatchParty/Api/WatchPartyService.cs | 45 +--- src/WatchParty/Configuration/partyPage.html | 212 +++++++++++++++++++ src/WatchParty/Plugin.cs | 26 ++- src/WatchParty/WatchParty.csproj | 4 +- web/control-panel/app.css | 142 ------------- web/control-panel/app.js | 214 -------------------- web/control-panel/index.html | 43 ---- 9 files changed, 314 insertions(+), 477 deletions(-) create mode 100644 src/WatchParty/Configuration/partyPage.html delete mode 100644 web/control-panel/app.css delete mode 100644 web/control-panel/app.js delete mode 100644 web/control-panel/index.html diff --git a/README.md b/README.md index f065051..a49bc06 100644 --- a/README.md +++ b/README.md @@ -13,25 +13,30 @@ still need verification against your exact server build. ## How it's meant to work 1. You start playing a title in your normal Emby app. -2. You open the Watch Party control panel (a small web page the plugin - serves) and start a party. You get a 6-character join code. -3. Friends open the control panel, sign in with their own Emby account, start - playing the *same* title in their own Emby app, and enter the code to join. +2. In Emby's web client (or the Electron desktop app), open **Watch Party** + from the menu and start a party. You get a 6-character join code. No + separate login — it's a real page inside Emby, using your existing session. +3. Friends open the same **Watch Party** menu item, start playing the *same* + title in their own Emby app, and enter the code to join. 4. The server treats one member (the host, by default) as the reference position, and periodically nudges everyone else's session back in sync (seek correction, and mirroring pause/unpause) — see [docs/architecture.md](docs/architecture.md#sync-algorithm). -The control panel is a companion page, not a video player — it never touches -the actual video stream. +The Watch Party page is a companion panel, not a video player — it never +touches the actual video stream, playback stays in your normal Emby app. + +**Known limitation**: the menu item shows up on Web and Electron clients. +Official mobile/TV apps don't surface arbitrary plugin pages, so Watch Party +won't appear there — see [docs/architecture.md](docs/architecture.md#components). ## Status Builds clean (0 errors, 0 warnings) against the real `MediaBrowser.Server.Core` -/ `MediaBrowser.Common` 4.9.1.90 assemblies from NuGet. Not yet installed or -run against an actual Emby Server — a few things (auth header format, HTTP -status codes, the dashboard settings page rendering) are only compile-checked -so far, not runtime-verified. See +/ `MediaBrowser.Common` 4.9.1.90 assemblies from NuGet, and is installed on a +real Emby Server (Docker) — the plugin loads and its admin page shows up. +The actual feature (`partyPage.html`, reached via `EnableInUserMenu`) hasn't +been exercised live yet. See [docs/architecture.md](docs/architecture.md#assumptions-to-verify) for exactly what was wrong on the first guess, what was fixed, and what's still open. @@ -49,12 +54,13 @@ open. ``` src/WatchParty/ the plugin itself (C#, netstandard2.0) - Plugin.cs plugin registration + admin settings page - Configuration/ plugin settings + the dashboard config page HTML + Plugin.cs plugin registration + both plugin pages + Configuration/ plugin settings, the admin settings page, and + partyPage.html - the actual user-facing feature, + integrated into Emby's own menu (not a separate app) Parties/ Party / PartyMember model + in-memory PartyManager Sync/ the drift-correction background loop - Api/ REST endpoints + serving the control panel's static files -web/control-panel/ the user-facing page (plain HTML/JS/CSS, no build step) + Api/ REST endpoints, called from partyPage.html docs/architecture.md full design doc ``` @@ -78,11 +84,15 @@ portable install vs. Docker volume). - [x] Design doc + scaffold - [x] Build against the real SDK, fix whatever the `TODO(verify)` spots got wrong -- [ ] Try it against a real Emby Server + two clients -- [ ] Live (WebSocket) updates in the control panel instead of polling -- [ ] Optional text chat, reusing the same realtime channel +- [x] Install on a real Emby Server (Docker) +- [x] Integrate into Emby's own UI instead of a standalone web app + (`EnableInUserMenu` — see [docs/architecture.md](docs/architecture.md#components)) +- [ ] Try a full party end-to-end with two accounts +- [ ] Live (WebSocket) updates in the party page instead of polling - [ ] Per-party override of sync settings instead of only server-wide defaults +No chat/reactions — deliberately out of scope, not just deferred. + ## License MIT — see [LICENSE](LICENSE). diff --git a/docs/architecture.md b/docs/architecture.md index 6238de6..8f057f0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -119,8 +119,12 @@ at the same time. Documented here rather than hidden. ``` src/WatchParty/ - Plugin.cs BasePlugin + admin settings page - Configuration/ PluginConfiguration (defaults) + configPage.html (dashboard UI) + Plugin.cs BasePlugin + both plugin pages (see GetPages()) + Configuration/ + PluginConfiguration.cs defaults (drift threshold, cooldown, etc.) + configPage.html admin settings page (Dashboard > Plugins > Watch Party) + partyPage.html the actual feature: create/join, member list, resync/leave - + EnableInUserMenu=true, so regular users see it too Parties/ Party.cs party id, join code, item id, host/reference user, members PartyMember.cs per-member session id + last known position/pause + EstimatedPositionTicks @@ -128,25 +132,45 @@ src/WatchParty/ Sync/ PartySyncEntryPoint.cs IServerEntryPoint: session event wiring + the tick loop above Api/ - WatchPartyService.cs REST endpoints (create/join/leave/get/resync) + - serves web/control-panel's static files -web/control-panel/ plain HTML/JS/CSS control panel (sign in, create/join, - member list with a sync-status dot, resync/leave) + WatchPartyService.cs REST endpoints (create/join/leave/get/resync), called from + partyPage.html via the Emby web client's own ApiClient ``` Parties are **in-memory only** — a server restart clears them. That's fine for v1: a party is meant to live for one watch session, not persist for weeks. -The control panel is served *by the plugin itself* (`GET /WatchParty/app`, -`/app.js`, `/app.css`, reading embedded resources) rather than injected into -Emby's main web client. Emby doesn't document a stable way to inject a -user-facing (non-admin) page/script into the main web client, and hand-patching -the client's install directory is a well-known but fragile hack that breaks -on every Emby update — not worth it for v1. The tradeoff: opening the control -panel is a separate step from opening Emby, rather than a button inside the -Emby UI. It authenticates with your normal Emby username/password (via Emby's -standard `/Users/AuthenticateByName`) and only ever talks to your own server. +The party UI is a real Emby plugin page (`Configuration/partyPage.html`, +registered in `Plugin.cs` via `IHasWebPages.GetPages()`), not a separate +web app. `PluginPageInfo` turns out to support exactly this: setting +`EnableInUserMenu = true` (`Plugin.cs`) surfaces the page to regular users, not +just admins — the same mechanism as the admin settings page +(`configPage.html`), just with different visibility flags. This was not +obvious from the official docs (which only cover the admin/dashboard case) — +confirmed instead via reflection on the real `PluginPageInfo` type (it has +`EnableInMainMenu`, `EnableInUserMenu`, `MenuSection`, `MenuIcon`, +`IsMainConfigPage`) and an Emby community thread showing another plugin using +`EnableInUserMenu = true` in practice. + +Two consequences of doing it this way instead of a standalone page: +- No separate login: the page runs inside the already-authenticated Emby web + client and just calls the global `ApiClient`, the same as the admin + settings page already did. There's no server URL / username / password + form, no token stored anywhere by this plugin. +- **Known limitation**: this reaches the **Web browser and Electron desktop + client only**. Official mobile/TV apps allowlist which plugin pages they + render client-side (independent of what the server exposes), so the page + won't show up there without changes on the client side that aren't + realistic to make from this plugin. Playback sync itself (the + `SendPlaystateCommand` nudges) still works on any client regardless — it's + only the *page to create/join a party* that's Web/Electron-only. + +An earlier version of this plugin shipped a fully standalone web app +(`web/control-panel/`, its own login form, served via custom +`GET /WatchParty/app` routes) to sidestep exactly this uncertainty. It was +removed once `EnableInUserMenu` was confirmed to work — see git history if +you ever need the standalone version back (e.g. to reach a client where the +integrated page doesn't show up). ## Assumptions to verify @@ -214,6 +238,13 @@ can't check): - The dashboard settings page (`configPage.html`) actually rendering/saving correctly — embedding compiles, but the page's own JS (`ApiClient`, `Dashboard` globals) only exists inside a running Emby dashboard. +- **`partyPage.html`**: whether `ApiClient.ajax({...})` really behaves like + the jQuery-style options object it's called with here (`type`, `url`, + `dataType`) — that's the long-standing Emby/Jellyfin web client convention, + not confirmed against this specific build. Whether `EnableInMainMenu`/ + `MenuIcon = "group"` place the page where you'd expect (main nav vs. a + user submenu) is also cosmetic-only until seen live — easy to adjust once + visible. The remaining way to close these out: install the plugin DLL on a real Emby Server 4.9.x and exercise it with two accounts. diff --git a/src/WatchParty/Api/WatchPartyService.cs b/src/WatchParty/Api/WatchPartyService.cs index ec6bf64..2df6438 100644 --- a/src/WatchParty/Api/WatchPartyService.cs +++ b/src/WatchParty/Api/WatchPartyService.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Threading; using MediaBrowser.Controller.Net; @@ -28,8 +27,10 @@ namespace WatchParty.Api // - Exact HTTP-error exception types (using plain ArgumentException / // InvalidOperationException below as a placeholder - they compile fine, // but Emby likely has something that maps more precisely to a 404/400). - // - Content-Type on the static file responses (returning bare strings for - // now; IHasResultFactory looks like the right extension point later). + // + // The UI used to be served from here too (GET /WatchParty/app + friends, + // reading embedded static files) before the feature moved to a proper + // Emby-integrated page - see Configuration/partyPage.html and Plugin.cs. // --------------------------------------------------------------------------- [Route("/WatchParty/Parties", "POST")] @@ -61,21 +62,6 @@ namespace WatchParty.Api public Guid PartyId { get; set; } } - [Route("/WatchParty/app", "GET")] - public class GetControlPanelHtml : IReturn - { - } - - [Route("/WatchParty/app.js", "GET")] - public class GetControlPanelJs : IReturn - { - } - - [Route("/WatchParty/app.css", "GET")] - public class GetControlPanelCss : IReturn - { - } - public class PartyStateDto { public Guid PartyId { get; set; } @@ -178,12 +164,6 @@ namespace WatchParty.Api }, CancellationToken.None); } - public object Get(GetControlPanelHtml request) => ReadEmbedded("index.html"); - - public object Get(GetControlPanelJs request) => ReadEmbedded("app.js"); - - public object Get(GetControlPanelCss request) => ReadEmbedded("app.css"); - private string CallerUserId() { // AuthorizationInfo.UserId is a `long` (an internal row id) on this SDK @@ -216,23 +196,6 @@ namespace WatchParty.Api return candidate; } - private static string ReadEmbedded(string logicalName) - { - var assembly = typeof(WatchPartyService).Assembly; - using (var stream = assembly.GetManifestResourceStream("WatchParty.web." + logicalName)) - { - if (stream == null) - { - return string.Empty; - } - - using (var reader = new StreamReader(stream)) - { - return reader.ReadToEnd(); - } - } - } - private static PartyStateDto ToDto(Party party) { var now = DateTime.UtcNow; diff --git a/src/WatchParty/Configuration/partyPage.html b/src/WatchParty/Configuration/partyPage.html new file mode 100644 index 0000000..ee68ba1 --- /dev/null +++ b/src/WatchParty/Configuration/partyPage.html @@ -0,0 +1,212 @@ +
+
+
+
+

Watch Party

+

+ Regarde le même titre en même temps que d'autres utilisateurs de ce serveur. + Lance la lecture dans Emby comme d'habitude - cette page sert juste à créer ou + rejoindre une party et à rester synchronisé. +

+
+ +
+ + +
+ + +
+ + +

+
+ +
+

Party

+

+ Partage le code ci-dessus. Chacun garde son appli Emby habituelle - cette page + sert juste à rester synchronisé. +

+
+ + +
+
+
+ + + + +
diff --git a/src/WatchParty/Plugin.cs b/src/WatchParty/Plugin.cs index 8ee5e06..6034cd4 100644 --- a/src/WatchParty/Plugin.cs +++ b/src/WatchParty/Plugin.cs @@ -38,13 +38,35 @@ namespace WatchParty "start or join a party, and the server nudges everyone's existing " + "playback back in sync using Emby's normal remote-control commands."; - /// Admin-only settings page (thresholds etc.) - see Configuration/configPage.html. public IEnumerable GetPages() { + // Admin-only settings page (thresholds etc.) - reached from + // Dashboard > Plugins > Watch Party > Settings. yield return new PluginPageInfo { Name = "WatchParty", - EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html" + DisplayName = "Watch Party", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html", + IsMainConfigPage = true + }; + + // The actual feature, reachable by regular users - not just admins. + // EnableInUserMenu/EnableInMainMenu is a real, if barely-documented, + // PluginPageInfo mechanism (confirmed via MediaBrowser.Model + // reflection and an Emby community thread showing a working plugin + // using this exact combination). Known limitation: this reaches + // Web/Electron clients, NOT the official mobile/TV apps - those + // allowlist which plugin pages they show client-side, independent + // of the server. See docs/architecture.md#assumptions-to-verify. + yield return new PluginPageInfo + { + Name = "WatchPartyRoom", + DisplayName = "Watch Party", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.partyPage.html", + EnableInMainMenu = true, + EnableInUserMenu = true, + MenuIcon = "group", + IsMainConfigPage = false }; } } diff --git a/src/WatchParty/WatchParty.csproj b/src/WatchParty/WatchParty.csproj index 969cd24..45d115c 100644 --- a/src/WatchParty/WatchParty.csproj +++ b/src/WatchParty/WatchParty.csproj @@ -19,9 +19,7 @@ - - - +