Add in-player SyncPlus overlay via served-index.html script injection
An IStartupFilter-registered middleware intercepts /web/index.html responses and injects a script tag for /SyncPlus/Stats/Client.js -- response-level injection rather than the write-to-disk approach other plugins use, since index.html is root-owned in the linuxserver image while Jellyfin runs unprivileged. The overlay floats its own fixed pill/panel (no dependency on jellyfin-web's minified OSD DOM), shows per-member drift for the caller's own group via the new non-admin /SyncPlus/Stats/Mine endpoint, and reuses the existing one-shot /SyncPlus/Stats/Sync action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -361,6 +361,48 @@ modes the reverted features did.
|
||||
real drifted second device (only one browser session available this
|
||||
round, same recurring constraint as everything else that needs two real
|
||||
logins).
|
||||
- **In-player overlay added 2026-07-09.** User-requested (correctly pushing
|
||||
back on an earlier "not possible without patching jellyfin-web" claim --
|
||||
plugins like InPlayerEpisodePreview prove the pattern exists): the stats
|
||||
panel + sync button now also appear *inside the video player page*, not
|
||||
just on the standalone stats page.
|
||||
- Mechanism: an `IStartupFilter` registered from plugin DI adds
|
||||
`SyncPlusIndexInjectionMiddleware` to the front of the pipeline, which
|
||||
intercepts responses for `/web/` and `/web/index.html` and injects a
|
||||
`<script src="/SyncPlus/Stats/Client.js" defer>` tag before `</body>`
|
||||
**in the served response only** -- deliberately NOT the
|
||||
write-to-index.html-on-disk approach other plugins default to, because
|
||||
in the linuxserver image (dev and the real k8s deployment alike)
|
||||
index.html is root-owned while Jellyfin runs unprivileged, so disk
|
||||
writes fail with permission errors there (the known issue
|
||||
InPlayerEpisodePreview's README warns about). Response interception has
|
||||
no permission problem and leaves nothing behind on uninstall. The
|
||||
middleware also strips `Accept-Encoding` on those requests (so
|
||||
downstream compression can't garble the `</body>` marker) and drops
|
||||
`ETag`/`Last-Modified` on modified responses (so browsers can't
|
||||
cache-revalidate back to an uninjected copy).
|
||||
- `GET /SyncPlus/Stats/Client.js` serves the overlay script
|
||||
(`[AllowAnonymous]` -- script tags fetch without auth; the script itself
|
||||
only calls auth-gated endpoints via `window.ApiClient.accessToken()`,
|
||||
available because the script runs inside the real SPA).
|
||||
- `GET /SyncPlus/Stats/Mine` -- new non-admin endpoint returning only the
|
||||
*caller's own group's* stats (regular viewers aren't admins, and the
|
||||
admin-gated all-groups endpoint would leak other users' sessions).
|
||||
Returns `{"Group": null}`-shaped response when not in a group (nulls
|
||||
are omitted by Jellyfin's JSON serializer, so the body is literally
|
||||
`{}` -- the overlay JS handles `undefined` fine).
|
||||
- Overlay design: deliberately does NOT hook into jellyfin-web's OSD
|
||||
button DOM (minified class names churn across releases) -- it floats
|
||||
its own fixed-position "SyncPlus" pill + panel, shown only while a
|
||||
`<video>` element exists on the page. Survives web client updates at
|
||||
the cost of not looking native.
|
||||
- **Server-side verified 2026-07-09**: script tag confirmed present in
|
||||
served `/web/index.html` and `/web/` (including with
|
||||
`Accept-Encoding: gzip, br` requests), `Client.js` serves as
|
||||
`application/javascript` and passes `node --check`, `Mine` returns 200
|
||||
with token / 401 without. **Browser-side behavior (overlay appearing
|
||||
during playback, panel polling, sync button) handed to the user to
|
||||
test** -- not yet verified as of this note.
|
||||
|
||||
## Open questions (revisit before/at the relevant phase, not now)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user