A Jellyfin server plugin that hides selected media libraries from clients — without blocking access for other plugins or the filesystem.
Jellyfin's built-in way to restrict a library is through user policies, which also blocks filesystem access. This breaks plugins like Cinema Mode that rely on direct library access.
GhostLibrary intercepts API responses before they reach the client and silently removes the configured libraries. Internally, everything keeps working as normal.
| Feature | Description |
|---|---|
| Hidden mode | Library tile and all its content are removed from every API response |
| Stealth mode | Library tile is kept but shown as empty (ChildCount = 0); content is hidden |
| Content filtering | Items from hidden/stealth libraries are also removed from Latest, Continue Watching, Next Up, and Similar rows |
| Admin bypass | Administrators always see all libraries (configurable) |
| Master switch | Pause all filtering without losing any settings |
| Schedule rules | Per-library rules combining date range, day of week, and daily time window — all conditions must match |
| Client filter | Restrict filtering to specific clients by User-Agent substring |
| Sub-folder hiding | Hide individual folders within an otherwise visible library |
| Activity log | Real-time log of every filter event — what was removed, when, and for which client |
| Configurable retention | Set how many log entries to keep in memory (10 – 10 000) |
| Webhook | HTTP POST notification on every config save — useful for Home Assistant |
| Export / Import | Download and restore the full configuration as JSON |
What it does NOT touch:
ILibraryManager— Cinema Mode and other server-side plugins still see every library- File system access — paths and permissions are unchanged
- User policies — no database modifications
- Open the Jellyfin dashboard
- Go to Plugins → Repositories
- Click + and add this URL:
https://raw.githubusercontent.com/upchui/Jellyfin-GhostLibrary/main/manifest.json - Go to Plugins → Catalog, find GhostLibrary and install it
- Restart Jellyfin
- Download the latest
GhostLibrary_x.x.x.x.zipfrom Releases - Extract
Jellyfin.Plugin.GhostLibrary.dllinto your Jellyfin plugins directory:# Linux ~/.config/jellyfin/plugins/GhostLibrary_x.x.x.x/ # Windows %AppData%\Jellyfin\plugins\GhostLibrary_x.x.x.x\ - Restart Jellyfin
- Go to Dashboard → Plugins → GhostLibrary
- Click a library card to cycle through its mode: Visible → Hidden → Stealth
- Click Save
- Visible — no filtering, shown normally to all clients
- Hidden — library tile and all its content are removed from every client response
- Stealth — tile stays but shows 0 items; content is filtered from aggregated rows
Per-library rules for when hiding is active. All conditions are ANDed — leave everything blank to hide at all times.
| Field | Description |
|---|---|
| Date Range | First and last calendar date the rule applies. Leave Until empty for permanent access. |
| Weekly Schedule | Which days of the week the rule is active. No days selected = every day. |
| Daily Time Window | Time-of-day window (e.g. 22:00 – 06:00). Overnight spans are supported. Leave blank = all day. |
Enter comma-separated User-Agent substrings (e.g. AndroidTV, Infuse). When set, only those clients are filtered — all others see every library.
Switch to the Activity Log tab to see a live, auto-refreshing table of filter events. Each row shows the timestamp, API path, client User-Agent, and how many items were removed.
Android TV / first use: After saving, clear the Jellyfin app cache once so the app discards its old cached library list.
GhostLibrary registers a global ASP.NET Core IAsyncActionFilter via PostConfigure<MvcOptions>. After every controller action it checks whether the response is a QueryResult<BaseItemDto>. If so:
- CollectionFolder items matching hidden IDs are removed; stealth IDs have their
ChildCountset to 0. - Content items (movies, episodes, etc.) are checked against their library via
ILibraryManager.GetCollectionFolders()— the authoritative API for mapping any item to its top-level library. - Schedule rules are evaluated against the current time of day before building the hidden ID set.
- The
ETagresponse header is replaced with a SHA-256 hash of the filtered result to prevent stale 304 responses. - Conditional GET headers (
If-None-Match,If-Modified-Since) are stripped from/Viewsrequests before execution to prevent 304 bypass.
Endpoints that must always pass through unfiltered (e.g. /Intros) are explicitly bypassed before any processing.
The plugin exposes two admin-only endpoints (requires RequiresElevation policy):
| Method | Path | Description |
|---|---|---|
GET |
/GhostLibrary/Log |
Returns the last N filter events (newest first) |
DELETE |
/GhostLibrary/Log |
Clears the in-memory log |
No local .NET SDK required — Docker handles the build:
git clone https://github.com/upchui/Jellyfin-GhostLibrary.git
cd Jellyfin-GhostLibrary
docker build -f Dockerfile.build --output type=local,dest=./dist .
# Built DLL lands in ./dist/| Jellyfin | Plugin | .NET |
|---|---|---|
| 10.9.x | 1.x | 8.0 |
MIT