1 """Support to interface with the Plex API."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from functools
import wraps
8 from typing
import Any, Concatenate, cast
10 import plexapi.exceptions
11 import requests.exceptions
17 MediaPlayerEntityFeature,
27 async_dispatcher_connect,
28 async_dispatcher_send,
35 CONF_SERVER_IDENTIFIER,
40 PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL,
41 PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
42 PLEX_UPDATE_SENSOR_SIGNAL,
43 TRANSIENT_DEVICE_MODELS,
45 from .helpers
import get_plex_data, get_plex_server
46 from .media_browser
import browse_media
47 from .services
import process_plex_payload
49 _LOGGER = logging.getLogger(__name__)
52 def needs_session[_PlexMediaPlayerT: PlexMediaPlayer, **_P, _R](
53 func: Callable[Concatenate[_PlexMediaPlayerT, _P], _R],
54 ) -> Callable[Concatenate[_PlexMediaPlayerT, _P], _R |
None]:
55 """Ensure session is available for certain attributes."""
58 def get_session_attribute(
59 self: _PlexMediaPlayerT, *args: _P.args, **kwargs: _P.kwargs
61 if self.session
is None:
63 return func(self, *args, **kwargs)
65 return get_session_attribute
70 config_entry: ConfigEntry,
71 async_add_entities: AddEntitiesCallback,
73 """Set up Plex media_player from a config entry."""
74 server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
75 registry = er.async_get(hass)
78 def async_new_media_players(new_entities):
82 hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players
85 _LOGGER.debug(
"New entity listener created")
90 """Set up Plex media_player entities."""
91 _LOGGER.debug(
"New entities: %s", new_entities)
94 for entity_params
in new_entities:
96 entities.append(plex_mp)
99 old_entity_id = registry.async_get_entity_id(
100 MP_DOMAIN, DOMAIN, plex_mp.machine_identifier
102 if old_entity_id
is not None:
103 new_unique_id = f
"{server_id}:{plex_mp.machine_identifier}"
105 "Migrating unique_id from [%s] to [%s]",
106 plex_mp.machine_identifier,
109 registry.async_update_entity(old_entity_id, new_unique_id=new_unique_id)
115 """Representation of a Plex device."""
117 _attr_available =
False
118 _attr_should_poll =
False
119 _attr_state = MediaPlayerState.IDLE
121 def __init__(self, plex_server, device, player_source, session=None):
122 """Initialize the Plex device."""
141 f
"{self.plex_server.machine_identifier}:{self.machine_identifier}"
148 """Run when about to be added to hass."""
153 PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.
unique_idunique_id),
161 PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(self.
unique_idunique_id),
168 """Set instance objects and trigger an entity state update."""
169 _LOGGER.debug(
"Refreshing %s [%s / %s]", self.
entity_identity_id, device, session)
170 self.
devicedevice = device
178 PLEX_UPDATE_SENSOR_SIGNAL.format(self.
plex_serverplex_server.machine_identifier),
183 """Update the entity based on new websocket data."""
189 PLEX_UPDATE_SENSOR_SIGNAL.format(self.
plex_serverplex_server.machine_identifier),
193 """Refresh key device data."""
204 except plexapi.exceptions.BadRequest:
205 device_url =
"127.0.0.1"
206 if "127.0.0.1" in device_url:
207 self.
devicedevice.proxyThroughServer()
223 name_parts.insert(0, self.
usernameusername)
224 self.
_attr_name_attr_name = NAME_FORMAT.format(
" - ".join(name_parts))
227 """Force client to idle."""
228 self.
_attr_state_attr_state = MediaPlayerState.IDLE
236 """Return the active session for this player."""
246 self.
_attr_state_attr_state = MediaPlayerState.IDLE
251 """Return the username of the client owner."""
255 """Set the state of the device, handle session termination."""
256 if state ==
"playing":
257 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
258 elif state ==
"paused":
259 self.
_attr_state_attr_state = MediaPlayerState.PAUSED
260 elif state ==
"stopped":
264 self.
_attr_state_attr_state = MediaPlayerState.IDLE
268 """Report if the client is playing media."""
269 return self.
statestatestatestate
in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}
273 """Get the active media type required by PlexAPI commands."""
282 """Return current session key."""
288 """Return the library name of playing media."""
294 """Return the content ID of current playing media."""
300 """Return the content type of current playing media."""
306 """Return the content rating of current playing media."""
312 """Return the artist of current playing media, music track only."""
318 """Return the album name of current playing media, music track only."""
324 """Return the album artist of current playing media, music only."""
330 """Return the track number of current playing media, music only."""
336 """Return the duration of current playing media in seconds."""
342 """Return the duration of current playing media in seconds."""
348 """When was the position of the current playing media valid."""
354 """Return the image URL of current playing media."""
360 """Return the summary of current playing media."""
366 """Return the title of current playing media."""
372 """Return the season of current playing media (TV Show only)."""
378 """Return the title of the series of current playing media."""
384 """Return the episode of current playing media (TV Show only)."""
389 """Flag media player features that are supported."""
392 MediaPlayerEntityFeature.PAUSE
393 | MediaPlayerEntityFeature.PREVIOUS_TRACK
394 | MediaPlayerEntityFeature.NEXT_TRACK
395 | MediaPlayerEntityFeature.STOP
396 | MediaPlayerEntityFeature.SEEK
397 | MediaPlayerEntityFeature.VOLUME_SET
398 | MediaPlayerEntityFeature.PLAY
399 | MediaPlayerEntityFeature.PLAY_MEDIA
400 | MediaPlayerEntityFeature.VOLUME_MUTE
401 | MediaPlayerEntityFeature.BROWSE_MEDIA
405 MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
409 """Set volume level, range 0..1."""
416 """Return the volume level of the client (0..1)."""
427 """Return boolean if volume is currently muted."""
435 Since we can't actually mute, we'll:
436 - On mute, store volume and set volume to 0
437 - On unmute, set volume to previously stored volume
450 """Send play command."""
455 """Send pause command."""
460 """Send stop command."""
465 """Send the seek command."""
470 """Send next track command."""
475 """Send previous track command."""
480 self, media_type: MediaType | str, media_id: str, **kwargs: Any
482 """Play a piece of media."""
485 f
"Client is not currently accepting playback controls: {self.name}"
489 self.
hasshass, media_type, media_id, default_plex_server=self.
plex_serverplex_server
491 _LOGGER.debug(
"Attempting to play %s on %s", result.media, self.
namename)
494 self.
devicedevice.playMedia(result.media, offset=result.offset)
495 except requests.exceptions.ConnectTimeout
as exc:
497 f
"Request failed when playing on {self.name}"
502 """Return the scene state attributes."""
505 "media_content_rating",
506 "media_library_title",
511 if value := getattr(self, attr,
None):
512 attributes[attr] = value
518 """Return a device description for device registry."""
524 identifiers={(DOMAIN,
"plex.tv-clients")},
525 name=
"Plex Client Service",
527 model=
"Plex Clients",
528 entry_type=DeviceEntryType.SERVICE,
538 name=cast(str |
None, self.
namename),
540 via_device=(DOMAIN, self.
plex_serverplex_server.machine_identifier),
545 media_content_type: MediaType | str |
None =
None,
546 media_content_id: str |
None =
None,
548 """Implement the websocket media browsing helper."""
550 return await self.
hasshass.async_add_executor_job(
None async_schedule_update_ha_state(self, bool force_refresh=False)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
str|UndefinedType|None name(self)
PlexData get_plex_data(HomeAssistant hass)
PlexServer get_plex_server(HomeAssistant hass, str server_id)
PlexMediaSearchResult process_plex_payload(HomeAssistant hass, str content_type, str content_id, PlexServer|None default_plex_server=None, bool supports_playqueues=True)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
bool is_internal_request(HomeAssistant hass)