"""Jellyfin session stream-info reader. Piggybacks on HA's built-in Jellyfin integration: its coordinator polls /Sessions every 10s and exposes the parsed result on ``config_entry.runtime_data.data``. We reach across to that cached dict by walking from the user-selected Jellyfin media_player entity back to its owning Jellyfin config entry. Zero additional config, zero extra HTTP. """ import logging from typing import Any from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er _LOGGER = logging.getLogger(__name__) def _parse_streams(item: dict) -> dict | None: """Extract friendly video/audio labels from a Jellyfin NowPlayingItem.""" streams = item.get("MediaStreams", []) result: dict = {} for stream in streams: stype = stream.get("Type", "") if stype == "Video" and "video" not in result: width = stream.get("Width", 0) height = stream.get("Height", 0) codec = (stream.get("Codec") or "").upper() display = stream.get("DisplayTitle", "") if height >= 2160: res = "4K" elif height >= 1080: res = "1080p" elif height >= 720: res = "720p" elif height > 0: res = f"{height}p" else: res = None result["video"] = { "resolution": res, "width": width, "height": height, "codec": codec, "display": display, "hdr": "HDR" in display or "DV" in display or "Dolby Vision" in display, } elif stype == "Audio" and "audio" not in result: codec = (stream.get("Codec") or "").upper() layout = stream.get("ChannelLayout", "") display = stream.get("DisplayTitle", "") if "atmos" in display.lower(): audio_label = "Atmos" elif "truehd" in codec.lower(): audio_label = "TrueHD" elif "dts" in codec.lower(): audio_label = "DTS-HD" if "hd" in display.lower() else "DTS" elif "eac3" in codec.lower() or "e-ac-3" in codec.lower(): audio_label = "DD+" elif "ac3" in codec.lower(): audio_label = "DD" elif "aac" in codec.lower(): audio_label = "AAC" elif "flac" in codec.lower(): audio_label = "FLAC" else: audio_label = codec result["audio"] = { "codec": codec, "channels": layout, "label": audio_label, "display": display, } return result or None def _get_jellyfin_coordinator(hass: HomeAssistant, jellyfin_entity_id: str) -> Any: """Resolve the Jellyfin integration's data-update coordinator for the entity the user picked. Returns the coordinator, or None if the entity is not registered / its config entry has no runtime_data yet.""" if not jellyfin_entity_id: return None registry = er.async_get(hass) entity_entry = registry.async_get(jellyfin_entity_id) if not entity_entry or not entity_entry.config_entry_id: return None config_entry = hass.config_entries.async_get_entry(entity_entry.config_entry_id) if not config_entry: return None return getattr(config_entry, "runtime_data", None) async def fetch_session_media_info( hass: HomeAssistant, jellyfin_entity_id: str, device_name: str, ) -> dict | None: """Find stream info for the session whose DeviceName matches ``device_name``. Reads from the already-authenticated Jellyfin coordinator — no new HTTP calls, no credentials in this integration's config. """ coordinator = _get_jellyfin_coordinator(hass, jellyfin_entity_id) if coordinator is None: return None sessions = getattr(coordinator, "data", None) or {} device_lower = device_name.lower() for sess in sessions.values(): sess_device = (sess.get("DeviceName") or "").lower() if device_lower not in sess_device: continue item = sess.get("NowPlayingItem") if not item: continue return _parse_streams(item) return None