A Jellyfin server plugin that adds a custom UI layer to Jellyfin Web and the Mobile App and cross-client settings synchronization. Includes an optional Jellyseerr/Seerr integration with seamless authenticated proxy support.
- Custom Details Screen - Full-screen overlay with backdrop, logos, metadata, and a permission-aware context menu matching jellyfin-web's behavior
- Navigation Bar - Pill-shaped toolbar with Home, Search, Shuffle, Genres, Favorites, Library buttons, and user avatar
- Featured Media Bar - Hero slideshow with Ken Burns animation, content logos, and metadata overlay
- Jellyseerr/Seerr Panel - Embedded Jellyseerr or Seerr iframe with automatic session-based authentication via the server proxy
- Settings Panel - Per-user settings for all features, synced across clients
- TV Support - Spatial navigation and remote-friendly focus management for webOS/Tizen
- Settings Sync API - Per-user preference storage with merge/replace modes, synced across all Moonfin clients
- Jellyseerr/Seerr Proxy - Authenticated reverse proxy that creates browser sessions automatically, so the iframe loads without a separate login (supports both Jellyseerr and Seerr v3)
- Admin Configuration - Dashboard page for Jellyseerr/Seerr URL, display name, enable/disable toggles
- Web Injection - Serves the frontend JS/CSS as embedded resources, automatically injected via the File Transformation plugin
Disclaimer: Screenshots shown in this documentation feature media content, artwork, and actor likenesses for demonstration purposes only. None of the media, studios, actors, or other content depicted are affiliated with, sponsored by, or endorsing the Moonfin client or the Jellyfin project. All rights to the portrayed content belong to their respective copyright holders. These screenshots are used solely to demonstrate the functionality and interface of the application.
- Jellyfin Dashboard → Administration → Plugins → Repositories
- Add repository:
- Name:
Moonfin - URL:
https://raw.githubusercontent.com/Moonfin-Client/Plugin/refs/heads/master/manifest.json
- Name:
- Go to Catalog → find Moonfin → Install
- Restart Jellyfin
- Download the latest
Moonfin.Server-x.x.x.x.zipfrom Releases - Extract to your Jellyfin plugins folder:
Platform Path Linux /var/lib/jellyfin/plugins/Moonfin/Docker /config/plugins/Moonfin/Windows %ProgramData%\Jellyfin\Server\plugins\Moonfin\ - Restart Jellyfin
Moonfin uses the File Transformation plugin to automatically inject its web UI.
- Add the File Transformation plugin repository to Jellyfin:
- URL:
https://www.iamparadox.dev/jellyfin/plugins/manifest.json
- URL:
- Install the File Transformation plugin from the catalog
- Restart Jellyfin
- Force refresh your browser (Ctrl+Shift+R)
UI not loading? Go to Dashboard → Scheduled Tasks and run the Moonfin Startup task once, then refresh your browser.
Jellyfin Dashboard → Administration → Plugins → Moonfin to configure server-wide options like the Jellyseerr URL.
Once the web UI is loaded, click your user avatar in the top right to open the Settings panel and click Moonfin. From there you can customize the navbar, media bar, details screen, seasonal effects, ratings, and more. Settings are saved per-user and synced across all your Moonfin clients.
If you run Jellyfin behind a reverse proxy (e.g., Nginx, Caddy, Traefik), make sure your proxy is configured to forward all /Moonfin/ paths to Jellyfin. Jellyseerr loads inside Jellyfin through a special path (/Moonfin/Jellyseerr/Web/). If your reverse proxy isn't set up to pass those paths through, the page can't load and you'll just see a black screen. Some proxies also add security headers that block embedded content from showing up.
Seerr v3 is built on Next.js, which can have issues when proxied through subpaths due to hardcoded asset paths and hydration mismatches. If you're experiencing problems with Seerr v3 loading through the proxy (blank screen, 404 errors on chunks, navigation issues), you can configure a Direct Iframe URL in the admin settings:
- Go to Jellyfin Dashboard → Administration → Plugins → Moonfin
- Set the Jellyseerr URL to your Seerr instance (used for API proxying)
- Set the Direct Iframe URL to your public Seerr URL (e.g.,
https://seerr.yourdomain.com)
When the Direct Iframe URL is set, the iframe loads directly from that URL instead of through the Moonfin proxy. SSO API calls still go through the Jellyfin server, but the web UI comes directly from Seerr.
- .NET 8 SDK
- Node.js (LTS)
./build.sh.\build.ps1Both scripts accept optional parameters:
./build.sh [VERSION] [TARGET_ABI]
.\build.ps1 -Version "1.0.0.0" -TargetAbi "10.10.0"
The build will:
- Bundle the frontend JS and CSS
- Compile the .NET server plugin
- Package
Moonfin.Server.dllandmeta.jsoninto a ZIP - Update
manifest.jsonwith the new checksum
Output: Moonfin.Server-{VERSION}.zip in the repo root.
├── build.sh # Build script (Linux/macOS/Git Bash)
├── build.ps1 # Build script (Windows PowerShell)
├── backend/ # .NET 8 Jellyfin server plugin
│ ├── Api/ # REST controllers (settings, Jellyseerr proxy)
│ ├── Helpers/ # File Transformation patch callbacks
│ ├── Models/ # User settings, patch payload models
│ ├── Services/ # Startup task, settings persistence
│ ├── Pages/ # Admin config page HTML
│ └── Web/ # Embedded JS/CSS/HTML served to clients
└── frontend/ # Web UI plugin source
├── build.js # JS/CSS bundler
└── src/
├── plugin.js # Entry point
├── components/ # Details, Navbar, MediaBar, Jellyseerr, Settings
├── styles/ # Component CSS
└── utils/ # API helpers, storage, device detection, TV nav
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/Moonfin/Ping |
GET | Yes | Check plugin status and configuration |
/Moonfin/Settings |
GET | Yes | Get current user's settings |
/Moonfin/Settings |
POST | Yes | Save settings (merge or replace) |
/Moonfin/Settings |
HEAD | Yes | Check if user has saved settings |
/Moonfin/Settings |
DELETE | Yes | Delete user's settings |
/Moonfin/Jellyseerr/Config |
GET | Yes | Get Jellyseerr/Seerr configuration (auto-detects variant) |
/Moonfin/Jellyseerr/Login |
POST | Yes | Authenticate with Jellyseerr/Seerr via Jellyfin credentials |
/Moonfin/Jellyseerr/Status |
GET | Yes | Check current user's SSO session status |
/Moonfin/Jellyseerr/Logout |
DELETE | Yes | Clear SSO session |
/Moonfin/Jellyseerr/Api/* |
* | Session | Authenticated API proxy to Jellyseerr/Seerr |
/Moonfin/Jellyseerr/Web/* |
GET | Yes | Proxied web UI with injected session |
/Moonfin/Assets/{fileName} |
GET | Yes | Serve embedded rating icons |
/Moonfin/MDBList/Batch |
POST | Yes | Batch fetch ratings for multiple items |
/Moonfin/MDBList/{imdbId} |
GET | Yes | Get MDBList ratings for a single item |
/Moonfin/TMDB/Episode/{seriesId}/{seasonNumber}/{episodeNumber} |
GET | Yes | Get TMDB episode rating |
{
"enabled": true,
"url": "https://seerr.example.com",
"directUrl": null,
"displayName": "Seerr",
"variant": "seerr",
"userEnabled": true
}| Field | Type | Description |
|---|---|---|
enabled |
bool | Whether Jellyseerr/Seerr is enabled by admin |
url |
string | Server URL (used for API proxying) |
directUrl |
string? | Optional direct iframe URL for Seerr v3 subpath issues |
displayName |
string | UI display name (admin override or auto: "Jellyseerr"/"Seerr") |
variant |
string | Auto-detected: "jellyseerr" (version < 3.0) or "seerr" (version ≥ 3.0) |
userEnabled |
bool | Whether enabled in user's personal settings |
Direction: Bidirectional, local-wins
Note: Not all settings listed below have been integrated into every client yet. The server model defines the full set of syncable settings. Each client only reads and writes the ones it currently supports. Unsupported fields are preserved on the server and ignored by clients that don't use them.
Settings stored on the server per-user and shared across all Moonfin clients.
| Setting | Type | Description |
|---|---|---|
navbarEnabled |
bool | Enable custom navbar |
navbarPosition |
string | Navbar position (top, left) |
showClock |
bool | Show clock in navbar |
use24HourClock |
bool | Use 24-hour time format |
showShuffleButton |
bool | Show shuffle button in toolbar |
showGenresButton |
bool | Show genres button in toolbar |
showFavoritesButton |
bool | Show favorites button in toolbar |
showCastButton |
bool | Show cast/remote playback button |
showSyncPlayButton |
bool | Show SyncPlay button |
showLibrariesInToolbar |
bool | Show library buttons in toolbar |
shuffleContentType |
string | Shuffle content type (movies, tv, both) |
mediaBarEnabled |
bool | Enable featured media bar |
mediaBarContentType |
string | Media bar content type (movies, tv, both) |
mediaBarItemCount |
int | Number of items in media bar |
mediaBarOpacity |
int | Media bar overlay opacity (0–100) |
mediaBarOverlayColor |
string | Media bar overlay color key |
seasonalSurprise |
string | Seasonal particle effect (none, winter, spring, summer, fall, halloween) |
mdblistEnabled |
bool | Enable MDBList ratings |
mdblistApiKey |
string | MDBList API key |
mdblistRatingSources |
list | Which rating sources to display |
mergeContinueWatchingNextUp |
bool | Merge Continue Watching and Next Up rows |
enableMultiServerLibraries |
bool | Enable multi-server library aggregation |
homeRowsImageTypeOverride |
bool | Override home rows image type |
homeRowsImageType |
string | Home rows image type (poster, thumb, banner) |
detailsScreenBlur |
string | Blur intensity for details background |
browsingBlur |
string | Blur intensity for browsing backgrounds |
themeMusicEnabled |
bool | Enable theme music playback |
themeMusicOnHomeRows |
bool | Play theme music on home rows |
themeMusicVolume |
int | Theme music volume (0–100) |
blockedRatings |
list | Content ratings to block |
jellyseerrEnabled |
bool | Enable Jellyseerr integration |
jellyseerrApiKey |
string | Jellyseerr API key |
jellyseerrRows |
object | Jellyseerr discovery row configuration |
tmdbApiKey |
string | TMDB API key for episode ratings |
tmdbEpisodeRatingsEnabled |
bool | Enable TMDB episode ratings |
These settings are stored in localStorage only and do not sync across clients:
| Setting | Description |
|---|---|
detailsPageEnabled |
Enable custom details screen |
mediaBarAutoAdvance |
Auto-advance media bar slides |
mediaBarIntervalMs |
Auto-advance interval in milliseconds |
backdropEnabled |
Enable backdrop images |
- Pings
GET /Moonfin/Pingto check if the server plugin is installed and sync is enabled - Fetches server settings via
GET /Moonfin/Settings - A snapshot of the last-synced settings is stored in localStorage as a common ancestor for three-way merges
- Sync scenarios:
- Both local & server exist (with snapshot): Three-way merge using the snapshot as the common ancestor. For each setting: changed locally only → keep local; changed on server only → accept server; both changed → local wins
- Both local & server exist (no snapshot): First sync on this client — local wins (
{ ...server, ...local }), then pushes the merged result to the server - Server only (fresh install/new browser): Restores server settings to localStorage. This is how settings carry over to a new client
- Local only (no server data yet): Pushes local settings to the server
- After merging, the result is saved as the new snapshot for the next sync
- Saves to localStorage immediately
- If server is available, also pushes to server via
POST /Moonfin/Settings
- When you open Jellyfin on a new device/browser with no local settings, it pulls from the server and your settings follow you
- If you change settings on Client A, they push to server. When Client B next loads (page refresh/login), it syncs but Client B's local settings win in the merge, so it won't overwrite unsaved local preferences
- Sync only runs once on initial page load, not continuously, so if two clients are open simultaneously, they won't live-sync between each other
- Three-way merge resolves most conflicts, but when both clients change the same setting, local wins. If you change different settings on two clients, the merge picks up both changes correctly
- No real-time push between clients (no WebSocket/polling)
- Sensitive data like
mdblistApiKeyis synced to the server (stored per-user)
We welcome contributions to Moonfin for Jellyfin Web!
- Check existing issues - See if your idea/bug is already reported
- Discuss major changes - Open an issue first for significant features
- Follow code style - Match the existing codebase conventions
- Test across clients - Verify changes work on desktop browsers and mobile
- Consider upstream - Features that benefit all users should go to Jellyfin first!
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes with clear commit messages
- Test thoroughly on desktop and mobile browsers
- Submit a pull request with a detailed description
- Issues - GitHub Issues for bugs and feature requests
- Discussions - GitHub Discussions for questions and ideas
- Upstream Jellyfin - jellyfin.org for server-related questions
Moonfin for Jellyfin Web is built upon the excellent work of:
- Jellyfin Project - The foundation and upstream codebase
- MakD - Original Jellyfin-Media-Bar concept that inspired our featured media bar
- Druidblack - Original MDBList Ratings plugin
- Moonfin Contributors - Everyone who has contributed to this project
This project is licensed under GPL-3.0. See the LICENSE file for details.
Moonfin for Jellyfin Web is an independent project and is not affiliated with the Jellyfin project.
← Back to main Moonfin project
