Add Jellyfin Now Playing + Grafana Panel embed widgets
Two new additive widget kinds: Jellyfin 'now_playing': like the existing 'activity' widget but filters to only sessions with active playback (NowPlayingItem present + not paused). Shows who's actually watching right now. The 'activity' kind is unchanged (shows all sessions including idle). Grafana 'panel': embeds a single Grafana panel directly in the app via an iframe, using Grafana's /d-solo/ endpoint (renders one panel without dashboard chrome, kiosk=tv). Configurable dashboard_uid, panel_id, and time range (from/to, defaults now-1h/now). Includes a fallback 'Open in Grafana' link for when embedding is blocked by X-Frame-Options/CSP. The 'link' kind is unchanged (still builds a deep-link URL). Backend: new widget configs + definitions on jellyfin/grafana; source adapter logic (session filter for now_playing; d-solo embed URL for panel); 6 new tests. Frontend: JellyfinNowPlayingWidget + GrafanaPanelWidget components; registry bindings; 6 new tests. 278 backend tests pass (+6); 127 frontend tests pass (+6); lint/build green both sides.
This commit is contained in:
@@ -97,7 +97,7 @@ class StaticWidgetSource:
|
||||
|
||||
|
||||
class GrafanaWidgetSource:
|
||||
"""Build a Grafana deep-link (no embedding)."""
|
||||
"""Build a Grafana deep-link or panel embed URL."""
|
||||
|
||||
async def fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
@@ -107,6 +107,19 @@ class GrafanaWidgetSource:
|
||||
dashboard_uid = config.get("dashboard_uid")
|
||||
if not dashboard_uid:
|
||||
return {"error": "dashboard_uid is required"}
|
||||
|
||||
if widget_kind == "panel":
|
||||
panel_id = config.get("panel_id")
|
||||
if panel_id is None:
|
||||
return {"error": "panel_id is required"}
|
||||
from_ts = config.get("from_ts", "now-1h")
|
||||
to_ts = config.get("to_ts", "now")
|
||||
embed_url = (
|
||||
f"{base_url}/d-solo/{dashboard_uid}/manage?panelId={panel_id}&from={from_ts}&to={to_ts}&kiosk=tv"
|
||||
)
|
||||
return {"embed_url": embed_url}
|
||||
|
||||
# Default: deep-link
|
||||
url = f"{base_url}/d/{dashboard_uid}"
|
||||
panel_id = config.get("panel_id")
|
||||
if panel_id is not None:
|
||||
@@ -208,6 +221,10 @@ class JellyfinWidgetSource:
|
||||
asyncio.to_thread(client.sessions),
|
||||
timeout=timeout,
|
||||
)
|
||||
if widget_kind == "now_playing":
|
||||
sessions = [
|
||||
s for s in sessions if s.get("NowPlayingItem") and not s.get("PlayState", {}).get("IsPaused", True)
|
||||
]
|
||||
rows = _map_sessions_to_activity_rows(sessions)
|
||||
return {"sessions": rows}
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
Reference in New Issue
Block a user