A Jellyfin server plugin that enables cross-client settings synchronization and Jellyseerr integration for Moonfin clients.
- Open Jellyfin Dashboard → Administration → Plugins → Repositories
- Click "Add" and enter:
- Name: Moonfin
- URL:
https://raw.githubusercontent.com/RadicalMuffinMan/moonfin-server/master/manifest.json
- Go to the Catalog tab and find "Moonfin Settings Sync"
- Click Install and restart Jellyfin
- Download the latest release ZIP from the Releases page
- Extract the ZIP to your Jellyfin plugins folder:
- Linux:
/var/lib/jellyfin/plugins/Moonfin/ - Docker:
/config/plugins/Moonfin/ - Windows:
%ProgramData%\Jellyfin\Server\plugins\Moonfin\
- Linux:
- Restart Jellyfin
# Clone the repository
git clone https://github.com/RadicalMuffinMan/moonfin-server.git
cd moonfin-server
# Build with .NET 8 SDK
dotnet build -c Release
# Copy Moonfin.Server.dll to your plugins folder
cp bin/Release/net8.0/Moonfin.Server.dll /var/lib/jellyfin/plugins/Moonfin/
# Restart Jellyfin
sudo systemctl restart jellyfin- Settings Sync: Store user preferences on the server so they sync across all Moonfin clients (Android TV, Roku, Tizen, webOS, and Web)
- Per-User Storage: Each user's settings are stored separately
- Merge Mode: Settings can be merged (only update non-null values) or replaced entirely
- Flexible Schema: Uses nullable fields to support different client features
- Jellyseerr Integration: Admin-configurable Jellyseerr server URL with user-controlled API keys
Configure in Jellyfin Dashboard → Plugins → Moonfin:
| Setting | Default | Description |
|---|---|---|
EnableSettingsSync |
true |
Enable cross-client settings synchronization |
JellyseerrEnabled |
false |
Enable Jellyseerr integration in Moonfin clients |
JellyseerrUrl |
null |
Jellyseerr server URL (e.g., https://jellyseerr.example.com) |
Check if the Moonfin plugin is installed. Can be called without authentication.
Response:
{
"installed": true,
"version": "1.0.0.0",
"settingsSyncEnabled": true,
"serverName": "Jellyfin",
"jellyseerrEnabled": true,
"jellyseerrUrl": "https://jellyseerr.example.com"
}Get Jellyseerr configuration (requires authentication).
Response:
{
"enabled": true,
"url": "https://jellyseerr.example.com",
"userEnabled": true
}Get settings for the currently authenticated user.
Headers: Authorization: MediaBrowser Token="..."
Response: MoonfinUserSettings object or 404 if not found.
Save settings for the currently authenticated user.
Headers: Authorization: MediaBrowser Token="..."
Request Body:
{
"settings": {
"mediaBarEnabled": true,
"themeMusicEnabled": false,
"navbarPosition": "bottom"
},
"clientId": "moonfin-androidtv",
"mergeMode": "merge"
}Merge Modes:
merge: Only update non-null fields in the incoming settings (default)replace: Replace all settings with the incoming values
Response:
{
"success": true,
"created": false,
"userId": "..."
}Check if settings exist for the current user (no body returned).
Delete settings for the current user.
The MoonfinUserSettings model stores user preferences synced across Moonfin clients.
Based on FEATURES.md.
| Setting | Type | Description |
|---|---|---|
jellyseerrEnabled |
bool |
Enable Jellyseerr for this user |
jellyseerrApiKey |
string |
User's Jellyseerr API key |
jellyseerrRows |
object |
Jellyseerr discovery rows config |
mdblistEnabled |
bool |
Enable MDBList ratings |
mdblistApiKey |
string |
User's MDBList API key |
tmdbApiKey |
string |
User's TMDB API key |
| Setting | Type | Description |
|---|---|---|
navbarPosition |
string |
Position: top, bottom, left, right |
showShuffleButton |
bool |
Show shuffle button |
showGenresButton |
bool |
Show genres button |
showFavoritesButton |
bool |
Show favorites button |
showLibrariesInToolbar |
bool |
Show library buttons |
shuffleContentType |
string |
Shuffle content type |
| Setting | Type | Description |
|---|---|---|
mergeContinueWatchingNextUp |
bool |
Merge Continue Watching and Next Up |
enableMultiServerLibraries |
bool |
Multi-server library aggregation |
enableFolderView |
bool |
Folder-based library view |
confirmExit |
bool |
Show confirm exit dialog |
| Setting | Type | Description |
|---|---|---|
mediaBarEnabled |
bool |
Enable media bar on home |
mediaBarContentType |
string |
Content type filter |
mediaBarItemCount |
int |
Number of items |
mediaBarOpacity |
int |
Background opacity (0-100) |
mediaBarOverlayColor |
string |
Hex overlay color |
| Setting | Type | Description |
|---|---|---|
homeRowsImageTypeOverride |
bool |
Override default image type |
homeRowsImageType |
string |
Image type: poster, thumb, banner |
detailsScreenBlur |
string |
Details screen blur intensity |
browsingBlur |
string |
Browsing screen blur intensity |
| Setting | Type | Description |
|---|---|---|
themeMusicEnabled |
bool |
Enable theme music |
themeMusicOnHomeRows |
bool |
Play on home rows |
themeMusicVolume |
int |
Volume (0-100) |
| Setting | Type | Description |
|---|---|---|
blockedRatings |
List<string> |
Content ratings to block |
| Setting | Type | Description |
|---|---|---|
clientSpecific |
Dictionary<string, string> |
Platform-specific settings blob |
| Setting | Type | Description |
|---|---|---|
trendingMovies |
bool |
Show trending movies row |
trendingTv |
bool |
Show trending TV row |
popularMovies |
bool |
Show popular movies row |
popularTv |
bool |
Show popular TV row |
upcomingMovies |
bool |
Show upcoming movies row |
upcomingTv |
bool |
Show upcoming TV row |
rowOrder |
List<string> |
Custom row order |
cd Moonfin.Server
dotnet buildThe built DLL goes in your Jellyfin plugin folder.
Clients should:
- On Login/Startup: Call
GET /Moonfin/Pingto check if plugin is available - If Available: Call
GET /Moonfin/Settingsto fetch user settings - Merge Settings: If server has settings, merge with local (server wins for conflicts)
- No Server Settings: If 404, push local settings to server with
POST /Moonfin/Settings - On Settings Change: Save to server with
mergeMode: "merge"
Example client code:
// Check if server plugin is installed
const ping = await fetch(`${serverUrl}/Moonfin/Ping`);
const { installed, settingsSyncEnabled } = await ping.json();
if (installed && settingsSyncEnabled) {
// Fetch settings
const response = await fetch(`${serverUrl}/Moonfin/Settings`, {
headers: { Authorization: `MediaBrowser Token="${token}"` }
});
if (response.ok) {
const serverSettings = await response.json();
// Merge with local
} else if (response.status === 404) {
// Push local settings to server
await fetch(`${serverUrl}/Moonfin/Settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `MediaBrowser Token="${token}"`
},
body: JSON.stringify({
settings: localSettings,
clientId: 'my-client',
mergeMode: 'replace'
})
});
}
}MIT