Central update for Plex platforms (#27764)

* Update Plex platforms together

* Remove unnecessary methods

* Overhaul of Plex update logic

* Apply suggestions from code review

Use set instead of list

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Review suggestions and cleanup

* Fixes, remove sensor throttle

* Guarantee entity name, use common scheme

* Keep name stable once set
This commit is contained in:
jjlawren 2019-10-19 16:31:15 -05:00 committed by Martin Hjelmare
parent 5c50fa3405
commit eeb1bfc6f5
6 changed files with 252 additions and 266 deletions

View file

@ -1,5 +1,4 @@
"""Support to interface with the Plex API."""
from datetime import timedelta
import json
import logging
from xml.etree.ElementTree import ParseError
@ -29,14 +28,17 @@ from homeassistant.const import (
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.helpers.event import track_time_interval
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
from .const import (
CONF_SERVER_IDENTIFIER,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
REFRESH_LISTENERS,
PLEX_NEW_MP_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
SERVERS,
)
@ -53,142 +55,53 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex media_player from a config entry."""
def add_entities(entities, update_before_add=False):
"""Sync version of async add entities."""
hass.add_job(async_add_entities, entities, update_before_add)
hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities)
def _setup_platform(hass, config_entry, add_entities_callback):
"""Set up the Plex media_player platform."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
def async_new_media_players(new_entities):
_async_add_entities(
hass, config_entry, async_add_entities, server_id, new_entities
)
unsub = async_dispatcher_connect(hass, PLEX_NEW_MP_SIGNAL, async_new_media_players)
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
def _async_add_entities(
hass, config_entry, async_add_entities, server_id, new_entities
):
"""Set up Plex media_player entities."""
entities = []
plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
plex_clients = {}
plex_sessions = {}
hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval(
hass, lambda now: update_devices(), timedelta(seconds=10)
)
for entity_params in new_entities:
plex_mp = PlexMediaPlayer(plexserver, **entity_params)
entities.append(plex_mp)
def update_devices():
"""Update the devices objects."""
try:
devices = plexserver.clients()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex devices")
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Could not connect to Plex server: %s (%s)",
plexserver.friendly_name,
ex,
)
return
new_plex_clients = []
available_client_ids = []
for device in devices:
# For now, let's allow all deviceClass types
if device.deviceClass in ["badClient"]:
continue
available_client_ids.append(device.machineIdentifier)
if device.machineIdentifier not in plex_clients:
new_client = PlexClient(
plexserver, device, None, plex_sessions, update_devices
)
plex_clients[device.machineIdentifier] = new_client
_LOGGER.debug("New device: %s", device.machineIdentifier)
new_plex_clients.append(new_client)
else:
_LOGGER.debug("Refreshing device: %s", device.machineIdentifier)
plex_clients[device.machineIdentifier].refresh(device, None)
# add devices with a session and no client (ex. PlexConnect Apple TV's)
try:
sessions = plexserver.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex sessions")
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Could not connect to Plex server: %s (%s)",
plexserver.friendly_name,
ex,
)
return
plex_sessions.clear()
for session in sessions:
for player in session.players:
plex_sessions[player.machineIdentifier] = session, player
for machine_identifier, (session, player) in plex_sessions.items():
if machine_identifier in available_client_ids:
# Avoid using session if already added as a device.
_LOGGER.debug("Skipping session, device exists: %s", machine_identifier)
continue
if (
machine_identifier not in plex_clients
and machine_identifier is not None
):
new_client = PlexClient(
plexserver, player, session, plex_sessions, update_devices
)
plex_clients[machine_identifier] = new_client
_LOGGER.debug("New session: %s", machine_identifier)
new_plex_clients.append(new_client)
else:
_LOGGER.debug("Refreshing session: %s", machine_identifier)
plex_clients[machine_identifier].refresh(None, session)
for client in plex_clients.values():
# force devices to idle that do not have a valid session
if client.session is None:
client.force_idle()
client.set_availability(
client.machine_identifier in available_client_ids
or client.machine_identifier in plex_sessions
)
if client not in new_plex_clients:
client.schedule_update_ha_state()
if new_plex_clients:
add_entities_callback(new_plex_clients)
async_add_entities(entities, True)
class PlexClient(MediaPlayerDevice):
class PlexMediaPlayer(MediaPlayerDevice):
"""Representation of a Plex device."""
def __init__(self, plex_server, device, session, plex_sessions, update_devices):
def __init__(self, plex_server, device, session=None):
"""Initialize the Plex device."""
self.plex_server = plex_server
self.device = device
self.session = session
self._app_name = ""
self._device = None
self._available = False
self._marked_unavailable = None
self._device_protocol_capabilities = None
self._is_player_active = False
self._is_player_available = False
self._player = None
self._machine_identifier = None
self._machine_identifier = device.machineIdentifier
self._make = ""
self._name = None
self._player_state = "idle"
self._previous_volume_level = 1 # Used in fake muting
self._session = None
self._session_type = None
self._session_username = None
self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # since we can't retrieve remotely
self.plex_server = plex_server
self.plex_sessions = plex_sessions
self.update_devices = update_devices
# General
self._media_content_id = None
self._media_content_rating = None
@ -208,7 +121,22 @@ class PlexClient(MediaPlayerDevice):
self._media_season = None
self._media_series_title = None
self.refresh(device, session)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
server_id = self.plex_server.machine_identifier
unsub = async_dispatcher_connect(
self.hass,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id),
self.async_refresh_media_player,
)
self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
def async_refresh_media_player(self, device, session):
"""Set instance objects and trigger an entity state update."""
self.device = device
self.session = session
self.async_schedule_update_ha_state(True)
def _clear_media_details(self):
"""Set all Media Items to None."""
@ -232,52 +160,46 @@ class PlexClient(MediaPlayerDevice):
# Clear library Name
self._app_name = ""
def refresh(self, device, session):
def update(self):
"""Refresh key device data."""
self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App
self._session = session
if device:
self._device = device
self._available = self.device or self.session
name_base = None
if self.device:
try:
device_url = self._device.url("/")
device_url = self.device.url("/")
except plexapi.exceptions.BadRequest:
device_url = "127.0.0.1"
if "127.0.0.1" in device_url:
self._device.proxyThroughServer()
self._session = None
self._machine_identifier = self._device.machineIdentifier
self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME)
self._device_protocol_capabilities = self._device.protocolCapabilities
self.device.proxyThroughServer()
name_base = self.device.title or self.device.product
self._device_protocol_capabilities = self.device.protocolCapabilities
self._player_state = self.device.state
# set valid session, preferring device session
if self._device.machineIdentifier in self.plex_sessions:
self._session = self.plex_sessions.get(
self._device.machineIdentifier, [None, None]
)[0]
if self._session:
if (
self._device is not None
and self._device.machineIdentifier is not None
and self._session.players
):
self._is_player_available = True
self._player = [
if not self.session:
self.force_idle()
else:
session_device = next(
(
p
for p in self._session.players
if p.machineIdentifier == self._device.machineIdentifier
][0]
self._name = NAME_FORMAT.format(self._player.title)
self._player_state = self._player.state
self._session_username = self._session.usernames[0]
self._make = self._player.device
for p in self.session.players
if p.machineIdentifier == self.device.machineIdentifier
),
None,
)
if session_device:
self._make = session_device.device or ""
self._player_state = session_device.state
name_base = name_base or session_device.title or session_device.product
else:
self._is_player_available = False
_LOGGER.warning("No player associated with active session")
self._session_username = self.session.usernames[0]
# Calculate throttled position for proper progress display.
position = int(self._session.viewOffset / 1000)
position = int(self.session.viewOffset / 1000)
now = dt_util.utcnow()
if self._media_position is not None:
pos_diff = position - self._media_position
@ -289,21 +211,22 @@ class PlexClient(MediaPlayerDevice):
self._media_position_updated_at = now
self._media_position = position
self._media_content_id = self._session.ratingKey
self._media_content_rating = getattr(self._session, "contentRating", None)
self._media_content_id = self.session.ratingKey
self._media_content_rating = getattr(self.session, "contentRating", None)
self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME)
self._set_player_state()
if self._is_player_active and self._session is not None:
self._session_type = self._session.type
self._media_duration = int(self._session.duration / 1000)
if self._is_player_active and self.session is not None:
self._session_type = self.session.type
self._media_duration = int(self.session.duration / 1000)
# title (movie name, tv episode name, music song name)
self._media_title = self._session.title
self._media_title = self.session.title
# media type
self._set_media_type()
self._app_name = (
self._session.section().title
if self._session.section() is not None
self.session.section().title
if self.session.section() is not None
else ""
)
self._set_media_image()
@ -311,33 +234,21 @@ class PlexClient(MediaPlayerDevice):
self._session_type = None
def _set_media_image(self):
thumb_url = self._session.thumbUrl
thumb_url = self.session.thumbUrl
if (
self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.plex_server.use_episode_art
):
thumb_url = self._session.url(self._session.grandparentThumb)
thumb_url = self.session.url(self.session.grandparentThumb)
if thumb_url is None:
_LOGGER.debug(
"Using media art because media thumb " "was not found: %s",
self.entity_id,
"Using media art because media thumb was not found: %s", self.name
)
thumb_url = self.session.url(self._session.art)
thumb_url = self.session.url(self.session.art)
self._media_image_url = thumb_url
def set_availability(self, available):
"""Set the device as available/unavailable noting time."""
if not available:
self._clear_media_details()
if self._marked_unavailable is None:
self._marked_unavailable = dt_util.utcnow()
else:
self._marked_unavailable = None
self._available = available
def _set_player_state(self):
if self._player_state == "playing":
self._is_player_active = True
@ -357,41 +268,41 @@ class PlexClient(MediaPlayerDevice):
self._media_content_type = MEDIA_TYPE_TVSHOW
# season number (00)
if callable(self._session.season):
self._media_season = str((self._session.season()).index).zfill(2)
elif self._session.parentIndex is not None:
self._media_season = self._session.parentIndex.zfill(2)
if callable(self.session.season):
self._media_season = str((self.session.season()).index).zfill(2)
elif self.session.parentIndex is not None:
self._media_season = self.session.parentIndex.zfill(2)
else:
self._media_season = None
# show name
self._media_series_title = self._session.grandparentTitle
self._media_series_title = self.session.grandparentTitle
# episode number (00)
if self._session.index is not None:
self._media_episode = str(self._session.index).zfill(2)
if self.session.index is not None:
self._media_episode = str(self.session.index).zfill(2)
elif self._session_type == "movie":
self._media_content_type = MEDIA_TYPE_MOVIE
if self._session.year is not None and self._media_title is not None:
self._media_title += " (" + str(self._session.year) + ")"
if self.session.year is not None and self._media_title is not None:
self._media_title += " (" + str(self.session.year) + ")"
elif self._session_type == "track":
self._media_content_type = MEDIA_TYPE_MUSIC
self._media_album_name = self._session.parentTitle
self._media_album_artist = self._session.grandparentTitle
self._media_track = self._session.index
self._media_artist = self._session.originalTitle
self._media_album_name = self.session.parentTitle
self._media_album_artist = self.session.grandparentTitle
self._media_track = self.session.index
self._media_artist = self.session.originalTitle
# use album artist if track artist is missing
if self._media_artist is None:
_LOGGER.debug(
"Using album artist because track artist " "was not found: %s",
self.entity_id,
"Using album artist because track artist was not found: %s",
self.name,
)
self._media_artist = self._media_album_artist
def force_idle(self):
"""Force client to idle."""
self._state = STATE_IDLE
self._session = None
self.session = None
self._clear_media_details()
@property
@ -402,7 +313,7 @@ class PlexClient(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this plex client."""
return self.machine_identifier
return self._machine_identifier
@property
def available(self):
@ -414,31 +325,11 @@ class PlexClient(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
@property
def machine_identifier(self):
"""Return the machine identifier of the device."""
return self._machine_identifier
@property
def app_name(self):
"""Return the library name of playing media."""
return self._app_name
@property
def device(self):
"""Return the device, if any."""
return self._device
@property
def marked_unavailable(self):
"""Return time device was marked unavailable."""
return self._marked_unavailable
@property
def session(self):
"""Return the session, if any."""
return self._session
@property
def state(self):
"""Return the state of the device."""
@ -462,8 +353,7 @@ class PlexClient(MediaPlayerDevice):
"""Return the content type of current playing media."""
if self._session_type == "clip":
_LOGGER.debug(
"Clip content type detected, " "compatibility may vary: %s",
self.entity_id,
"Clip content type detected, compatibility may vary: %s", self.name
)
return MEDIA_TYPE_TVSHOW
if self._session_type == "episode":
@ -560,8 +450,8 @@ class PlexClient(MediaPlayerDevice):
# no mute support
if self.make.lower() == "shield android tv":
_LOGGER.debug(
"Shield Android TV client detected, disabling mute " "controls: %s",
self.entity_id,
"Shield Android TV client detected, disabling mute controls: %s",
self.name,
)
return (
SUPPORT_PAUSE
@ -579,7 +469,7 @@ class PlexClient(MediaPlayerDevice):
_LOGGER.debug(
"Tivo client detected, only enabling pause, play, "
"stop, and off controls: %s",
self.entity_id,
self.name,
)
return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF
@ -603,7 +493,7 @@ class PlexClient(MediaPlayerDevice):
if self.device and "playback" in self._device_protocol_capabilities:
self.device.setVolume(int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve
self.update_devices()
self.plex_server.update_platforms()
@property
def volume_level(self):
@ -642,19 +532,19 @@ class PlexClient(MediaPlayerDevice):
"""Send play command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.play(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def media_pause(self):
"""Send pause command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.pause(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def media_stop(self):
"""Send stop command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.stop(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def turn_off(self):
"""Turn the client off."""
@ -665,13 +555,13 @@ class PlexClient(MediaPlayerDevice):
"""Send next track command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.skipNext(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def media_previous_track(self):
"""Send previous track command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.skipPrevious(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
@ -706,7 +596,7 @@ class PlexClient(MediaPlayerDevice):
except requests.exceptions.ConnectTimeout:
_LOGGER.error("Timed out playing on %s", self.name)
self.update_devices()
self.plex_server.update_platforms()
def _get_music_media(self, library_name, src):
"""Find music media and return a Plex media object."""