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.

curl connected to the Jellyfin SSE endpoint, streaming playback, session, library, and server stat events

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.

  1. Open Jellyfin → Dashboard → Plugins → Repositories.
  2. Add a repository:
    • Name: Tracearr
    • URL: https://raw.githubusercontent.com/Tracearr/Media-Server-SSE/main/manifest.json
  3. Open the Catalog, find Tracearr SSE, install.
  4. 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.

  1. Download Tracearr.Sse.Emby_<version>.zip from Releases.
  2. Extract Emby.Plugin.Sse.dll into Emby's programdata/plugins/ directory.
  3. 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

EventFieldsWhen
helloversion, serverSent once on connect; server is jellyfin or emby
playingsessionId, itemId, userId, state, positionTicksPlayback started
progresssessionId, itemId, userId, state, positionTicksPlayback position update
pausedsessionId, itemId, userId, state, positionTicksPlayback paused
stoppedsessionId, itemId, userId, state, positionTicks, playedToCompletionPlayback stopped
session.startsessionId, userIdDevice session connected
session.endsessionId, userIdDevice session disconnected
library.item.addeditemId, itemType, parentIdItem finished being added to a library
library.item.removeditemId, itemType, parentIdItem removed from a library
task.startedtaskId, taskName, taskCategoryScheduled task began running
task.progresstaskId, taskName, progressTask progress (throttled to 1% or 2s per task)
task.completedtaskId, taskName, stateTask finished; state is the completion status
server.statsat, hostCpuUtilization, processCpuUtilization, hostMemoryUtilization, processMemoryUtilizationCPU/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.stats host values read /proc and are omitted on non-Linux hosts; inside a container /proc reports 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. parentId is 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 itemId they 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. Uses System.Threading.Channels for fan-out.
  • Jellyfin.Plugin.Sse: five IEventConsumer<T> implementations for playback/session events, three hosted services (library events via ILibraryManager, scheduled task events, server stats sampling), and an ASP.NET Core controller for the SSE endpoint. Ships as Jellyfin.Plugin.Sse.dll + MediaServer.Sse.Core.dll.
  • Emby.Plugin.Sse: single IServerEntryPoint that subscribes to ISessionManager and ILibraryManager events + an IService + IAsyncStreamWriter endpoint. Ships as a single Emby.Plugin.Sse.dll; Core sources are inlined at compile time because Emby's plugin loader only resolves a single DLL per plugin.

License

GPL-3.0-or-later