Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """MediaPlayer platform for Music Assistant integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable, Coroutine, Mapping
7 from contextlib import suppress
8 import functools
9 import os
10 from typing import TYPE_CHECKING, Any
11 
12 from music_assistant_models.enums import (
13  EventType,
14  MediaType,
15  PlayerFeature,
16  PlayerState as MassPlayerState,
17  QueueOption,
18  RepeatMode as MassRepeatMode,
19 )
20 from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
21 from music_assistant_models.event import MassEvent
22 from music_assistant_models.media_items import ItemMapping, MediaItemType, Track
23 import voluptuous as vol
24 
25 from homeassistant.components import media_source
27  ATTR_MEDIA_ENQUEUE,
28  ATTR_MEDIA_EXTRA,
29  BrowseMedia,
30  MediaPlayerDeviceClass,
31  MediaPlayerEnqueue,
32  MediaPlayerEntity,
33  MediaPlayerEntityFeature,
34  MediaPlayerState,
35  MediaType as HAMediaType,
36  RepeatMode,
37  async_process_play_media_url,
38 )
39 from homeassistant.const import STATE_OFF
40 from homeassistant.core import HomeAssistant
41 from homeassistant.exceptions import HomeAssistantError
42 from homeassistant.helpers import entity_registry as er
45  AddEntitiesCallback,
46  async_get_current_platform,
47 )
48 from homeassistant.util.dt import utc_from_timestamp
49 
50 from . import MusicAssistantConfigEntry
51 from .const import ATTR_ACTIVE_QUEUE, ATTR_MASS_PLAYER_TYPE, DOMAIN
52 from .entity import MusicAssistantEntity
53 from .media_browser import async_browse_media
54 
55 if TYPE_CHECKING:
56  from music_assistant_client import MusicAssistantClient
57  from music_assistant_models.player import Player
58  from music_assistant_models.player_queue import PlayerQueue
59 
60 SUPPORTED_FEATURES = (
61  MediaPlayerEntityFeature.PAUSE
62  | MediaPlayerEntityFeature.VOLUME_SET
63  | MediaPlayerEntityFeature.STOP
64  | MediaPlayerEntityFeature.PREVIOUS_TRACK
65  | MediaPlayerEntityFeature.NEXT_TRACK
66  | MediaPlayerEntityFeature.SHUFFLE_SET
67  | MediaPlayerEntityFeature.REPEAT_SET
68  | MediaPlayerEntityFeature.TURN_ON
69  | MediaPlayerEntityFeature.TURN_OFF
70  | MediaPlayerEntityFeature.PLAY
71  | MediaPlayerEntityFeature.PLAY_MEDIA
72  | MediaPlayerEntityFeature.VOLUME_STEP
73  | MediaPlayerEntityFeature.CLEAR_PLAYLIST
74  | MediaPlayerEntityFeature.BROWSE_MEDIA
75  | MediaPlayerEntityFeature.MEDIA_ENQUEUE
76  | MediaPlayerEntityFeature.MEDIA_ANNOUNCE
77  | MediaPlayerEntityFeature.SEEK
78 )
79 
80 QUEUE_OPTION_MAP = {
81  # map from HA enqueue options to MA enqueue options
82  # which are the same but just in case
83  MediaPlayerEnqueue.ADD: QueueOption.ADD,
84  MediaPlayerEnqueue.NEXT: QueueOption.NEXT,
85  MediaPlayerEnqueue.PLAY: QueueOption.PLAY,
86  MediaPlayerEnqueue.REPLACE: QueueOption.REPLACE,
87 }
88 
89 SERVICE_PLAY_MEDIA_ADVANCED = "play_media"
90 SERVICE_PLAY_ANNOUNCEMENT = "play_announcement"
91 SERVICE_TRANSFER_QUEUE = "transfer_queue"
92 ATTR_RADIO_MODE = "radio_mode"
93 ATTR_MEDIA_ID = "media_id"
94 ATTR_MEDIA_TYPE = "media_type"
95 ATTR_ARTIST = "artist"
96 ATTR_ALBUM = "album"
97 ATTR_URL = "url"
98 ATTR_USE_PRE_ANNOUNCE = "use_pre_announce"
99 ATTR_ANNOUNCE_VOLUME = "announce_volume"
100 ATTR_SOURCE_PLAYER = "source_player"
101 ATTR_AUTO_PLAY = "auto_play"
102 
103 
104 def catch_musicassistant_error[_R, **P](
105  func: Callable[..., Awaitable[_R]],
106 ) -> Callable[..., Coroutine[Any, Any, _R | None]]:
107  """Check and log commands to players."""
108 
109  @functools.wraps(func)
110  async def wrapper(
111  self: MusicAssistantPlayer, *args: P.args, **kwargs: P.kwargs
112  ) -> _R | None:
113  """Catch Music Assistant errors and convert to Home Assistant error."""
114  try:
115  return await func(self, *args, **kwargs)
116  except MusicAssistantError as err:
117  error_msg = str(err) or err.__class__.__name__
118  raise HomeAssistantError(error_msg) from err
119 
120  return wrapper
121 
122 
124  hass: HomeAssistant,
125  entry: MusicAssistantConfigEntry,
126  async_add_entities: AddEntitiesCallback,
127 ) -> None:
128  """Set up Music Assistant MediaPlayer(s) from Config Entry."""
129  mass = entry.runtime_data.mass
130  added_ids = set()
131 
132  async def handle_player_added(event: MassEvent) -> None:
133  """Handle Mass Player Added event."""
134  if TYPE_CHECKING:
135  assert event.object_id is not None
136  if event.object_id in added_ids:
137  return
138  added_ids.add(event.object_id)
139  async_add_entities([MusicAssistantPlayer(mass, event.object_id)])
140 
141  # register listener for new players
142  entry.async_on_unload(mass.subscribe(handle_player_added, EventType.PLAYER_ADDED))
143  mass_players = []
144  # add all current players
145  for player in mass.players:
146  added_ids.add(player.player_id)
147  mass_players.append(MusicAssistantPlayer(mass, player.player_id))
148 
149  async_add_entities(mass_players)
150 
151  # add platform service for play_media with advanced options
152  platform = async_get_current_platform()
153  platform.async_register_entity_service(
154  SERVICE_PLAY_MEDIA_ADVANCED,
155  {
156  vol.Required(ATTR_MEDIA_ID): vol.All(cv.ensure_list, [cv.string]),
157  vol.Optional(ATTR_MEDIA_TYPE): vol.Coerce(MediaType),
158  vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Coerce(QueueOption),
159  vol.Optional(ATTR_ARTIST): cv.string,
160  vol.Optional(ATTR_ALBUM): cv.string,
161  vol.Optional(ATTR_RADIO_MODE): vol.Coerce(bool),
162  },
163  "_async_handle_play_media",
164  )
165  platform.async_register_entity_service(
166  SERVICE_PLAY_ANNOUNCEMENT,
167  {
168  vol.Required(ATTR_URL): cv.string,
169  vol.Optional(ATTR_USE_PRE_ANNOUNCE): vol.Coerce(bool),
170  vol.Optional(ATTR_ANNOUNCE_VOLUME): vol.Coerce(int),
171  },
172  "_async_handle_play_announcement",
173  )
174  platform.async_register_entity_service(
175  SERVICE_TRANSFER_QUEUE,
176  {
177  vol.Optional(ATTR_SOURCE_PLAYER): cv.entity_id,
178  vol.Optional(ATTR_AUTO_PLAY): vol.Coerce(bool),
179  },
180  "_async_handle_transfer_queue",
181  )
182 
183 
185  """Representation of MediaPlayerEntity from Music Assistant Player."""
186 
187  _attr_name = None
188  _attr_media_image_remotely_accessible = True
189  _attr_media_content_type = HAMediaType.MUSIC
190 
191  def __init__(self, mass: MusicAssistantClient, player_id: str) -> None:
192  """Initialize MediaPlayer entity."""
193  super().__init__(mass, player_id)
194  self._attr_icon_attr_icon = self.playerplayer.icon.replace("mdi-", "mdi:")
195  self._attr_supported_features_attr_supported_features = SUPPORTED_FEATURES
196  if PlayerFeature.SET_MEMBERS in self.playerplayer.supported_features:
197  self._attr_supported_features_attr_supported_features |= MediaPlayerEntityFeature.GROUPING
198  if PlayerFeature.VOLUME_MUTE in self.playerplayer.supported_features:
199  self._attr_supported_features_attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
200  self._attr_device_class_attr_device_class = MediaPlayerDeviceClass.SPEAKER
201  self._prev_time_prev_time: float = 0
202 
203  async def async_added_to_hass(self) -> None:
204  """Register callbacks."""
205  await super().async_added_to_hass()
206 
207  # we subscribe to player queue time update but we only
208  # accept a state change on big time jumps (e.g. seeking)
209  async def queue_time_updated(event: MassEvent) -> None:
210  if event.object_id != self.playerplayer.active_source:
211  return
212  if abs((self._prev_time_prev_time or 0) - event.data) > 5:
213  await self.async_on_updateasync_on_updateasync_on_update()
214  self.async_write_ha_stateasync_write_ha_state()
215  self._prev_time_prev_time = event.data
216 
217  self.async_on_removeasync_on_remove(
218  self.massmass.subscribe(
219  queue_time_updated,
220  EventType.QUEUE_TIME_UPDATED,
221  )
222  )
223 
224  @property
225  def active_queue(self) -> PlayerQueue | None:
226  """Return the active queue for this player (if any)."""
227  if not self.playerplayer.active_source:
228  return None
229  return self.massmass.player_queues.get(self.playerplayer.active_source)
230 
231  @property
232  def extra_state_attributes(self) -> Mapping[str, Any]:
233  """Return additional state attributes."""
234  return {
235  ATTR_MASS_PLAYER_TYPE: self.playerplayer.type.value,
236  ATTR_ACTIVE_QUEUE: (
237  self.active_queueactive_queue.queue_id if self.active_queueactive_queue else None
238  ),
239  }
240 
241  async def async_on_update(self) -> None:
242  """Handle player updates."""
243  if not self.availableavailableavailable:
244  return
245  player = self.playerplayer
246  active_queue = self.active_queueactive_queue
247  # update generic attributes
248  if player.powered and active_queue is not None:
249  self._attr_state_attr_state = MediaPlayerState(active_queue.state.value)
250  if player.powered and player.state is not None:
251  self._attr_state_attr_state = MediaPlayerState(player.state.value)
252  else:
253  self._attr_state_attr_state = MediaPlayerState(STATE_OFF)
254  group_members_entity_ids: list[str] = []
255  if player.group_childs:
256  # translate MA group_childs to HA group_members as entity id's
257  entity_registry = er.async_get(self.hasshass)
258  group_members_entity_ids = [
259  entity_id
260  for child_id in player.group_childs
261  if (
262  entity_id := entity_registry.async_get_entity_id(
263  self.platformplatform.domain, DOMAIN, child_id
264  )
265  )
266  ]
267  # NOTE: we sort the group_members for now,
268  # until the MA API returns them sorted (group_childs is now a set)
269  self._attr_group_members_attr_group_members = sorted(group_members_entity_ids)
270  self._attr_volume_level_attr_volume_level = (
271  player.volume_level / 100 if player.volume_level is not None else None
272  )
273  self._attr_is_volume_muted_attr_is_volume_muted = player.volume_muted
274  self._update_media_attributes_update_media_attributes(player, active_queue)
275  self._update_media_image_url_update_media_image_url(player, active_queue)
276 
277  @catch_musicassistant_error
278  async def async_media_play(self) -> None:
279  """Send play command to device."""
280  await self.massmass.players.player_command_play(self.player_idplayer_id)
281 
282  @catch_musicassistant_error
283  async def async_media_pause(self) -> None:
284  """Send pause command to device."""
285  await self.massmass.players.player_command_pause(self.player_idplayer_id)
286 
287  @catch_musicassistant_error
288  async def async_media_stop(self) -> None:
289  """Send stop command to device."""
290  await self.massmass.players.player_command_stop(self.player_idplayer_id)
291 
292  @catch_musicassistant_error
293  async def async_media_next_track(self) -> None:
294  """Send next track command to device."""
295  await self.massmass.players.player_command_next_track(self.player_idplayer_id)
296 
297  @catch_musicassistant_error
298  async def async_media_previous_track(self) -> None:
299  """Send previous track command to device."""
300  await self.massmass.players.player_command_previous_track(self.player_idplayer_id)
301 
302  @catch_musicassistant_error
303  async def async_media_seek(self, position: float) -> None:
304  """Send seek command."""
305  position = int(position)
306  await self.massmass.players.player_command_seek(self.player_idplayer_id, position)
307 
308  @catch_musicassistant_error
309  async def async_mute_volume(self, mute: bool) -> None:
310  """Mute the volume."""
311  await self.massmass.players.player_command_volume_mute(self.player_idplayer_id, mute)
312 
313  @catch_musicassistant_error
314  async def async_set_volume_level(self, volume: float) -> None:
315  """Send new volume_level to device."""
316  volume = int(volume * 100)
317  await self.massmass.players.player_command_volume_set(self.player_idplayer_id, volume)
318 
319  @catch_musicassistant_error
320  async def async_volume_up(self) -> None:
321  """Send new volume_level to device."""
322  await self.massmass.players.player_command_volume_up(self.player_idplayer_id)
323 
324  @catch_musicassistant_error
325  async def async_volume_down(self) -> None:
326  """Send new volume_level to device."""
327  await self.massmass.players.player_command_volume_down(self.player_idplayer_id)
328 
329  @catch_musicassistant_error
330  async def async_turn_on(self) -> None:
331  """Turn on device."""
332  await self.massmass.players.player_command_power(self.player_idplayer_id, True)
333 
334  @catch_musicassistant_error
335  async def async_turn_off(self) -> None:
336  """Turn off device."""
337  await self.massmass.players.player_command_power(self.player_idplayer_id, False)
338 
339  @catch_musicassistant_error
340  async def async_set_shuffle(self, shuffle: bool) -> None:
341  """Set shuffle state."""
342  if not self.active_queueactive_queue:
343  return
344  await self.massmass.player_queues.queue_command_shuffle(
345  self.active_queueactive_queue.queue_id, shuffle
346  )
347 
348  @catch_musicassistant_error
349  async def async_set_repeat(self, repeat: RepeatMode) -> None:
350  """Set repeat state."""
351  if not self.active_queueactive_queue:
352  return
353  await self.massmass.player_queues.queue_command_repeat(
354  self.active_queueactive_queue.queue_id, MassRepeatMode(repeat)
355  )
356 
357  @catch_musicassistant_error
358  async def async_clear_playlist(self) -> None:
359  """Clear players playlist."""
360  if TYPE_CHECKING:
361  assert self.playerplayer.active_source is not None
362  if queue := self.massmass.player_queues.get(self.playerplayer.active_source):
363  await self.massmass.player_queues.queue_command_clear(queue.queue_id)
364 
365  @catch_musicassistant_error
366  async def async_play_media(
367  self,
368  media_type: MediaType | str,
369  media_id: str,
370  enqueue: MediaPlayerEnqueue | None = None,
371  announce: bool | None = None,
372  **kwargs: Any,
373  ) -> None:
374  """Send the play_media command to the media player."""
375  if media_source.is_media_source_id(media_id):
376  # Handle media_source
377  sourced_media = await media_source.async_resolve_media(
378  self.hasshass, media_id, self.entity_identity_id
379  )
380  media_id = sourced_media.url
381  media_id = async_process_play_media_url(self.hasshass, media_id)
382 
383  if announce:
384  await self._async_handle_play_announcement_async_handle_play_announcement(
385  media_id,
386  use_pre_announce=kwargs[ATTR_MEDIA_EXTRA].get("use_pre_announce"),
387  announce_volume=kwargs[ATTR_MEDIA_EXTRA].get("announce_volume"),
388  )
389  return
390 
391  # forward to our advanced play_media handler
392  await self._async_handle_play_media_async_handle_play_media(
393  media_id=[media_id],
394  enqueue=enqueue,
395  media_type=media_type,
396  radio_mode=kwargs[ATTR_MEDIA_EXTRA].get(ATTR_RADIO_MODE),
397  )
398 
399  @catch_musicassistant_error
400  async def async_join_players(self, group_members: list[str]) -> None:
401  """Join `group_members` as a player group with the current player."""
402  player_ids: list[str] = []
403  entity_registry = er.async_get(self.hasshass)
404  for child_entity_id in group_members:
405  # resolve HA entity_id to MA player_id
406  if not (entity_reg_entry := entity_registry.async_get(child_entity_id)):
407  raise HomeAssistantError(f"Entity {child_entity_id} not found")
408  # unique id is the MA player_id
409  player_ids.append(entity_reg_entry.unique_id)
410  await self.massmass.players.player_command_group_many(self.player_idplayer_id, player_ids)
411 
412  @catch_musicassistant_error
413  async def async_unjoin_player(self) -> None:
414  """Remove this player from any group."""
415  await self.massmass.players.player_command_ungroup(self.player_idplayer_id)
416 
417  @catch_musicassistant_error
419  self,
420  media_id: list[str],
421  artist: str | None = None,
422  album: str | None = None,
423  enqueue: MediaPlayerEnqueue | QueueOption | None = None,
424  radio_mode: bool | None = None,
425  media_type: str | None = None,
426  ) -> None:
427  """Send the play_media command to the media player."""
428  media_uris: list[str] = []
429  item: MediaItemType | ItemMapping | None = None
430  # work out (all) uri(s) to play
431  for media_id_str in media_id:
432  # URL or URI string
433  if "://" in media_id_str:
434  media_uris.append(media_id_str)
435  continue
436  # try content id as library id
437  if media_type and media_id_str.isnumeric():
438  with suppress(MediaNotFoundError):
439  item = await self.massmass.music.get_item(
440  MediaType(media_type), media_id_str, "library"
441  )
442  if isinstance(item, MediaItemType | ItemMapping) and item.uri:
443  media_uris.append(item.uri)
444  continue
445  # try local accessible filename
446  elif await asyncio.to_thread(os.path.isfile, media_id_str):
447  media_uris.append(media_id_str)
448  continue
449  # last resort: search for media item by name/search
450  if item := await self.massmass.music.get_item_by_name(
451  name=media_id_str,
452  artist=artist,
453  album=album,
454  media_type=MediaType(media_type) if media_type else None,
455  ):
456  media_uris.append(item.uri)
457 
458  if not media_uris:
459  raise HomeAssistantError(
460  f"Could not resolve {media_id} to playable media item"
461  )
462 
463  # determine active queue to send the play request to
464  if TYPE_CHECKING:
465  assert self.playerplayer.active_source is not None
466  if queue := self.massmass.player_queues.get(self.playerplayer.active_source):
467  queue_id = queue.queue_id
468  else:
469  queue_id = self.player_idplayer_id
470 
471  await self.massmass.player_queues.play_media(
472  queue_id,
473  media=media_uris,
474  option=self._convert_queueoption_to_media_player_enqueue_convert_queueoption_to_media_player_enqueue(enqueue),
475  radio_mode=radio_mode if radio_mode else False,
476  )
477 
478  @catch_musicassistant_error
480  self,
481  url: str,
482  use_pre_announce: bool | None = None,
483  announce_volume: int | None = None,
484  ) -> None:
485  """Send the play_announcement command to the media player."""
486  await self.massmass.players.play_announcement(
487  self.player_idplayer_id, url, use_pre_announce, announce_volume
488  )
489 
490  @catch_musicassistant_error
492  self, source_player: str | None = None, auto_play: bool | None = None
493  ) -> None:
494  """Transfer the current queue to another player."""
495  if not source_player:
496  # no source player given; try to find a playing player(queue)
497  for queue in self.massmass.player_queues:
498  if queue.state == MassPlayerState.PLAYING:
499  source_queue_id = queue.queue_id
500  break
501  else:
502  raise HomeAssistantError(
503  "Source player not specified and no playing player found."
504  )
505  else:
506  # resolve HA entity_id to MA player_id
507  entity_registry = er.async_get(self.hasshass)
508  if (entity := entity_registry.async_get(source_player)) is None:
509  raise HomeAssistantError("Source player not available.")
510  source_queue_id = entity.unique_id # unique_id is the MA player_id
511  target_queue_id = self.player_idplayer_id
512  await self.massmass.player_queues.transfer_queue(
513  source_queue_id, target_queue_id, auto_play
514  )
515 
517  self,
518  media_content_type: MediaType | str | None = None,
519  media_content_id: str | None = None,
520  ) -> BrowseMedia:
521  """Implement the websocket media browsing helper."""
522  return await async_browse_media(
523  self.hasshass,
524  self.massmass,
525  media_content_id,
526  media_content_type,
527  )
528 
530  self, player: Player, queue: PlayerQueue | None
531  ) -> None:
532  """Update image URL for the active queue item."""
533  if queue is None or queue.current_item is None:
534  self._attr_media_image_url_attr_media_image_url = None
535  return
536  if image_url := self.massmass.get_media_item_image_url(queue.current_item):
538  self.massmass.server_url not in image_url
539  )
540  self._attr_media_image_url_attr_media_image_url = image_url
541  return
542  self._attr_media_image_url_attr_media_image_url = None
543 
545  self, player: Player, queue: PlayerQueue | None
546  ) -> None:
547  """Update media attributes for the active queue item."""
548  # pylint: disable=too-many-statements
549  self._attr_media_artist_attr_media_artist = None
550  self._attr_media_album_artist_attr_media_album_artist = None
551  self._attr_media_album_name_attr_media_album_name = None
552  self._attr_media_title_attr_media_title = None
553  self._attr_media_content_id_attr_media_content_id = None
554  self._attr_media_duration_attr_media_duration = None
555  self._attr_media_position_attr_media_position = None
556  self._attr_media_position_updated_at_attr_media_position_updated_at = None
557 
558  if queue is None and player.current_media:
559  # player has some external source active
560  self._attr_media_content_id_attr_media_content_id = player.current_media.uri
561  self._attr_app_id_attr_app_id = player.active_source
562  self._attr_media_title_attr_media_title = player.current_media.title
563  self._attr_media_artist_attr_media_artist = player.current_media.artist
564  self._attr_media_album_name_attr_media_album_name = player.current_media.album
565  self._attr_media_duration_attr_media_duration = player.current_media.duration
566  # shuffle and repeat are not (yet) supported for external sources
567  self._attr_shuffle_attr_shuffle = None
568  self._attr_repeat_attr_repeat = None
569  if TYPE_CHECKING:
570  assert player.elapsed_time is not None
571  self._attr_media_position_attr_media_position = int(player.elapsed_time)
572  self._attr_media_position_updated_at_attr_media_position_updated_at = (
573  utc_from_timestamp(player.elapsed_time_last_updated)
574  if player.elapsed_time_last_updated
575  else None
576  )
577  if TYPE_CHECKING:
578  assert player.elapsed_time is not None
579  self._prev_time_prev_time = player.elapsed_time
580  return
581 
582  if queue is None:
583  # player has no MA queue active
584  self._attr_source_attr_source = player.active_source
585  self._attr_app_id_attr_app_id = player.active_source
586  return
587 
588  # player has an MA queue active (either its own queue or some group queue)
589  self._attr_app_id_attr_app_id = DOMAIN
590  self._attr_shuffle_attr_shuffle = queue.shuffle_enabled
591  self._attr_repeat_attr_repeat = queue.repeat_mode.value
592  if not (cur_item := queue.current_item):
593  # queue is empty
594  return
595 
596  self._attr_media_content_id_attr_media_content_id = queue.current_item.uri
597  self._attr_media_duration_attr_media_duration = queue.current_item.duration
598  self._attr_media_position_attr_media_position = int(queue.elapsed_time)
599  self._attr_media_position_updated_at_attr_media_position_updated_at = utc_from_timestamp(
600  queue.elapsed_time_last_updated
601  )
602  self._prev_time_prev_time = queue.elapsed_time
603 
604  # handle stream title (radio station icy metadata)
605  if (stream_details := cur_item.streamdetails) and stream_details.stream_title:
606  self._attr_media_album_name_attr_media_album_name = cur_item.name
607  if " - " in stream_details.stream_title:
608  stream_title_parts = stream_details.stream_title.split(" - ", 1)
609  self._attr_media_title_attr_media_title = stream_title_parts[1]
610  self._attr_media_artist_attr_media_artist = stream_title_parts[0]
611  else:
612  self._attr_media_title_attr_media_title = stream_details.stream_title
613  return
614 
615  if not (media_item := cur_item.media_item):
616  # queue is not playing a regular media item (edge case?!)
617  self._attr_media_title_attr_media_title = cur_item.name
618  return
619 
620  # queue is playing regular media item
621  self._attr_media_title_attr_media_title = media_item.name
622  # for tracks we can extract more info
623  if media_item.media_type == MediaType.TRACK:
624  if TYPE_CHECKING:
625  assert isinstance(media_item, Track)
626  self._attr_media_artist_attr_media_artist = media_item.artist_str
627  if media_item.version:
628  self._attr_media_title_attr_media_title += f" ({media_item.version})"
629  if media_item.album:
630  self._attr_media_album_name_attr_media_album_name = media_item.album.name
631  self._attr_media_album_artist_attr_media_album_artist = getattr(
632  media_item.album, "artist_str", None
633  )
634 
636  self, queue_option: MediaPlayerEnqueue | QueueOption | None
637  ) -> QueueOption | None:
638  """Convert a QueueOption to a MediaPlayerEnqueue."""
639  if isinstance(queue_option, MediaPlayerEnqueue):
640  queue_option = QUEUE_OPTION_MAP.get(queue_option)
641  return queue_option
None async_play_media(self, MediaType|str media_type, str media_id, MediaPlayerEnqueue|None enqueue=None, bool|None announce=None, **Any kwargs)
None _async_handle_play_media(self, list[str] media_id, str|None artist=None, str|None album=None, MediaPlayerEnqueue|QueueOption|None enqueue=None, bool|None radio_mode=None, str|None media_type=None)
None _update_media_attributes(self, Player player, PlayerQueue|None queue)
None __init__(self, MusicAssistantClient mass, str player_id)
None _update_media_image_url(self, Player player, PlayerQueue|None queue)
QueueOption|None _convert_queueoption_to_media_player_enqueue(self, MediaPlayerEnqueue|QueueOption|None queue_option)
None _async_handle_play_announcement(self, str url, bool|None use_pre_announce=None, int|None announce_volume=None)
None _async_handle_transfer_queue(self, str|None source_player=None, bool|None auto_play=None)
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str async_process_play_media_url(HomeAssistant hass, str media_content_id, *bool allow_relative_url=False, bool for_supervisor_network=False)
Definition: browse_media.py:36
Callable[[], None] subscribe(HomeAssistant hass, str topic, MessageCallbackType msg_callback, int qos=DEFAULT_QOS, str encoding="utf-8")
Definition: client.py:247
None async_setup_entry(HomeAssistant hass, MusicAssistantConfigEntry entry, AddEntitiesCallback async_add_entities)