Tracearr SSE
August 8, 2026 · View on GitHub
Jellyfin and Emby plugins that add a Server-Sent Events (SSE) endpoint for real-time playback, session, library, scheduled task, and server stat events. One authenticated HTTP connection, events the moment they happen. Built for Tracearr. Works with anything that consumes SSE.

Why
Neither Jellyfin nor Emby lets an outside app subscribe to playback events over a persistent HTTP connection. The webhook plugins push to external URLs, so your client has to run a reachable HTTP server of its own to listen. On Emby, webhooks also require a Premiere subscription. Jellyfin's WebSocket withholds session data from API-key clients (jellyfin#13479). That leaves polling /Sessions, which finds a session only after the fact.
This plugin gives you a standard SSE endpoint on the server itself. Events fire when playback starts, stops, pauses, progresses, when sessions connect or disconnect, when items are added to or removed from a library, when scheduled tasks run, and with server CPU/RAM utilization every 6 seconds. Pause is a first-class event, library items arrive one event per item, and no subscription is involved.
The plugin docs cover installation, the full event reference, client examples in curl, JavaScript, and Python, troubleshooting, and an honest comparison with the webhook plugin, the WebSocket, and polling.
Install
Jellyfin
Requires Jellyfin 10.11 or newer.
- Open Jellyfin → Dashboard → Plugins → Repositories.
- Add a repository:
- Name:
Tracearr - URL:
https://raw.githubusercontent.com/Tracearr/Media-Server-SSE/main/manifest.json
- Name:
- Open the Catalog, find Tracearr SSE, install.
- Restart Jellyfin.
Manual install (if you don't want to add the repository): download Tracearr.Sse.Jellyfin_<version>.zip from Releases, extract Jellyfin.Plugin.Sse.dll and MediaServer.Sse.Core.dll into your Jellyfin data directory at plugins/Tracearr SSE/. Restart.
Emby
Requires Emby 4.9 or newer. No Emby Premiere needed. Emby has no equivalent of Jellyfin's user-pasteable plugin repository URL, so install is manual.
- Download
Tracearr.Sse.Emby_<version>.zipfrom Releases. - Extract
Emby.Plugin.Sse.dllinto Emby'sprogramdata/plugins/directory. - Restart Emby.
Updates: repeat the same steps with the new release zip.
Usage
Connect to the SSE endpoint with any client that can set custom headers.
Jellyfin:
curl -N -H 'Authorization: MediaBrowser Token="YOUR_API_KEY"' \
http://your-jellyfin:8096/api/sse/events
Emby:
curl -N -H 'X-Emby-Token: YOUR_API_KEY' \
http://your-emby:8096/emby/sse/events
The plugin answers immediately with a hello event carrying its version, so you know the connection reached it. Jellyfin also accepts the token as X-Emby-Token, X-MediaBrowser-Token, or an api_key query parameter. The query form is what makes browser EventSource work, at the cost of the token appearing in access logs.
Events
| Event | Fields | When |
|---|---|---|
hello | version, server | Sent once on connect; server is jellyfin or emby |
playing | sessionId, itemId, userId, state, positionTicks | Playback started |
progress | sessionId, itemId, userId, state, positionTicks | Playback position update |
paused | sessionId, itemId, userId, state, positionTicks | Playback paused |
stopped | sessionId, itemId, userId, state, positionTicks, playedToCompletion | Playback stopped |
session.start | sessionId, userId | Device session connected |
session.end | sessionId, userId | Device session disconnected |
library.item.added | itemId, itemType, parentId | Item finished being added to a library |
library.item.removed | itemId, itemType, parentId | Item removed from a library |
task.started | taskId, taskName, taskCategory | Scheduled task began running |
task.progress | taskId, taskName, progress | Task progress (throttled to 1% or 2s per task) |
task.completed | taskId, taskName, state | Task finished; state is the completion status |
server.stats | at, hostCpuUtilization, processCpuUtilization, hostMemoryUtilization, processMemoryUtilization | CPU/RAM sample every 6 seconds while a client is connected |
ping | (empty) | Keepalive every 30 seconds |
Wire format
event: hello
data: {"version":"0.4.0.0","server":"jellyfin"}
event: playing
data: {"sessionId":"abc123","itemId":"def456","userId":"user1","state":"playing","positionTicks":0}
event: stopped
data: {"sessionId":"abc123","itemId":"def456","userId":"user1","state":"stopped","positionTicks":50000000,"playedToCompletion":true}
event: library.item.added
data: {"itemId":"def456","itemType":"Movie","parentId":"lib789"}
event: task.progress
data: {"taskId":"7738148ffcd07979c7ceb148e06b3aed","taskName":"Scan Media Library","progress":42.5}
event: server.stats
data: {"at":1786151199,"hostCpuUtilization":3.257,"processCpuUtilization":0.622,"hostMemoryUtilization":30.042,"processMemoryUtilization":0.548}
event: ping
data: {}
sessionId is the device session ID (matches what Jellyfin/Emby return from /Sessions), not the per-playback PlaySessionId. Events broadcast to all connected clients; there's no per-connection filtering. Null fields are omitted.
Behavior notes
- Bounded channel per subscriber (capacity 512). A client that falls behind is disconnected so it can reconnect and resync via
/Sessions. server.statshost values read/procand are omitted on non-Linux hosts; inside a container/procreports the host machine. Sampling runs every 6 seconds but only broadcasts while at least one client is connected.- Progress events pass through at whatever rate the media server reports them (typically every 5–10 seconds). No server-side throttling.
- Theme music and local trailer playback events are filtered out.
- Library events fire once per changed item, no batching. Theme media and virtual/placeholder items (e.g. missing episodes) are filtered out, same as playback events.
parentIdis the item's immediate parent (a season for an episode, a library folder for a top-level series or movie), not necessarily the library root. - Emby also raises events for container folders (a new movie's directory arrives as
itemType: "Folder"alongside the movie), and deleting a directory fires a removed event for the folder only, not each child. Jellyfin reports each media item individually in both directions. Don't assume added/removed pairs match one-to-one on Emby; treat removals as a cue to re-query. - Library event ids match what each server's REST API reports: the 32-character GUID form on Jellyfin, the numeric internal id on Emby. Emby playback events keep the GUID
itemIdthey have always had.
Verifying releases
Each release has SHA-256 checksums and a build attestation.
# Plain checksum verification
sha256sum -c SHA256SUMS
# Build attestation (proves the zip came from this repo's CI)
gh attestation verify Tracearr.Sse.Jellyfin_0.1.0.zip --owner Tracearr
Build from source
Requires .NET 9 SDK.
git clone https://github.com/Tracearr/Media-Server-SSE.git
cd Media-Server-SSE
# Jellyfin
dotnet publish Jellyfin.Plugin.Sse --configuration Release --output bin/jellyfin
# Emby
dotnet publish Emby.Plugin.Sse --configuration Release --output bin/emby
Run tests:
dotnet test
Architecture
Three projects, two distribution shapes:
MediaServer.Sse.Core: platform-agnostic event model and broadcaster. UsesSystem.Threading.Channelsfor fan-out.Jellyfin.Plugin.Sse: fiveIEventConsumer<T>implementations for playback/session events, three hosted services (library events viaILibraryManager, scheduled task events, server stats sampling), and an ASP.NET Core controller for the SSE endpoint. Ships asJellyfin.Plugin.Sse.dll+MediaServer.Sse.Core.dll.Emby.Plugin.Sse: singleIServerEntryPointthat subscribes toISessionManagerandILibraryManagerevents + anIService+IAsyncStreamWriterendpoint. Ships as a singleEmby.Plugin.Sse.dll; Core sources are inlined at compile time because Emby's plugin loader only resolves a single DLL per plugin.