Skip to content

Repository files navigation

projectionist-banner

██████╗ ██████╗  ██████╗      ██╗███████╗ ██████╗████████╗██╗ ██████╗ ███╗   ██╗██╗███████╗████████╗
██╔══██╗██╔══██╗██╔═══██╗     ██║██╔════╝██╔════╝╚══██╔══╝██║██╔═══██╗████╗  ██║██║██╔════╝╚══██╔══╝
██████╔╝██████╔╝██║   ██║     ██║█████╗  ██║        ██║   ██║██║   ██║██╔██╗ ██║██║███████╗   ██║
██╔═══╝ ██╔══██╗██║   ██║██   ██║██╔══╝  ██║        ██║   ██║██║   ██║██║╚██╗██║██║╚════██║   ██║
██║     ██║  ██║╚██████╔╝╚█████╔╝███████╗╚██████╗   ██║   ██║╚██████╔╝██║ ╚████║██║███████║   ██║
╚═╝     ╚═╝  ╚═╝ ╚═════╝  ╚════╝ ╚══════╝ ╚═════╝   ╚═╝   ╚═╝ ╚═════╝ ╚═╝  ╚═══╝╚═╝╚══════╝   ╚═╝

🎬 Projectionist for Jellyfin

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.


📑 Table of contents

  • Core features — source, triggers, smart selection, advanced rules, stats

  • Episode support

  • Installation

  • First-time setup

  • Sidecar metadata

  • Screenshots

  • How it works

  • Building from source

  • Compatibility

  • Support the project

  • License

  • 🗂️ 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.mp4 and a xmas.mp4 next 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.


🧩 Core features

🗂️ Source

  • Folder-based discovery — no library required.
  • Multiple preroll folders, each with default tags so library rules can target them.
  • Optional sidecar metadata: myfile.mp4.json next to each video, OR a folder-level prerolls.json. Set per-file tags, weight, rating, and schedule.
  • 6 video formats supported out of the box (.mp4 .mkv .mov .webm .avi .m4v), configurable.

🎬 Triggers

  • 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).

🎲 Selection — 5 strategies

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

🧠 Smart selection

  • 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)

🎯 Advanced

  • 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

📊 Stats

  • Per-file playback counts + last-played timestamps
  • Per-user playback counts
  • Persisted to <jellyfin>/data/plugins/configurations/Projectionist.stats.json so it survives restarts
  • Reset button

✅ Validation

  • 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.


📺 Episode support

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).


🛠️ Installation

ℹ️ Projectionist works standalone — FileTransformation is an optional fallback

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.html via 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.html or 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:

  1. Dashboard → Plugins → Repositories → +
  2. Paste: https://www.iamparadox.dev/jellyfin/plugins/manifest.json
  3. Save → Catalog → install File Transformation → Restart

Plugin repository (recommended)

One-click install + auto-updates. Add this URL to Dashboard → Plugins → Repositories:

https://raw.githubusercontent.com/ZL154/jellyfin-projectionist/main/manifest.json

Then:

  1. Dashboard → Plugins → Repositories → + (top-right)
  2. Repository Name: anything (e.g. Projectionist)
  3. Repository URL: paste the URL above
  4. Save
  5. Switch to the Catalog tab → find Projectionist under General → Install
  6. Restart Jellyfin when prompted
  7. Dashboard → Plugins → Projectionist → configure

Manual install

  1. Download projectionist_1.2.0.0.zip from the latest release.
  2. 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/
  3. Restart Jellyfin.
  4. Dashboard → Plugins → Projectionist → configure.

🚀 First-time setup

  1. Drop your preroll videos in any folder Jellyfin can read.
  2. Open Projectionist settings → set the Preroll folder path → click Save path.
  3. 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.
  4. Pick which content types should trigger a preroll (Movies, TV episodes, etc.).
  5. Save.
  6. 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.

📝 Sidecar metadata (tags, weights, schedules)

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.

Schedule shape

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)
};

📷 Screenshots

Source + Internal Library

Source and Library

Playback Rules

Triggers and selection

Smart Selection

Smart rules

Advanced — Folders, Library rules, Series, Trailer mode

Advanced

Skippable, Filters, Stats

Skippable + filters + stats

Validation

Configuration check


🔧 How it works

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:

  1. Checks the requested item's content type against the enabled-content-type config
  2. Checks per-user inclusion / exclusion
  3. Checks resume position (if enabled, skip if resuming)
  4. Checks session mode (FirstOfSession / OncePerDay / FirstOfBinge)
  5. Looks for a per-series preroll if the feature is an episode and EnableSeriesPrerolls is on
  6. Otherwise discovers candidates from all configured folders, applies schedule + maturity + tag + cooldown filters
  7. Picks N according to the selection mode
  8. (Optional) prepends N local trailers
  9. Resolves each pick via the hidden library to get a real BaseItem.Id (required for MediaSourceInfo lookup)
  10. Optionally warms the feature's Jellyfin playback info in the web client while prerolls are running
  11. 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.


🏗️ Building from source

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.json

Or use the bundled PowerShell build script (zips everything for upload):

./build/build.ps1
# Produces: build-output/Projectionist_<version>.zip

Targets Jellyfin 10.11 ABI (Jellyfin.Controller 10.11.0).


📋 Compatibility

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 ⚠️ untested 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.


🤔 Why FileTransformation? (you probably don't need it)

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.


❓ FAQ & Known limitations

Does this work on Android TV / Roku / iOS native apps?

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).

Does it slow down playback start?

Slightly — a few hundred milliseconds added by discovery + selection + media-source resolution. Enable Feature Preload (Warm or Hot) to mask most of that latency.

My preroll plays but the skip button never shows.

Fixed in v1.2.0. Update.

The "Projectionist Prerolls" library is showing in my home view.

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.

Does this interfere with Intro Skipper or similar?

No. They hook chapter detection; we hook the intro-list provider. Tested together in production.


❤ Support the project

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:

Not expected, just appreciated. Issue reports and clear bug reports are equally valuable.


📜 License

MIT — see LICENSE.

About

Jellyfin plugin: folder-based prerolls before movies AND TV episodes. Schedules, library rules, per-user rules, maturity gating, cooldowns, skippable, stats. Custom dark admin UI.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages