def play_media(self, media_type, media_id, **kwargs): # Play a piece of media. # <ServiceCall media_player.play_media: media_content_type=music, media_content_id=http://192.168.13.91:8123/api/tts_proxy/74a4297365735b6c107b85e034347ce013eeae01_en_-_google.mp3, entity_id=['media_player.mt_office']> self._parent.send('mrad.setzone "{}"'.format(self._zoneId)) self._parent.send('mrad.setsource') media_type = media_type.lower() if media_source.is_media_source_id(media_id): media_type = "music" media_id = (asyncio.run_coroutine_threadsafe( media_source.async_resolve_media(self._hass, media_id, self.entity_id), self._hass.loop, ).result().url) media_id = async_process_play_media_url(self.hass, media_id) if media_type == "music": self._parent.send('duckplay "{}"'.format(media_id)) elif media_type == "scene": self._parent.send('recallscene "{}"'.format(media_id)) elif media_type == "preset": self._parent.send('recallpreset "{}"'.format(media_id)) elif media_type == "radiostation": self._parent.send('playradiostation "{}"'.format(media_id)) else: _LOGGER.error("Unexpected media_type='%s'.", media_type)
async def async_play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media from a URL or file.""" # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id) media_type = sourced_media.mime_type media_id = sourced_media.url if media_type != MEDIA_TYPE_MUSIC and not media_type.startswith( "audio/"): raise HomeAssistantError( f"Invalid media type {media_type}. Only {MEDIA_TYPE_MUSIC} is supported" ) # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url( self.hass, media_id, for_supervisor_network=self._using_addon) await self._vlc.add(media_id) self._state = STATE_PLAYING
async def async_play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media from a URL or file, launch an application, or tune to a channel.""" extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} original_media_type: str = media_type original_media_id: str = media_id mime_type: str | None = None stream_name: str | None = None stream_format: str | None = extra.get(ATTR_FORMAT) # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id) media_type = MEDIA_TYPE_URL media_id = sourced_media.url mime_type = sourced_media.mime_type stream_name = original_media_id stream_format = guess_stream_format(media_id, mime_type) # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: media_type = MEDIA_TYPE_VIDEO mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] stream_name = "Camera Stream" stream_format = "hls" if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): parsed = yarl.URL(media_id) if mime_type is None: mime_type, _ = mimetypes.guess_type(parsed.path) if stream_format is None: stream_format = guess_stream_format(media_id, mime_type) if extra.get(ATTR_FORMAT) is None: extra[ATTR_FORMAT] = stream_format if extra[ATTR_FORMAT] not in STREAM_FORMAT_TO_MEDIA_TYPE: _LOGGER.error( "Media type %s is not supported with format %s (mime: %s)", original_media_type, extra[ATTR_FORMAT], mime_type, ) return if (media_type == MEDIA_TYPE_URL and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MEDIA_TYPE_MUSIC): media_type = MEDIA_TYPE_MUSIC if media_type == MEDIA_TYPE_MUSIC and "tts_proxy" in media_id: stream_name = "Text to Speech" elif stream_name is None: if stream_format == "ism": stream_name = parsed.parts[-2] else: stream_name = parsed.name if extra.get(ATTR_NAME) is None: extra[ATTR_NAME] = stream_name if media_type == MEDIA_TYPE_APP: params = { param: extra[attr] for attr, param in ATTRS_TO_LAUNCH_PARAMS.items() if attr in extra } await self.coordinator.roku.launch(media_id, params) elif media_type == MEDIA_TYPE_CHANNEL: await self.coordinator.roku.tune(media_id) elif media_type == MEDIA_TYPE_MUSIC: if extra.get(ATTR_ARTIST_NAME) is None: extra[ATTR_ARTIST_NAME] = "Home Assistant" params = { param: extra[attr] for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS.items() if attr in extra } params = {"t": "a", **params} await self.coordinator.roku.play_on_roku(media_id, params) elif media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): params = { param: extra[attr] for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items() if attr in extra } await self.coordinator.roku.play_on_roku(media_id, params) else: _LOGGER.error("Media type %s is not supported", original_media_type) return await self.coordinator.async_request_refresh()
def play_media( # noqa: C901 self, media_type: str, media_id: str, **kwargs: Any) -> None: """ Send the play_media command to the media player. If media_id is a Plex payload, attempt Plex->Sonos playback. If media_id is an Apple Music, Deezer, Sonos, or Tidal share link, attempt playback using the respective service. If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. """ # Use 'replace' as the default enqueue option enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) media_id = spotify.spotify_uri_from_media_browser_url(media_id) is_radio = False if media_source.is_media_source_id(media_id): is_radio = media_id.startswith("media-source://radio_browser/") media_type = MEDIA_TYPE_MUSIC media_id = (run_coroutine_threadsafe( media_source.async_resolve_media(self.hass, media_id, self.entity_id), self.hass.loop, ).result().url) if media_type == "favorite_item_id": favorite = self.speaker.favorites.lookup_by_item_id(media_id) if favorite is None: raise ValueError(f"Missing favorite for media_id: {media_id}") self._play_favorite(favorite) return soco = self.coordinator.soco if media_id and media_id.startswith(PLEX_URI_SCHEME): plex_plugin = self.speaker.plex_plugin result = process_plex_payload(self.hass, media_type, media_id, supports_playqueues=False) if result.shuffle: self.set_shuffle(True) if enqueue == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = plex_plugin.add_to_queue(result.media, position=pos) if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() plex_plugin.add_to_queue(result.media) soco.play_from_queue(0) return share_link = self.coordinator.share_link if share_link.is_share_link(media_id): if enqueue == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = share_link.add_share_link_to_queue(media_id, position=pos) if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() share_link.add_share_link_to_queue(media_id) soco.play_from_queue(0) elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) if enqueue == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = soco.add_uri_to_queue(media_id, position=pos) if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) elif enqueue == MediaPlayerEnqueue.REPLACE: soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): item = media_browser.get_media( self.media.library, media_id, media_type) # type: ignore[no-untyped-call] soco.play_uri(item.get_uri()) return try: playlists = soco.get_sonos_playlists() playlist = next(p for p in playlists if p.title == media_id) except StopIteration: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: soco.clear_queue() soco.add_to_queue(playlist) soco.play_from_queue(0) elif media_type in PLAYABLE_MEDIA_TYPES: item = media_browser.get_media( self.media.library, media_id, media_type) # type: ignore[no-untyped-call] if not item: _LOGGER.error('Could not find "%s" in the library', media_id) return soco.play_uri(item.get_uri()) else: _LOGGER.error('Sonos does not support a media type of "%s"', media_type)
class DlnaDmrEntity(MediaPlayerEntity): """Representation of a DLNA DMR device as a HA entity.""" udn: str device_type: str _event_addr: EventListenAddr poll_availability: bool # Last known URL for the device, used when adding this entity to hass to try # to connect before SSDP has rediscovered it, or when SSDP discovery fails. location: str _device_lock: asyncio.Lock # Held when connecting or disconnecting the device _device: DmrDevice | None = None check_available: bool = False _ssdp_connect_failed: bool = False # Track BOOTID in SSDP advertisements for device changes _bootid: int | None = None # DMR devices need polling for track position information. async_update will # determine whether further device polling is required. _attr_should_poll = True def __init__( self, udn: str, device_type: str, name: str, event_port: int, event_callback_url: str | None, poll_availability: bool, location: str, ) -> None: """Initialize DLNA DMR entity.""" self.udn = udn self.device_type = device_type self._attr_name = name self._event_addr = EventListenAddr(None, event_port, event_callback_url) self.poll_availability = poll_availability self.location = location self._device_lock = asyncio.Lock() async def async_added_to_hass(self) -> None: """Handle addition.""" # Update this entity when the associated config entry is modified if self.registry_entry and self.registry_entry.config_entry_id: config_entry = self.hass.config_entries.async_get_entry( self.registry_entry.config_entry_id) assert config_entry is not None self.async_on_remove( config_entry.add_update_listener( self.async_config_update_listener)) # Try to connect to the last known location, but don't worry if not available if not self._device: try: await self._device_connect(self.location) except UpnpError as err: _LOGGER.debug("Couldn't connect immediately: %r", err) # Get SSDP notifications for only this device self.async_on_remove(await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, {"USN": self.usn})) # async_upnp_client.SsdpListener only reports byebye once for each *UDN* # (device name) which often is not the USN (service within the device) # that we're interested in. So also listen for byebye advertisements for # the UDN, which is reported in the _udn field of the combined_headers. self.async_on_remove(await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, { "_udn": self.udn, "NTS": NotificationSubType.SSDP_BYEBYE }, )) async def async_will_remove_from_hass(self) -> None: """Handle removal.""" await self._device_disconnect() async def async_ssdp_callback(self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange) -> None: """Handle notification from SSDP of device state change.""" _LOGGER.debug( "SSDP %s notification of device %s at %s", change, info.ssdp_usn, info.ssdp_location, ) try: bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_BOOTID] bootid: int | None = int(bootid_str, 10) except (KeyError, ValueError): bootid = None if change == ssdp.SsdpChange.UPDATE: # This is an announcement that bootid is about to change if self._bootid is not None and self._bootid == bootid: # Store the new value (because our old value matches) so that we # can ignore subsequent ssdp:alive messages with contextlib.suppress(KeyError, ValueError): next_bootid_str = info.ssdp_headers[ ssdp.ATTR_SSDP_NEXTBOOTID] self._bootid = int(next_bootid_str, 10) # Nothing left to do until ssdp:alive comes through return if self._bootid is not None and self._bootid != bootid: # Device has rebooted # Maybe connection will succeed now self._ssdp_connect_failed = False if self._device: # Drop existing connection and maybe reconnect await self._device_disconnect() self._bootid = bootid if change == ssdp.SsdpChange.BYEBYE: # Device is going away if self._device: # Disconnect from gone device await self._device_disconnect() # Maybe the next alive message will result in a successful connection self._ssdp_connect_failed = False if (change == ssdp.SsdpChange.ALIVE and not self._device and not self._ssdp_connect_failed): assert info.ssdp_location location = info.ssdp_location try: await self._device_connect(location) except UpnpError as err: self._ssdp_connect_failed = True _LOGGER.warning( "Failed connecting to recently alive device at %s: %r", location, err, ) # Device could have been de/re-connected, state probably changed self.async_write_ha_state() async def async_config_update_listener( self, hass: HomeAssistant, entry: config_entries.ConfigEntry) -> None: """Handle options update by modifying self in-place.""" _LOGGER.debug( "Updating: %s with data=%s and options=%s", self.name, entry.data, entry.options, ) self.location = entry.data[CONF_URL] self.poll_availability = entry.options.get(CONF_POLL_AVAILABILITY, False) new_port = entry.options.get(CONF_LISTEN_PORT) or 0 new_callback_url = entry.options.get(CONF_CALLBACK_URL_OVERRIDE) if (new_port == self._event_addr.port and new_callback_url == self._event_addr.callback_url): return # Changes to eventing requires a device reconnect for it to update correctly await self._device_disconnect() # Update _event_addr after disconnecting, to stop the right event listener self._event_addr = self._event_addr._replace( port=new_port, callback_url=new_callback_url) try: await self._device_connect(self.location) except UpnpError as err: _LOGGER.warning("Couldn't (re)connect after config change: %r", err) # Device was de/re-connected, state might have changed self.async_write_ha_state() async def _device_connect(self, location: str) -> None: """Connect to the device now that it's available.""" _LOGGER.debug("Connecting to device at %s", location) async with self._device_lock: if self._device: _LOGGER.debug( "Trying to connect when device already connected") return domain_data = get_domain_data(self.hass) # Connect to the base UPNP device upnp_device = await domain_data.upnp_factory.async_create_device( location) # Create/get event handler that is reachable by the device, using # the connection's local IP to listen only on the relevant interface _, event_ip = await async_get_local_ip(location, self.hass.loop) self._event_addr = self._event_addr._replace(host=event_ip) event_handler = await domain_data.async_get_event_notifier( self._event_addr, self.hass) # Create profile wrapper self._device = DmrDevice(upnp_device, event_handler) self.location = location # Subscribe to event notifications try: self._device.on_event = self._on_event await self._device.async_subscribe_services( auto_resubscribe=True) except UpnpResponseError as err: # Device rejected subscription request. This is OK, variables # will be polled instead. _LOGGER.debug("Device rejected subscription: %r", err) except UpnpError as err: # Don't leave the device half-constructed self._device.on_event = None self._device = None await domain_data.async_release_event_notifier(self._event_addr ) _LOGGER.debug( "Error while subscribing during device connect: %r", err) raise if (not self.registry_entry or not self.registry_entry.config_entry_id or self.registry_entry.device_id): return # Create linked HA DeviceEntry now the information is known. dev_reg = device_registry.async_get(self.hass) device_entry = dev_reg.async_get_or_create( config_entry_id=self.registry_entry.config_entry_id, # Connections are based on the root device's UDN, and the DMR # embedded device's UDN. They may be the same, if the DMR is the # root device. connections={ ( device_registry.CONNECTION_UPNP, self._device.profile_device.root_device.udn, ), (device_registry.CONNECTION_UPNP, self._device.udn), }, identifiers={(DOMAIN, self.unique_id)}, default_manufacturer=self._device.manufacturer, default_model=self._device.model_name, default_name=self._device.name, ) # Update entity registry to link to the device ent_reg = entity_registry.async_get(self.hass) ent_reg.async_get_or_create( self.registry_entry.domain, self.registry_entry.platform, self.unique_id, device_id=device_entry.id, ) async def _device_disconnect(self) -> None: """Destroy connections to the device now that it's not available. Also call when removing this entity from hass to clean up connections. """ async with self._device_lock: if not self._device: _LOGGER.debug("Disconnecting from device that's not connected") return _LOGGER.debug("Disconnecting from %s", self._device.name) self._device.on_event = None old_device = self._device self._device = None await old_device.async_unsubscribe_services() domain_data = get_domain_data(self.hass) await domain_data.async_release_event_notifier(self._event_addr) async def async_update(self) -> None: """Retrieve the latest data.""" if not self._device: if not self.poll_availability: return try: await self._device_connect(self.location) except UpnpError: return assert self._device is not None try: do_ping = self.poll_availability or self.check_available await self._device.async_update(do_ping=do_ping) except UpnpError as err: _LOGGER.debug("Device unavailable: %r", err) await self._device_disconnect() return finally: self.check_available = False def _on_event(self, service: UpnpService, state_variables: Sequence[UpnpStateVariable]) -> None: """State variable(s) changed, let home-assistant know.""" if not state_variables: # Indicates a failure to resubscribe, check if device is still available self.check_available = True force_refresh = False if service.service_id == "urn:upnp-org:serviceId:AVTransport": for state_variable in state_variables: # Force a state refresh when player begins or pauses playback # to update the position info. if (state_variable.name == "TransportState" and state_variable.value in (TransportState.PLAYING, TransportState.PAUSED_PLAYBACK)): force_refresh = True self.async_schedule_update_ha_state(force_refresh) @property def available(self) -> bool: """Device is available when we have a connection to it.""" return self._device is not None and self._device.profile_device.available @property def unique_id(self) -> str: """Report the UDN (Unique Device Name) as this entity's unique ID.""" return self.udn @property def usn(self) -> str: """Get the USN based on the UDN (Unique Device Name) and device type.""" return f"{self.udn}::{self.device_type}" @property def state(self) -> str | None: """State of the player.""" if not self._device or not self.available: return STATE_OFF if self._device.transport_state is None: return STATE_ON if self._device.transport_state in ( TransportState.PLAYING, TransportState.TRANSITIONING, ): return STATE_PLAYING if self._device.transport_state in ( TransportState.PAUSED_PLAYBACK, TransportState.PAUSED_RECORDING, ): return STATE_PAUSED if self._device.transport_state == TransportState.VENDOR_DEFINED: # Unable to map this state to anything reasonable, so it's "Unknown" return None return STATE_IDLE @property def supported_features(self) -> int: """Flag media player features that are supported at this moment. Supported features may change as the device enters different states. """ if not self._device: return 0 supported_features = 0 if self._device.has_volume_level: supported_features |= MediaPlayerEntityFeature.VOLUME_SET if self._device.has_volume_mute: supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE if self._device.can_play: supported_features |= MediaPlayerEntityFeature.PLAY if self._device.can_pause: supported_features |= MediaPlayerEntityFeature.PAUSE if self._device.can_stop: supported_features |= MediaPlayerEntityFeature.STOP if self._device.can_previous: supported_features |= MediaPlayerEntityFeature.PREVIOUS_TRACK if self._device.can_next: supported_features |= MediaPlayerEntityFeature.NEXT_TRACK if self._device.has_play_media: supported_features |= (MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA) if self._device.can_seek_rel_time: supported_features |= MediaPlayerEntityFeature.SEEK play_modes = self._device.valid_play_modes if play_modes & {PlayMode.RANDOM, PlayMode.SHUFFLE}: supported_features |= MediaPlayerEntityFeature.SHUFFLE_SET if play_modes & {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL}: supported_features |= MediaPlayerEntityFeature.REPEAT_SET if self._device.has_presets: supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE return supported_features @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if not self._device or not self._device.has_volume_level: return None return self._device.volume_level @catch_request_errors async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" assert self._device is not None await self._device.async_set_volume_level(volume) @property def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" if not self._device: return None return self._device.is_volume_muted @catch_request_errors async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" assert self._device is not None desired_mute = bool(mute) await self._device.async_mute_volume(desired_mute) @catch_request_errors async def async_media_pause(self) -> None: """Send pause command.""" assert self._device is not None await self._device.async_pause() @catch_request_errors async def async_media_play(self) -> None: """Send play command.""" assert self._device is not None await self._device.async_play() @catch_request_errors async def async_media_stop(self) -> None: """Send stop command.""" assert self._device is not None await self._device.async_stop() @catch_request_errors async def async_media_seek(self, position: int | float) -> None: """Send seek command.""" assert self._device is not None time = timedelta(seconds=position) await self._device.async_seek_rel_time(time) @catch_request_errors async def async_play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs) assert self._device is not None didl_metadata: str | None = None title: str = "" # If media is media_source, resolve it to url and MIME type, and maybe metadata if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id) media_type = sourced_media.mime_type media_id = sourced_media.url _LOGGER.debug("sourced_media is %s", sourced_media) if sourced_metadata := getattr(sourced_media, "didl_metadata", None): didl_metadata = didl_lite.to_xml_string( sourced_metadata).decode("utf-8") title = sourced_metadata.title # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} metadata: dict[str, Any] = extra.get("metadata") or {} if not title: title = extra.get("title") or metadata.get( "title") or "Home Assistant" if thumb := extra.get("thumb"): metadata["album_art_uri"] = thumb
async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id) media_type = sourced_media.mime_type media_id = sourced_media.url extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) metadata = extra.get("metadata") # Handle media supported by a known cast app if media_type == CAST_DOMAIN: try: app_data = json.loads(media_id) if metadata is not None: app_data["metadata"] = extra.get("metadata") except json.JSONDecodeError: _LOGGER.error("Invalid JSON in media_content_id") raise # Special handling for passed `app_id` parameter. This will only launch # an arbitrary cast app, generally for UX. if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) await self.hass.async_add_executor_job( self._chromecast.start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", app_data.keys(), ) return app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job(quick_play, self._chromecast, app_name, app_data) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) return # Try the cast platforms for platform in self.hass.data[CAST_DOMAIN].values(): result = await platform.async_play_media(self.hass, self.entity_id, self._chromecast, media_type, media_id) if result: return # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) # Configure play command for when playing a HLS stream if is_hass_url(self.hass, media_id): parsed = yarl.URL(media_id) if parsed.path.startswith("/api/hls/"): extra = { **extra, "stream_type": "LIVE", "media_info": { "hlsVideoSegmentFormat": "fmp4", }, } # Default to play with the default media receiver app_data = {"media_id": media_id, "media_type": media_type, **extra} await self.hass.async_add_executor_job(quick_play, self._chromecast, "default_media_receiver", app_data)
def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """ Send the play_media command to the media player. If media_id is a Plex payload, attempt Plex->Sonos playback. If media_id is an Apple Music, Deezer, Sonos, or Tidal share link, attempt playback using the respective service. If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) media_id = spotify.spotify_uri_from_media_browser_url(media_id) is_radio = False if media_source.is_media_source_id(media_id): is_radio = media_id.startswith("media-source://radio_browser/") media_type = MEDIA_TYPE_MUSIC media_id = (run_coroutine_threadsafe( media_source.async_resolve_media(self.hass, media_id), self.hass.loop, ).result().url) if media_type == "favorite_item_id": favorite = self.speaker.favorites.lookup_by_item_id(media_id) if favorite is None: raise ValueError(f"Missing favorite for media_id: {media_id}") self._play_favorite(favorite) return soco = self.coordinator.soco if media_id and media_id.startswith(PLEX_URI_SCHEME): plex_plugin = self.speaker.plex_plugin media_id = media_id[len(PLEX_URI_SCHEME):] payload = json.loads(media_id) if isinstance(payload, dict): shuffle = payload.pop("shuffle", False) else: shuffle = False media = lookup_plex_media(self.hass, media_type, json.dumps(payload)) if not kwargs.get(ATTR_MEDIA_ENQUEUE): soco.clear_queue() if shuffle: self.set_shuffle(True) plex_plugin.play_now(media) return share_link = self.coordinator.share_link if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE): share_link.add_share_link_to_queue(media_id) else: soco.clear_queue() share_link.add_share_link_to_queue(media_id) soco.play_from_queue(0) elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) if kwargs.get(ATTR_MEDIA_ENQUEUE): soco.add_uri_to_queue(media_id) else: soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): item = media_browser.get_media( self.media.library, media_id, media_type) # type: ignore[no-untyped-call] soco.play_uri(item.get_uri()) return try: playlists = soco.get_sonos_playlists() playlist = next(p for p in playlists if p.title == media_id) except StopIteration: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: soco.clear_queue() soco.add_to_queue(playlist) soco.play_from_queue(0) elif media_type in PLAYABLE_MEDIA_TYPES: item = media_browser.get_media( self.media.library, media_id, media_type) # type: ignore[no-untyped-call] if not item: _LOGGER.error('Could not find "%s" in the library', media_id) return soco.play_uri(item.get_uri()) else: _LOGGER.error('Sonos does not support a media type of "%s"', media_type)
async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" chromecast = self._get_chromecast() # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_type = sourced_media.mime_type media_id = sourced_media.url extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) # Handle media supported by a known cast app if media_type == CAST_DOMAIN: try: app_data = json.loads(media_id) if metadata := extra.get("metadata"): app_data["metadata"] = metadata except json.JSONDecodeError: _LOGGER.error("Invalid JSON in media_content_id") raise # Special handling for passed `app_id` parameter. This will only launch # an arbitrary cast app, generally for UX. if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) await self.hass.async_add_executor_job(chromecast.start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", app_data.keys(), ) return app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job( quick_play, chromecast, app_name, app_data ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) return # Try the cast platforms for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( self.hass, self.entity_id, chromecast, media_type, media_id ) if result: return # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) # Configure play command for when playing a HLS stream if is_hass_url(self.hass, media_id): parsed = yarl.URL(media_id) if parsed.path.startswith("/api/hls/"): extra = { **extra, "stream_type": "LIVE", "media_info": { "hlsVideoSegmentFormat": "fmp4", }, } elif ( media_id.endswith(".m3u") or media_id.endswith(".m3u8") or media_id.endswith(".pls") ): try: playlist = await parse_playlist(self.hass, media_id) _LOGGER.debug( "[%s %s] Playing item %s from playlist %s", self.entity_id, self._cast_info.friendly_name, playlist[0].url, media_id, ) media_id = playlist[0].url if title := playlist[0].title: extra = { **extra, "metadata": {"title": title}, } except PlaylistSupported as err: _LOGGER.debug( "[%s %s] Playlist %s is supported: %s", self.entity_id, self._cast_info.friendly_name, media_id, err, ) except PlaylistError as err: _LOGGER.warning( "[%s %s] Failed to parse playlist %s: %s", self.entity_id, self._cast_info.friendly_name, media_id, err, ) # Default to play with the default media receiver app_data = {"media_id": media_id, "media_type": media_type, **extra} _LOGGER.debug( "[%s %s] Playing %s with default_media_receiver", self.entity_id, self._cast_info.friendly_name, app_data, ) await self.hass.async_add_executor_job( quick_play, chromecast, "default_media_receiver", app_data )