██████╗ ██████╗ ██████╗ ██╗███████╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗██╗███████╗████████╗
██╔══██╗██╔══██╗██╔═══██╗ ██║██╔════╝██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║██║██╔════╝╚══██╔══╝
██████╔╝██████╔╝██║ ██║ ██║█████╗ ██║ ██║ ██║██║ ██║██╔██╗ ██║██║███████╗ ██║
██╔═══╝ ██╔══██╗██║ ██║██ ██║██╔══╝ ██║ ██║ ██║██║ ██║██║╚██╗██║██║╚════██║ ██║
██║ ██║ ██║╚██████╔╝╚█████╔╝███████╗╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║██║███████║ ██║
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝╚══════╝ ╚═╝
A Jellyfin plugin that plays preroll videos before movies and TV episodes. Folder-based source — no Jellyfin library required. Custom-designed dark admin UI. Schedules, per-library rules, per-user rules, maturity gating, cooldowns, skippable prerolls, stats dashboard, and a lot more.
Built because every other Jellyfin preroll plugin either (a) requires you to create a Jellyfin library full of preroll files that then clutters your homepage, or (b) only works for movies. Projectionist solves both.
ℹ️ Projectionist injects its episode-preroll script via its own ASP.NET middleware, so it works standalone in most setups. The optional FileTransformation plugin is supported as a fallback in case a reverse proxy or an unusual deployment swallows the inline injection — install it only if episode prerolls don't fire after a hard refresh.
-
Core features — source, triggers, smart selection, advanced rules, stats
-
🗂️ Folder-based. Point at a folder and you're done. We create + manage the hidden Jellyfin library transparently and remove it from every user's home screen automatically.
-
📺 Episodes too. A bundled web-client patch is injected into Jellyfin's
index.html(via Projectionist's own ASP.NET middleware — no extra plugin required for standard installs) and hooks the playback manager so episodes also fetch intros, which the vanilla web client refuses to do. -
🧠 Smart selection. Cooldown so you don't see the same intro on every rewatch. Maturity gating so your edgy ident doesn't play before Bluey. Skip-on-resume. First-of-binge for TV. Per-user rules.
-
🎃 Seasonal / scheduled prerolls. Drop a
halloween.mp4and axmas.mp4next to your default — they auto-trigger on the right dates. -
📊 Stats. Per-file + per-user playback counters with a live dashboard.
-
🎨 Custom dark admin UI.
- Folder-based discovery — no library required.
- Multiple preroll folders, each with default tags so library rules can target them.
- Optional sidecar metadata:
myfile.mp4.jsonnext to each video, OR a folder-levelprerolls.json. Set per-file tags, weight, rating, and schedule. - 6 video formats supported out of the box (
.mp4 .mkv .mov .webm .avi .m4v), configurable.
- Movies, TV episodes, music videos — toggle each independently.
- TV episodes work despite Jellyfin's web client only fetching intros for movies natively (we ship a tiny web-client patch).
| Mode | Behaviour |
|---|---|
| Random | Uniform random pick (default) |
| Sequential | Round-robin across the pool |
| Weighted | Honour weight field from sidecar metadata |
| Equal Rotation | Strict per-rule fairness — every file plays equally often |
| Recency Boost | Newly-added files get 2× weight for their first week |
- Skip on resume — replaying from a partial position skips the intro
- Maturity gating — preroll's rating must not exceed the feature's. Stops your R-rated ident from running before a kids' movie
- Cooldown per item — don't replay the same preroll for the same movie within N hours (default 12)
- Per-user rules — pick "all users", "only these users" (kids profile only?), or "all except these" (admin gets nothing). Click chips to toggle
- Session modes — every playback / first of session / once per day / first of binge (don't replay during episodes 2+ of the same series)
- Multiple folders — add as many as you want; each has default tags
- Library rules — match by Jellyfin library name and/or item type, require/exclude tags, or disable per rule. First match wins
- Series-specific prerolls — drop a
theme-preroll.mp4(configurable name) in any series folder. Overrides global selection for that show - Trailer mode — chain N local trailers from the feature's own metadata before it plays. Cinema-style "and now our feature presentation"
- Skippable prerolls — skip-button overlay during preroll playback with a configurable min-seconds delay
- Feature preload — best-effort web-client warmup that prepares Jellyfin playback info for the movie or episode while prerolls are running
- Per-file playback counts + last-played timestamps
- Per-user playback counts
- Persisted to
<jellyfin>/data/plugins/configurations/Projectionist.stats.jsonso it survives restarts - Reset button
- One-click sanity check of your configuration. Tells you exactly what's missing and how to fix it ("hidden library not set up", "no preroll files found", "all content types disabled", etc.)
🙈 Hidden internal library
Jellyfin's player only streams files registered as library items. Projectionist creates a library called Projectionist Prerolls for your configured preroll folders, hides it from every user via per-user policy preferences (MyMediaExcludes, LatestItemExcludes, OrderedViews), and uses it internally for playback. You should never see it in the UI — and if you do, click "Set up library" again to re-apply the hide.
Jellyfin's web/desktop/TV clients hard-code intro fetching to Type === 'Movie' only. Episodes never call /Items/{id}/Intros, so any plugin that just implements IIntroProvider can't do anything for episode playback.
Projectionist works around this with a small JavaScript hook that monkey-patches playbackManager.play(). When the user plays an episode, the hook fetches /Items/{episodeId}/Intros itself and prepends the result to the play options. The script is injected into Jellyfin's index.html via two parallel paths — an in-process ASP.NET IStartupFilter middleware AND a registration with the FileTransformation plugin (whichever fires first wins).
Jellyfin's web client only natively fetches intros for movies. Projectionist ships a small JavaScript hook that adds the same behaviour for episodes, and injects it into Jellyfin's
index.htmlvia its own ASP.NET middleware (IStartupFilter). This works out of the box in standard Jellyfin installs.Install FileTransformation only as a fallback if episode prerolls don't fire after a hard refresh — usually a sign that a reverse proxy is caching
index.htmlor otherwise swallowing the inline injection. Projectionist autodetects FileTransformation at runtime and will use it as a second injection path if present.To install the optional fallback:
- Dashboard → Plugins → Repositories → +
- Paste:
https://www.iamparadox.dev/jellyfin/plugins/manifest.json- Save → Catalog → install File Transformation → Restart
One-click install + auto-updates. Add this URL to Dashboard → Plugins → Repositories:
https://raw.githubusercontent.com/ZL154/jellyfin-projectionist/main/manifest.json
Then:
- Dashboard → Plugins → Repositories → + (top-right)
- Repository Name: anything (e.g.
Projectionist) - Repository URL: paste the URL above
- Save
- Switch to the Catalog tab → find Projectionist under General → Install
- Restart Jellyfin when prompted
- Dashboard → Plugins → Projectionist → configure
- Download
projectionist_1.2.0.0.zipfrom the latest release. - Extract it into your Jellyfin plugins directory so the path looks like:
- Docker:
<config-volume>/plugins/Projectionist_1.2.0.0/ - Bare metal:
<jellyfin-data>/plugins/Projectionist_1.2.0.0/
- Docker:
- Restart Jellyfin.
- Dashboard → Plugins → Projectionist → configure.
- Drop your preroll videos in any folder Jellyfin can read.
- Open Projectionist settings → set the Preroll folder path → click Save path.
- Scroll to the Internal Library card → click Set up library. This creates the hidden Jellyfin library at your folder, scans it, and applies the hide to every user. Wait for the status pill to flip to Library OK.
- Pick which content types should trigger a preroll (Movies, TV episodes, etc.).
- Save.
- Play any movie — preroll plays. Play any episode — preroll plays. Open the dev console and you should see
[Projectionist] queueing N preroll(s) before episode <name>for episodes.
Drop myfile.mp4.json next to a video to override its discovery defaults:
{
"tags": ["halloween", "spooky"],
"weight": 2.0,
"rating": "PG-13",
"schedule": {
"months": [10],
"dateRange": "10-15..10-31"
}
}Or use a single folder-level prerolls.json:
{
"halloween.mp4": {
"tags": ["halloween"],
"schedule": { "months": [10] }
},
"xmas.mp4": {
"tags": ["xmas"],
"schedule": { "dateRange": "12-15..12-31" }
},
"anime-default.mp4": {
"tags": ["anime"],
"weight": 1.5
}
}Then add a library rule in the UI: Library Name = "Anime" → RequireTags = anime. Done — your anime library uses anime-tagged prerolls.
type ScheduleRule = {
months?: number[]; // 1-12
daysOfWeek?: number[]; // 0=Sunday .. 6=Saturday
startHour?: number; // 0-23 inclusive
endHour?: number; // 0-24 exclusive
dateRange?: string; // "MM-DD..MM-DD" (handles year-wrap)
};The plugin implements MediaBrowser.Controller.Library.IIntroProvider — Jellyfin's official "play this before that" hook. When playback starts, Jellyfin asks every registered IIntroProvider for items to play first.
For movies that's the whole story. The provider:
- Checks the requested item's content type against the enabled-content-type config
- Checks per-user inclusion / exclusion
- Checks resume position (if enabled, skip if resuming)
- Checks session mode (FirstOfSession / OncePerDay / FirstOfBinge)
- Looks for a per-series preroll if the feature is an episode and
EnableSeriesPrerollsis on - Otherwise discovers candidates from all configured folders, applies schedule + maturity + tag + cooldown filters
- Picks N according to the selection mode
- (Optional) prepends N local trailers
- Resolves each pick via the hidden library to get a real
BaseItem.Id(required forMediaSourceInfolookup) - Optionally warms the feature's Jellyfin playback info in the web client while prerolls are running
- Records cooldown, stats, session bookkeeping
For episodes, the bundled JavaScript hook calls the same endpoint client-side because Jellyfin's web/desktop/TV clients won't.
The hidden internal library exists because Jellyfin's player resolves MediaSourceInfo via BaseItem.Id — a path-only intro returns successfully but silently fails to stream. The library gives our prerolls real BaseItem IDs without the user having to manage a Jellyfin library themselves.
Requires .NET SDK 9.0.
git clone https://github.com/ZL154/jellyfin-projectionist
cd jellyfin-projectionist
dotnet build src/Projectionist/Projectionist.csproj -c Release
# DLL output: src/Projectionist/bin/Release/net9.0/Jellyfin.Plugin.Projectionist.dll
# meta.json: src/Projectionist/meta.jsonOr use the bundled PowerShell build script (zips everything for upload):
./build/build.ps1
# Produces: build-output/Projectionist_<version>.zipTargets Jellyfin 10.11 ABI (Jellyfin.Controller 10.11.0).
| Jellyfin | Status | Notes |
|---|---|---|
| 10.11.9 - 10.11.10 | ✅ supported (v1.1.1+) | Plugin includes an ABI shim for the removal of IUserManager.Users from the interface. |
| 10.11.0 - 10.11.8 | ✅ supported | Original target ABI. |
| 10.10.x | Needs ABI bump in csproj + meta.json. |
Zero hard dependencies. Both movie and episode prerolls work out of the box on standard Jellyfin installs (Projectionist injects its episode hook via its own ASP.NET middleware).
If episode prerolls don't fire on your setup — usually because a reverse proxy is caching index.html — install FileTransformation as a fallback. Add this manifest URL to Dashboard → Plugins → Repositories and install File Transformation from the catalog:
https://www.iamparadox.dev/jellyfin/plugins/manifest.json
Projectionist autodetects FileTransformation at runtime and uses it as a second injection path when present.
Projectionist ships its OWN in-process injection via an ASP.NET IStartupFilter middleware. This works out of the box in standard Jellyfin installs. The FileTransformation plugin is an OPTIONAL fallback — install it only if episode prerolls don't fire on your setup, usually a sign that a reverse proxy is caching index.html.
Movie prerolls — yes (server-side IIntroProvider). Episode prerolls — no. The episode preroll depends on a JavaScript hook injected into Jellyfin Web's index.html. Native clients don't load the web shell, so the JS hook never runs there. Episode prerolls work in Jellyfin Web (browser) and Jellyfin Media Player (which uses the bundled web client).
Slightly — a few hundred milliseconds added by discovery + selection + media-source resolution. Enable Feature Preload (Warm or Hot) to mask most of that latency.
Fixed in v1.2.0. Update.
Click "Set up library" in plugin settings to re-apply the per-user hide. Some Jellyfin upgrades reset user preferences; HideOnStartupService re-applies the hide ~8 seconds after plugin load. Hard-refresh the web client to see it.
No. They hook chapter detection; we hook the intro-list provider. Tested together in production.
Projectionist for Jellyfin is built and maintained in my spare time. If it's making your server feel more polished and you'd like to support ongoing development, any of these means a lot:
- ⭐ Star this repo — it's free and helps others find it
- 💖 Sponsor on GitHub — one-off or monthly, every dollar reaches the project
- ☕ Buy me a coffee on Ko-fi — one-off tips
Not expected, just appreciated. Issue reports and clear bug reports are equally valuable.
MIT — see LICENSE.






