Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for the Jellyfin media player."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
8  BrowseMedia,
9  MediaPlayerEntity,
10  MediaPlayerEntityFeature,
11  MediaPlayerState,
12  MediaType,
13 )
14 from homeassistant.core import HomeAssistant, callback
15 from homeassistant.helpers.entity_platform import AddEntitiesCallback
16 from homeassistant.util.dt import parse_datetime
17 
18 from . import JellyfinConfigEntry
19 from .browse_media import build_item_response, build_root_response
20 from .client_wrapper import get_artwork_url
21 from .const import CONTENT_TYPE_MAP, LOGGER
22 from .coordinator import JellyfinDataUpdateCoordinator
23 from .entity import JellyfinClientEntity
24 
25 
27  hass: HomeAssistant,
28  entry: JellyfinConfigEntry,
29  async_add_entities: AddEntitiesCallback,
30 ) -> None:
31  """Set up Jellyfin media_player from a config entry."""
32  coordinator = entry.runtime_data
33 
34  @callback
35  def handle_coordinator_update() -> None:
36  """Add media player per session."""
37  entities: list[MediaPlayerEntity] = []
38  for session_id in coordinator.data:
39  if session_id not in coordinator.session_ids:
40  entity: MediaPlayerEntity = JellyfinMediaPlayer(coordinator, session_id)
41  LOGGER.debug("Creating media player for session: %s", session_id)
42  coordinator.session_ids.add(session_id)
43  entities.append(entity)
44  async_add_entities(entities)
45 
46  handle_coordinator_update()
47 
48  entry.async_on_unload(coordinator.async_add_listener(handle_coordinator_update))
49 
50 
52  """Represents a Jellyfin Player device."""
53 
54  def __init__(
55  self,
56  coordinator: JellyfinDataUpdateCoordinator,
57  session_id: str,
58  ) -> None:
59  """Initialize the Jellyfin Media Player entity."""
60  super().__init__(coordinator, session_id)
61  self._attr_unique_id_attr_unique_id = f"{coordinator.server_id}-{session_id}"
62 
63  self.now_playingnow_playing: dict[str, Any] | None = self.session_datasession_data.get(
64  "NowPlayingItem"
65  )
66  self.play_stateplay_state: dict[str, Any] | None = self.session_datasession_data.get("PlayState")
67 
68  self._update_from_session_data_update_from_session_data()
69 
70  @callback
71  def _handle_coordinator_update(self) -> None:
72  if self.availableavailableavailableavailable:
73  self.now_playingnow_playing = self.session_datasession_data.get("NowPlayingItem")
74  self.play_stateplay_state = self.session_datasession_data.get("PlayState")
75  else:
76  self.now_playingnow_playing = None
77  self.play_stateplay_state = None
78 
79  self._update_from_session_data_update_from_session_data()
81 
82  @callback
83  def _update_from_session_data(self) -> None:
84  """Process session data to update entity properties."""
85  state = None
86  media_content_type = None
87  media_content_id = None
88  media_title = None
89  media_series_title = None
90  media_season = None
91  media_episode = None
92  media_album_name = None
93  media_album_artist = None
94  media_artist = None
95  media_track = None
96  media_duration = None
97  media_position = None
98  media_position_updated = None
99  volume_muted = False
100  volume_level = None
101 
102  if self.availableavailableavailableavailable:
103  state = MediaPlayerState.IDLE
104  media_position_updated = (
105  parse_datetime(self.session_datasession_data["LastPlaybackCheckIn"])
106  if self.now_playingnow_playing
107  else None
108  )
109 
110  if self.now_playingnow_playing is not None:
111  state = MediaPlayerState.PLAYING
112  media_content_type = CONTENT_TYPE_MAP.get(self.now_playingnow_playing["Type"], None)
113  media_content_id = self.now_playingnow_playing["Id"]
114  media_title = self.now_playingnow_playing["Name"]
115 
116  if "RunTimeTicks" in self.now_playingnow_playing:
117  media_duration = int(self.now_playingnow_playing["RunTimeTicks"] / 10000000)
118 
119  if media_content_type == MediaType.EPISODE:
120  media_content_type = MediaType.TVSHOW
121  media_series_title = self.now_playingnow_playing.get("SeriesName")
122  media_season = self.now_playingnow_playing.get("ParentIndexNumber")
123  media_episode = self.now_playingnow_playing.get("IndexNumber")
124  elif media_content_type == MediaType.MUSIC:
125  media_album_name = self.now_playingnow_playing.get("Album")
126  media_album_artist = self.now_playingnow_playing.get("AlbumArtist")
127  media_track = self.now_playingnow_playing.get("IndexNumber")
128  if media_artists := self.now_playingnow_playing.get("Artists"):
129  media_artist = str(media_artists[0])
130 
131  if self.play_stateplay_state is not None:
132  if self.play_stateplay_state.get("IsPaused"):
133  state = MediaPlayerState.PAUSED
134 
135  media_position = (
136  int(self.play_stateplay_state["PositionTicks"] / 10000000)
137  if "PositionTicks" in self.play_stateplay_state
138  else None
139  )
140  volume_muted = bool(self.play_stateplay_state.get("IsMuted", False))
141  volume_level = (
142  float(self.play_stateplay_state["VolumeLevel"] / 100)
143  if "VolumeLevel" in self.play_stateplay_state
144  else None
145  )
146 
147  self._attr_state_attr_state = state
148  self._attr_is_volume_muted_attr_is_volume_muted = volume_muted
149  self._attr_volume_level_attr_volume_level = volume_level
150  self._attr_media_content_type_attr_media_content_type = media_content_type
151  self._attr_media_content_id_attr_media_content_id = media_content_id
152  self._attr_media_title_attr_media_title = media_title
153  self._attr_media_series_title_attr_media_series_title = media_series_title
154  self._attr_media_season_attr_media_season = media_season
155  self._attr_media_episode_attr_media_episode = media_episode
156  self._attr_media_album_name_attr_media_album_name = media_album_name
157  self._attr_media_album_artist_attr_media_album_artist = media_album_artist
158  self._attr_media_artist_attr_media_artist = media_artist
159  self._attr_media_track_attr_media_track = media_track
160  self._attr_media_duration_attr_media_duration = media_duration
161  self._attr_media_position_attr_media_position = media_position
162  self._attr_media_position_updated_at_attr_media_position_updated_at = media_position_updated
163  self._attr_media_image_remotely_accessible_attr_media_image_remotely_accessible = True
164 
165  @property
166  def media_image_url(self) -> str | None:
167  """Image url of current playing media."""
168  # We always need the now playing item.
169  # If there is none, there's also no url
170  if self.now_playingnow_playing is None:
171  return None
172 
173  return get_artwork_url(self.coordinator.api_client, self.now_playingnow_playing, 150)
174 
175  @property
176  def supported_features(self) -> MediaPlayerEntityFeature:
177  """Flag media player features that are supported."""
178  commands: list[str] = self.capabilities.get("SupportedCommands", [])
179  controllable = self.capabilities.get("SupportsMediaControl", False)
180  features = MediaPlayerEntityFeature(0)
181 
182  if controllable:
183  features |= (
184  MediaPlayerEntityFeature.BROWSE_MEDIA
185  | MediaPlayerEntityFeature.PLAY_MEDIA
186  | MediaPlayerEntityFeature.PAUSE
187  | MediaPlayerEntityFeature.PLAY
188  | MediaPlayerEntityFeature.STOP
189  | MediaPlayerEntityFeature.SEEK
190  )
191 
192  if "Mute" in commands:
193  features |= MediaPlayerEntityFeature.VOLUME_MUTE
194 
195  if "VolumeSet" in commands:
196  features |= MediaPlayerEntityFeature.VOLUME_SET
197 
198  return features
199 
200  def media_seek(self, position: float) -> None:
201  """Send seek command."""
202  self.coordinator.api_client.jellyfin.remote_seek(
203  self.session_idsession_id, int(position * 10000000)
204  )
205 
206  def media_pause(self) -> None:
207  """Send pause command."""
208  self.coordinator.api_client.jellyfin.remote_pause(self.session_idsession_id)
209  self._attr_state_attr_state = MediaPlayerState.PAUSED
210 
211  def media_play(self) -> None:
212  """Send play command."""
213  self.coordinator.api_client.jellyfin.remote_unpause(self.session_idsession_id)
214  self._attr_state_attr_state = MediaPlayerState.PLAYING
215 
216  def media_play_pause(self) -> None:
217  """Send the PlayPause command to the session."""
218  self.coordinator.api_client.jellyfin.remote_playpause(self.session_idsession_id)
219 
220  def media_stop(self) -> None:
221  """Send stop command."""
222  self.coordinator.api_client.jellyfin.remote_stop(self.session_idsession_id)
223  self._attr_state_attr_state = MediaPlayerState.IDLE
224 
226  self, media_type: MediaType | str, media_id: str, **kwargs: Any
227  ) -> None:
228  """Play a piece of media."""
229  self.coordinator.api_client.jellyfin.remote_play_media(
230  self.session_idsession_id, [media_id]
231  )
232 
233  def set_volume_level(self, volume: float) -> None:
234  """Set volume level, range 0..1."""
235  self.coordinator.api_client.jellyfin.remote_set_volume(
236  self.session_idsession_id, int(volume * 100)
237  )
238 
239  def mute_volume(self, mute: bool) -> None:
240  """Mute the volume."""
241  if mute:
242  self.coordinator.api_client.jellyfin.remote_mute(self.session_idsession_id)
243  else:
244  self.coordinator.api_client.jellyfin.remote_unmute(self.session_idsession_id)
245 
247  self,
248  media_content_type: MediaType | str | None = None,
249  media_content_id: str | None = None,
250  ) -> BrowseMedia:
251  """Return a BrowseMedia instance.
252 
253  The BrowseMedia instance will be used by the "media_player/browse_media" websocket command.
254 
255  """
256  if media_content_id is None or media_content_id == "media-source://jellyfin":
257  return await build_root_response(
258  self.hasshasshass, self.coordinator.api_client, self.coordinator.user_id
259  )
260 
261  return await build_item_response(
262  self.hasshasshass,
263  self.coordinator.api_client,
264  self.coordinator.user_id,
265  media_content_type,
266  media_content_id,
267  )
None play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
None __init__(self, JellyfinDataUpdateCoordinator coordinator, str session_id)
Definition: media_player.py:58
datetime|None parse_datetime(str|None value)
Definition: sensor.py:138
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
BrowseMedia build_root_response(HomeAssistant hass, JellyfinClient client, str user_id)
Definition: browse_media.py:69
BrowseMedia build_item_response(HomeAssistant hass, JellyfinClient client, str user_id, str|None media_content_type, str media_content_id)
Definition: browse_media.py:97
str|None get_artwork_url(JellyfinClient client, dict[str, Any] item, int max_width=600)
None async_setup_entry(HomeAssistant hass, JellyfinConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:30