A self-hosted, always-on TV guide built from your Emby library. Emby Guide turns movies, series, anime, stand-up, and theatre into deterministic channels that every viewer sees at the same time.
Browse what is on, save an upcoming programme for 24-hour catch-up, open it in the Emby web player, or send it to an active Emby device on your network.
There are two ways to run Emby Guide:
- Emby server plugin (recommended) — one DLL inside Emby itself. No extra services, no API keys to manage, and the guide reacts to library changes within a minute. The web app is served by the plugin.
- Standalone server — the original Node.js server plus web client, run via Docker Compose next to Emby. Still supported for setups where installing a plugin is not an option.
Requirements: Emby Server 4.9.x (built and tested against 4.9.1.90 / 4.9.3).
-
Get
EmbyGuide.Plugin.dll— from a release, or build it yourself:# Node 24+ and a .NET 8+ SDK required npm install emby-plugin/tools/build-web.sh # builds the web app into the plugin dotnet build -c Release emby-plugin/src/EmbyGuide.Plugin # → emby-plugin/src/EmbyGuide.Plugin/bin/Release/netstandard2.0/EmbyGuide.Plugin.dll
-
Copy the single DLL into Emby's
pluginsfolder and restart Emby Server. -
Open Dashboard → Plugins → Emby Guide and set the schedule time zone (IANA id, e.g.
Europe/London), then tick the libraries feeding each channel — a channel can merge several libraries (e.g.Movies+Animation Movies). The defaults areMovies,TV Shows,Anime,Stand Up,Theatre; a channel with no matching library stays empty. -
Open the guide at:
http://<your-emby-host>:8096/emby/EmbyGuide/web/
Viewers sign in with their own Emby profiles — passwordless profiles work with
one click. Playback, casting, saved programmes, per-user channel order, and
admin channel editing all work exactly as in the standalone version. Guide
data (curated channels, icons, preferences, saved items) lives in JSON files
under <emby data>/data/emby-guide/.
The plugin also registers itself as a Live TV source, so the channels and schedule appear in Emby's native Live TV guide — in every Emby app, with no extra software. Tuning a channel plays whatever the schedule says is on right now (from the programme's beginning; the mid-programme live join is a guide-app feature). This surface is read-only by design: browse and watch, while channel management stays in the guide web app.
Setup is automatic: the tuner registers on startup and Emby imports the EPG
on its normal Refresh Guide schedule (run that task once from the dashboard
for an immediate import; guide depth follows Emby's Live TV → guide-days
setting). Disable the integration with the plugin's LiveTvEnabled setting.
Emby requires an access token on every plugin route — it has no way to mark plugin endpoints anonymous. Signed-in users are covered automatically, but four read-only paths must work before sign-in (the web app itself, the profile picker, app config, and channel icons):
/emby/EmbyGuide/web*
/emby/EmbyGuide/Users
/emby/EmbyGuide/Config
/emby/EmbyGuide/Channels/*/Icon
On a trusted LAN, Emby's local-network allowance may cover these. For remote
access, create a dedicated API key in Emby (Settings → Advanced → API
Keys) and have whatever fronts Emby inject it as an X-Emby-Token request
header for exactly those paths:
- Cloudflare Tunnel: add a Modify Request Header transform rule setting
X-Emby-Tokenfor the four path patterns, and optionally a redirect rule from/to/emby/EmbyGuide/web/. - nginx / other reverse proxies: see docker/nginx.conf.template for a working example of per-path header injection.
The key never reaches browsers, and the injected paths only expose the household profile list, static files, and channel icons — the same surface the standalone server always served without login.
Curated channels, channel icons, per-user channel order, and the pinned channel migrate in one step from the standalone server's SQLite database:
node emby-plugin/tools/migrate-sqlite.mjs <path-to>/guide.db "<emby data>/data/emby-guide"Restart Emby (or run the Rebuild guide snapshot scheduled task) afterwards. Channel ids are preserved, so schedules stay identical to the day they were migrated — the plugin's engine is a bit-exact port of the Node scheduler (see emby-plugin/README.md for the parity test harness).
You need Docker Engine with the Compose plugin and an existing Emby server. From this repository, run:
docker compose up -d --buildOpen http://localhost:8687. On the first-run screen, enter:
- The Emby address reachable from the server container. With Docker
Desktop,
http://host.docker.internal:8096is commonly suitable. On Linux, use the host's LAN address, such ashttp://192.168.1.20:8096. - An administrator API key created in Emby under Settings → Advanced → API Keys → New API Key.
The app verifies the connection, encrypts the API key in its SQLite database, and never returns it to a browser. Other viewers go straight to the Emby profile picker and sign in with their own profile.
To use another host port or timezone:
GUIDE_PORT=8080 SCHEDULE_TZ=Europe/London docker compose up -d --buildSCHEDULE_TZ accepts an IANA timezone and controls day-based scheduling. The
default is Etc/UTC. Use the same timezone in both deployments if you ever
switch between them — the schedule is derived from it.
git pull
docker compose up -d --buildStop the app with docker compose down. That command preserves the named data
and master-key volumes. Do not use docker compose down --volumes unless you
intend to erase the installation.
Back up both named volumes:
emby-guide_emby-guide-datacontains the SQLite database.emby-guide_emby-guide-secretscontains the separate encryption master key.
You need both to restore the saved Emby connection. Never publish either one.
The browser setup is the recommended path. The Compose file also accepts these
optional values from your shell or a local .env file:
| Variable | Default | Purpose |
|---|---|---|
GUIDE_PORT |
8687 |
Port exposed by the web client |
SCHEDULE_TZ |
Etc/UTC |
IANA timezone for day-based scheduling |
CACHE_TTL_MINUTES |
360 |
Emby library refresh interval |
SAVED_MAX_PER_USER |
5 |
Maximum saved programmes per user |
LIB_MOVIES |
Movies |
Emby movie-library name(s), comma-separated |
LIB_TV |
TV Shows |
Emby TV-library name(s), comma-separated |
LIB_ANIME |
Anime |
Emby anime-library name(s), comma-separated |
LIB_STANDUP |
Stand Up |
Emby stand-up-library name(s), comma-separated |
LIB_THEATRE |
Theatre |
Emby theatre-library name(s), comma-separated |
PUBLIC_EMBY_URL |
Emby setup URL | Browser-reachable URL for web-player links |
Set PUBLIC_EMBY_URL when the server container reaches Emby through an
internal address that browsers cannot use. See .env.example
for the complete application configuration and unattended-install options.
Keep API keys out of .env; use EMBY_API_KEY_FILE when automating setup.
You need Node.js 24 and npm.
npm install
npm run build
npm startOpen http://localhost:8687 and complete the same first-run setup. Local data
is stored under data/ by default. Development mode is npm run dev; the Vite
client runs on port 5173 and proxies /api to the backend on port 8687 and
/emby to Emby (EMBY_URL, default http://localhost:8096).
- Channels are pure functions of the library: each uses a seeded shuffle and real media runtimes, so every schedule is deterministic without storing a single airing — every viewer and device computes the same lineup.
- The plugin builds its library snapshot in-process and refreshes it when the
library changes; the standalone server snapshots over the Emby HTTP API on
an interval. Both run the same scheduling engine — the C# port in
emby-plugin/is verified bit-identical to the Node original by golden parity tests. - The Svelte client presents a windowed, two-axis guide with detail, playback, casting, profile selection, and saved-programme views. It talks to either backend; against the plugin it authenticates with Emby tokens directly.
Emby Guide is designed for a trusted household network. The profile picker is visible to anyone who can reach the app, and the app can control active Emby devices. Before exposing it to the internet, put it behind HTTPS and read SECURITY.md. In the plugin deployment, exposing the guide means exposing your Emby server's own API surface too — the guide adds no additional authentication layer beyond Emby's.
npm run dev— run the backend and Vite development server.npm run build— build the client and server.npm run check -w client— type-check the Svelte client.npm test— run the backend test suite.emby-plugin/tools/build-web.sh && dotnet build emby-plugin— build the plugin.dotnet test emby-plugin— run the C# suite, including golden parity tests.