Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \
| [Weather Display](./plugins/ledmatrix-weather/) | Current conditions, hourly & daily forecasts (Open-Meteo) | <a href="./plugins/ledmatrix-weather/"><img src="./docs/assets/ledmatrix-weather/hero.png" width="240" alt="ledmatrix-weather on an LED panel"></a> |
| [Tide Display](./plugins/tide-display/) | Coastal tides with animated wave level, schedule & 24-hour chart (NOAA) | <a href="./plugins/tide-display/"><img src="./docs/assets/tide-display/hero.png" width="240" alt="tide-display on an LED panel"></a> |

### Media (2)
### Media (3)

| Plugin | Description | Preview |
|--------|-------------|---------|
| [Jellyfin Now Playing](./plugins/jellyfin-now-playing/) | Poster, title and playback progress from your Jellyfin server | <a href="./plugins/jellyfin-now-playing/"><img src="./docs/assets/jellyfin-now-playing/hero.png" width="240" alt="jellyfin-now-playing on an LED panel"></a> |
| [Music Player](./plugins/ledmatrix-music/) | Now playing with album art (Spotify & YouTube Music) | |
| [Static Image Display](./plugins/static-image/) | Image display with scaling and transparency | <a href="./plugins/static-image/"><img src="./docs/assets/static-image/hero.png" width="240" alt="static-image on an LED panel"></a> |

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/jellyfin-now-playing/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/jellyfin-now-playing/panel-sizes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/jellyfin-now-playing/poster-album.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/jellyfin-now-playing/poster-movie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions docs/assets/jellyfin-now-playing/sessions-idle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"matches": [
{
"url_contains": "/Sessions",
"body": []
},
{
"url_contains": "/Items/movie1/Images/Primary",
"body_file": "poster-movie.png",
"content_type": "image/png"
},
{
"url_contains": "/Items/series1/Images/Primary",
"body_file": "poster-series.png",
"content_type": "image/png"
},
{
"url_contains": "/Items/album1/Images/Primary",
"body_file": "poster-album.png",
"content_type": "image/png"
}
]
}
37 changes: 37 additions & 0 deletions docs/assets/jellyfin-now-playing/sessions-paused.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"matches": [
{
"url_contains": "/Sessions",
"body": [
{
"UserName": "alex",
"NowPlayingItem": {
"Id": "movie1",
"Name": "Northern Lights",
"Type": "Movie",
"RunTimeTicks": 69600000000
},
"PlayState": {
"IsPaused": true,
"PositionTicks": 25200000000
}
}
]
},
{
"url_contains": "/Items/movie1/Images/Primary",
"body_file": "poster-movie.png",
"content_type": "image/png"
},
{
"url_contains": "/Items/series1/Images/Primary",
"body_file": "poster-series.png",
"content_type": "image/png"
},
{
"url_contains": "/Items/album1/Images/Primary",
"body_file": "poster-album.png",
"content_type": "image/png"
}
]
}
69 changes: 69 additions & 0 deletions docs/assets/jellyfin-now-playing/sessions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"matches": [
{
"url_contains": "/Sessions",
"body": [
{
"UserName": "alex",
"NowPlayingItem": {
"Id": "movie1",
"Name": "Northern Lights",
"Type": "Movie",
"RunTimeTicks": 69600000000
},
"PlayState": {
"IsPaused": false,
"PositionTicks": 25200000000
}
},
{
"UserName": "sam",
"NowPlayingItem": {
"Id": "ep1",
"Name": "The Deep Field",
"Type": "Episode",
"RunTimeTicks": 28800000000,
"SeriesName": "Cosmos Unfolded",
"SeriesId": "series1"
},
"PlayState": {
"IsPaused": false,
"PositionTicks": 7200000000
}
},
{
"UserName": "jo",
"NowPlayingItem": {
"Id": "track1",
"Name": "Neon Harbour",
"Type": "Audio",
"RunTimeTicks": 2400000000,
"Artists": [
"The Glass Hours"
],
"AlbumId": "album1"
},
"PlayState": {
"IsPaused": false,
"PositionTicks": 1200000000
}
}
]
},
{
"url_contains": "/Items/movie1/Images/Primary",
"body_file": "poster-movie.png",
"content_type": "image/png"
},
{
"url_contains": "/Items/series1/Images/Primary",
"body_file": "poster-series.png",
"content_type": "image/png"
},
{
"url_contains": "/Items/album1/Images/Primary",
"body_file": "poster-album.png",
"content_type": "image/png"
}
]
}
73 changes: 73 additions & 0 deletions docs/assets/jellyfin-now-playing/shots.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"plugin": "jellyfin-now-playing",
"defaults": {
"width": 128,
"height": 32,
"scale": 6,
"freeze_time": "2026-09-02T21:00:00+00:00",
"http_replay": "sessions.json",
"config": {
"enabled": true,
"jellyfin_url": "http://192.168.1.50:8096",
"api_key": "docs-render-key",
"content_types": ["Movie"]
}
},
"shots": [
{ "name": "hero" },
{ "name": "type-movie", "standalone": false },
{ "name": "type-episode", "standalone": false,
"config": { "content_types": ["Episode"] } },
{ "name": "type-audio", "standalone": false,
"config": { "content_types": ["Audio"] } },
{ "name": "playing", "standalone": false },
{ "name": "paused", "standalone": false, "http_replay": "sessions-paused.json" },
{ "name": "idle", "standalone": false, "http_replay": "sessions-idle.json" },
{ "name": "bar-match", "standalone": false,
"width": 256, "config": { "progress_bar_match_text": true } },
{ "name": "bar-full", "standalone": false,
"width": 256, "config": { "progress_bar_match_text": false } },
{ "name": "size-64x32", "width": 64, "standalone": false },
{ "name": "size-128x32", "width": 128, "standalone": false },
{ "name": "size-128x64", "width": 128, "height": 64, "standalone": false },
{ "name": "size-256x32", "width": 256, "standalone": false }
],
"composites": [
{
"name": "content-types",
"columns": 1,
"cells": [
{"shot": "type-movie", "label": "Movie", "sublabel": "title, then the watching user"},
{"shot": "type-episode", "label": "Episode", "sublabel": "episode title, then the series"},
{"shot": "type-audio", "label": "Audio", "sublabel": "track title, then the artist"}
]
},
{
"name": "playback-states",
"columns": 1,
"cells": [
{"shot": "playing", "label": "Playing"},
{"shot": "paused", "label": "Paused", "sublabel": "the bar turns amber"},
{"shot": "idle", "label": "Nothing playing"}
]
},
{
"name": "progress-bar-width",
"columns": 1,
"cells": [
{"shot": "bar-match", "label": "progress_bar_match_text: true", "sublabel": "bar matches the text width"},
{"shot": "bar-full", "label": "progress_bar_match_text: false", "sublabel": "bar spans the text area"}
]
},
{
"name": "panel-sizes",
"columns": 2,
"cells": [
{"shot": "size-64x32", "label": "64 x 32"},
{"shot": "size-128x32", "label": "128 x 32"},
{"shot": "size-128x64", "label": "128 x 64"},
{"shot": "size-256x32", "label": "256 x 32"}
]
}
]
}
4 changes: 2 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -1116,10 +1116,10 @@
"plugin_path": "plugins/jellyfin-now-playing",
"stars": 0,
"downloads": 0,
"last_updated": "2026-07-20",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.1.0"
"latest_version": "1.2.0"
},
{
"id": "incoming-packages",
Expand Down
63 changes: 60 additions & 3 deletions plugins/jellyfin-now-playing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,31 @@
Shows what's currently playing on your Jellyfin media server: the poster on the
left, with the title, a subtitle, and a playback progress bar on the right.

![A movie playing on a 128x32 panel: poster on the left, title and user name on
the right, with a partly filled progress bar](../../docs/assets/jellyfin-now-playing/hero.png)

*Every image in this README is real plugin output, rendered at the true panel
size against a recorded Jellyfin response and scaled up so the pixels stay
pixels. The poster art in them is generated, not real cover art.*

- **Movies** show the movie poster with the title and the watching user's name.
- **TV episodes** show the *series* poster with the episode title and series name.
- **Music** shows the album art with the track title and artist.
- Long titles scroll as a marquee (or truncate with `...` if scrolling is disabled).
- The progress bar moves smoothly between polls and turns **amber with a ⏸
indicator** while playback is paused.
- When nothing is playing, a dim "Nothing Playing" screen is shown.
- Works on all supported panel sizes (64×32, 128×32, 128×64, 256×32). Wide and
tall panels also get a `position / duration` time readout.
- Works on all supported panel sizes. Wide and tall panels also get a
`position / duration` time readout.

![The three content types side by side](../../docs/assets/jellyfin-now-playing/content-types.png)

The subtitle row is what changes between them: a movie shows who is watching,
an episode shows its series, and a track shows its artist.

![Playing, paused, and nothing playing](../../docs/assets/jellyfin-now-playing/playback-states.png)

![The same session on four panel sizes](../../docs/assets/jellyfin-now-playing/panel-sizes.png)

## Setup

Expand Down Expand Up @@ -46,7 +62,48 @@ Until both are set, the panel shows `Jellyfin: Set URL/API Key`.
| `scroll_enabled` | `true` | Marquee-scroll text that doesn't fit |
| `scroll_speed` | `5` | Frames per one-character scroll step; higher is slower (advanced) |
| `scroll_separator` | `" "` | Gap text between marquee repetitions (advanced) |
| `customization` | — | Fonts, sizes, and colors for the title, subtitle, and progress bar |
| `progress_bar_match_text` | `true` | Size the bar to the text rather than the whole text area (advanced) — see below |

### Fonts and colors

These live under `customization` in `config.json`, and under **Display
Customization** in the web UI.

| Key | Default | Description |
|---|---|---|
| `customization.title_text.font` | `5by7.regular.ttf` | Font for the media title (advanced) |
| `customization.title_text.font_size` | `7` | Title height in pixels, 4–16 (advanced) |
| `customization.title_text.text_color` | `[255, 255, 255]` | Title color |
| `customization.subtitle_text.font` | `4x6-font.ttf` | Font for the subtitle and the time readout (advanced) |
| `customization.subtitle_text.font_size` | `6` | Subtitle height in pixels, 4–16 (advanced) |
| `customization.subtitle_text.text_color` | `[170, 170, 170]` | Subtitle color |
| `customization.progress_bar.bar_color` | `[124, 77, 255]` | Filled portion of the bar. Ignored while paused, when the bar is amber |
| `customization.progress_bar.background_color` | `[40, 40, 40]` | Unfilled portion of the bar |

Five fonts are offered. Three are TrueType and work at every size in the 4–16
range; two are `.bdf` bitmap faces, which exist at exactly one size each:

| Font | Kind | Sizes |
|---|---|---|
| `5by7.regular.ttf` | TrueType | any |
| `4x6-font.ttf` | TrueType | any |
| `PressStart2P-Regular.ttf` | TrueType | any |
| `5x7.bdf` | bitmap | 7 only |
| `4x6.bdf` | bitmap | 6 only |

Picking a bitmap font at a size it does not have used to fall back to a much
smaller built-in font with only a log warning. It now loads at the font's own
size instead, so the **Font Size** setting is simply ignored for those two.

### Progress bar width

`progress_bar_match_text` decides how far the bar runs. It matters most on a
wide panel, where a short title otherwise leaves a bar stretched across the
whole display:

![The bar matching the text versus spanning the text area](../../docs/assets/jellyfin-now-playing/progress-bar-width.png)

A title long enough to scroll fills the bar either way.

## Multiple sessions

Expand Down
6 changes: 2 additions & 4 deletions plugins/jellyfin-now-playing/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@
"4x6-font.ttf",
"5by7.regular.ttf",
"5x7.bdf",
"4x6.bdf",
"cozette.bdf"
"4x6.bdf"
],
"default": "5by7.regular.ttf",
"x-advanced": true
Expand Down Expand Up @@ -152,8 +151,7 @@
"4x6-font.ttf",
"5by7.regular.ttf",
"5x7.bdf",
"4x6.bdf",
"cozette.bdf"
"4x6.bdf"
],
"default": "4x6-font.ttf",
"x-advanced": true
Expand Down
30 changes: 29 additions & 1 deletion plugins/jellyfin-now-playing/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,21 @@ def _load_font(self, font_name: str, font_size: int):
try:
font = ImageFont.truetype(candidate, font_size)
break
except Exception as e:
except OSError as e:
# A .bdf is a bitmap face that exists at exactly one pixel
# size; FreeType refuses every other. Without this the
# offered bitmap fonts fall back to PIL's default at all
# but one of the sizes the schema allows.
native = self._bdf_pixel_size(candidate)
if native is not None and native != font_size:
try:
font = ImageFont.truetype(candidate, native)
self.logger.debug(
"Loaded bitmap font %s at its native size %d "
"(requested %d)", font_name, native, font_size)
break
except OSError:
pass
self.logger.warning("Could not load font %s: %s", candidate, e)
if font is None:
self.logger.warning("Font %s not found, using default", font_name)
Expand All @@ -168,6 +182,20 @@ def _load_font(self, font_name: str, font_size: int):
self._font_cache[cache_key] = font
return font

@staticmethod
def _bdf_pixel_size(path: str) -> Optional[int]:
"""The pixel size a .bdf font declares, or None if it does not."""
try:
with open(path, "r", encoding="latin-1") as handle:
for line in handle:
if line.startswith("PIXEL_SIZE"):
return int(line.split()[1])
if line.startswith("CHARS"):
break # past the header; no point reading the glyphs
except (OSError, ValueError, IndexError):
return None
return None

def _dims(self) -> Tuple[int, int]:
"""Panel (width, height); prefers .width/.height, falls back to .matrix."""
width = getattr(self.display_manager, 'width', None)
Expand Down
Loading
Loading