Plex refactor Part 1 - Update plexapi to 3.0.3 (#9988)

* Fix: Last Played Media Title in plex would stay even when player was idle/off
     Primary Fix is in the "if self._device" portion.
     code in "if self._session" is a catch all but i'm not 100% if it is needed.

* Fixed lint issues with previous commit

* 1st Pass at refactoring plex refresh
Moved _media** into clearMedia() which is called in _init_ and
at start of refresh.

Removed redunant _media_* = None entries

Grouped TV Show and Music under single if rather than testing
seperately for now.

* Fixed invalid name for _clearMedia()
Removed another media_* = None entry

* Removed print() statements used for debug

* Removed unneeded "if" statement

* Changes
* Updated Requests to 2.18.4
* Updated plexapi to 3.0.3
* Removed function to convert_NA_to_None
* Removed function get_thumb_url

Type changes
* _session.player is now a list players
* na_type deprecated and to be removed
* plexapi has change na to None

Known Issues:
* Player controls currently broken
* Last location (library) stays while player idle

* Username is now Usernames and a list

* Fix for broken controls

* Removed errant print statement

* Removed depecrated na_type

* Updated Plex Sensor to use plexapi 3.0.3
Added support for Token to be used in sensor

Known Issues:
Username and Password broken for Plex Sensor use Token instead for now

* removed TODOs

* Fixes for private access violations

* Removed need for _local_client_fix

* Removed unused import and fixed parens
This commit is contained in:
Ryan McLean 2017-10-25 10:42:13 +01:00 committed by Pascal Vizeli
parent 2a2a106e62
commit 41fa8cc8f2
3 changed files with 64 additions and 132 deletions

View File

@ -8,7 +8,6 @@ import json
import logging import logging
import os import os
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse
import requests import requests
import voluptuous as vol import voluptuous as vol
@ -24,7 +23,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['plexapi==2.0.2'] REQUIREMENTS = ['plexapi==3.0.3']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -183,7 +182,7 @@ def setup_plexserver(
if device.machineIdentifier not in plex_clients: if device.machineIdentifier not in plex_clients:
new_client = PlexClient(config, device, None, new_client = PlexClient(config, device, None,
plex_sessions, update_devices, plex_sessions, update_devices,
update_sessions) update_sessions, plexserver)
plex_clients[device.machineIdentifier] = new_client plex_clients[device.machineIdentifier] = new_client
new_plex_clients.append(new_client) new_plex_clients.append(new_client)
else: else:
@ -196,7 +195,7 @@ def setup_plexserver(
and machine_identifier is not None): and machine_identifier is not None):
new_client = PlexClient(config, None, session, new_client = PlexClient(config, None, session,
plex_sessions, update_devices, plex_sessions, update_devices,
update_sessions) update_sessions, plexserver)
plex_clients[machine_identifier] = new_client plex_clients[machine_identifier] = new_client
new_plex_clients.append(new_client) new_plex_clients.append(new_client)
else: else:
@ -225,9 +224,8 @@ def setup_plexserver(
plex_sessions.clear() plex_sessions.clear()
for session in sessions: for session in sessions:
if (session.player is not None and for player in session.players:
session.player.machineIdentifier is not None): plex_sessions[player.machineIdentifier] = session
plex_sessions[session.player.machineIdentifier] = session
update_sessions() update_sessions()
update_devices() update_devices()
@ -277,14 +275,15 @@ class PlexClient(MediaPlayerDevice):
"""Representation of a Plex device.""" """Representation of a Plex device."""
def __init__(self, config, device, session, plex_sessions, def __init__(self, config, device, session, plex_sessions,
update_devices, update_sessions): update_devices, update_sessions, plex_server):
"""Initialize the Plex device.""" """Initialize the Plex device."""
from plexapi.utils import NA
self._app_name = '' self._app_name = ''
self._server = plex_server
self._device = None self._device = None
self._device_protocol_capabilities = None self._device_protocol_capabilities = None
self._is_player_active = False self._is_player_active = False
self._is_player_available = False self._is_player_available = False
self._player = None
self._machine_identifier = None self._machine_identifier = None
self._make = '' self._make = ''
self._name = None self._name = None
@ -296,7 +295,6 @@ class PlexClient(MediaPlayerDevice):
self._state = STATE_IDLE self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely
self.na_type = NA
self.config = config self.config = config
self.plex_sessions = plex_sessions self.plex_sessions = plex_sessions
self.update_devices = update_devices self.update_devices = update_devices
@ -353,42 +351,35 @@ class PlexClient(MediaPlayerDevice):
self._session = session self._session = session
if device: if device:
self._device = device self._device = device
if "127.0.0.1" in self._device.url("/"):
self._device.proxyThroughServer()
self._session = None self._session = None
self._machine_identifier = self._device.machineIdentifier
if self._device: self._name = self._device.title or DEVICE_DEFAULT_NAME
self._machine_identifier = self._convert_na_to_none(
self._device.machineIdentifier)
self._name = self._convert_na_to_none(
self._device.title) or DEVICE_DEFAULT_NAME
self._device_protocol_capabilities = ( self._device_protocol_capabilities = (
self._device.protocolCapabilities) self._device.protocolCapabilities)
# set valid session, preferring device session # set valid session, preferring device session
if self._device and self.plex_sessions.get( if self.plex_sessions.get(self._device.machineIdentifier, None):
self._device.machineIdentifier, None): self._session = self.plex_sessions.get(
self._session = self._convert_na_to_none(self.plex_sessions.get( self._device.machineIdentifier, None)
self._device.machineIdentifier, None))
if self._session: if self._session:
self._media_position = self._convert_na_to_none( if self._device.machineIdentifier is not None and \
self._session.viewOffset) self._session.players:
self._media_content_id = self._convert_na_to_none( self._is_player_available = True
self._session.ratingKey) self._player = [p for p in self._session.players
self._media_content_rating = self._convert_na_to_none( if p.machineIdentifier ==
self._session.contentRating) self._device.machineIdentifier][0]
self._name = self._player.title
# player dependent data self._player_state = self._player.state
if self._session and self._session.player: self._session_username = self._session.usernames[0]
self._is_player_available = True self._make = self._player.device
self._machine_identifier = self._convert_na_to_none( else:
self._session.player.machineIdentifier) self._is_player_available = False
self._name = self._convert_na_to_none(self._session.player.title) self._media_position = self._session.viewOffset
self._player_state = self._session.player.state self._media_content_id = self._session.ratingKey
self._session_username = self._convert_na_to_none( self._media_content_rating = self._session.contentRating
self._session.username)
self._make = self._convert_na_to_none(self._session.player.device)
else:
self._is_player_available = False
if self._player_state == 'playing': if self._player_state == 'playing':
self._is_player_active = True self._is_player_active = True
@ -405,8 +396,7 @@ class PlexClient(MediaPlayerDevice):
if self._is_player_active and self._session is not None: if self._is_player_active and self._session is not None:
self._session_type = self._session.type self._session_type = self._session.type
self._media_duration = self._convert_na_to_none( self._media_duration = self._session.duration
self._session.duration)
else: else:
self._session_type = None self._session_type = None
@ -424,40 +414,34 @@ class PlexClient(MediaPlayerDevice):
# title (movie name, tv episode name, music song name) # title (movie name, tv episode name, music song name)
if self._session and self._is_player_active: if self._session and self._is_player_active:
self._media_title = self._convert_na_to_none(self._session.title) self._media_title = self._session.title
# Movies # Movies
if (self.media_content_type == MEDIA_TYPE_VIDEO and if (self.media_content_type == MEDIA_TYPE_VIDEO and
self._convert_na_to_none(self._session.year) is not None): self._session.year is not None):
self._media_title += ' (' + str(self._session.year) + ')' self._media_title += ' (' + str(self._session.year) + ')'
# TV Show # TV Show
if self._media_content_type is MEDIA_TYPE_TVSHOW: if self._media_content_type is MEDIA_TYPE_TVSHOW:
# season number (00) # season number (00)
if callable(self._convert_na_to_none(self._session.seasons)): if callable(self._session.seasons):
self._media_season = self._convert_na_to_none( self._media_season = self._session.seasons()[0].index.zfill(2)
self._session.seasons()[0].index).zfill(2) elif self._session.parentIndex is not None:
elif self._convert_na_to_none(
self._session.parentIndex) is not None:
self._media_season = self._session.parentIndex.zfill(2) self._media_season = self._session.parentIndex.zfill(2)
else: else:
self._media_season = None self._media_season = None
# show name # show name
self._media_series_title = self._convert_na_to_none( self._media_series_title = self._session.grandparentTitle
self._session.grandparentTitle)
# episode number (00) # episode number (00)
if self._convert_na_to_none(self._session.index) is not None: if self._session.index is not None:
self._media_episode = str(self._session.index).zfill(2) self._media_episode = str(self._session.index).zfill(2)
# Music # Music
if self._media_content_type == MEDIA_TYPE_MUSIC: if self._media_content_type == MEDIA_TYPE_MUSIC:
self._media_album_name = self._convert_na_to_none( self._media_album_name = self._session.parentTitle
self._session.parentTitle) self._media_album_artist = self._session.grandparentTitle
self._media_album_artist = self._convert_na_to_none( self._media_track = self._session.index
self._session.grandparentTitle) self._media_artist = self._session.originalTitle
self._media_track = self._convert_na_to_none(self._session.index)
self._media_artist = self._convert_na_to_none(
self._session.originalTitle)
# use album artist if track artist is missing # use album artist if track artist is missing
if self._media_artist is None: if self._media_artist is None:
_LOGGER.debug("Using album artist because track artist " _LOGGER.debug("Using album artist because track artist "
@ -466,41 +450,26 @@ class PlexClient(MediaPlayerDevice):
# set app name to library name # set app name to library name
if (self._session is not None if (self._session is not None
and self._session.librarySectionID is not None): and self._session.section() is not None):
self._app_name = self._convert_na_to_none( self._app_name = self._session.section().title
self._session.server.library.sectionByID(
self._session.librarySectionID).title)
else: else:
self._app_name = '' self._app_name = ''
# media image url # media image url
if self._session is not None: if self._session is not None:
thumb_url = self._get_thumbnail_url(self._session.thumb) thumb_url = self._session.thumbUrl
if (self.media_content_type is MEDIA_TYPE_TVSHOW if (self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.config.get(CONF_USE_EPISODE_ART)): and not self.config.get(CONF_USE_EPISODE_ART)):
thumb_url = self._get_thumbnail_url( thumb_url = self._server.url(
self._session.grandparentThumb) self._session.grandparentThumb)
if thumb_url is None: if thumb_url is None:
_LOGGER.debug("Using media art because media thumb " _LOGGER.debug("Using media art because media thumb "
"was not found: %s", self.entity_id) "was not found: %s", self.entity_id)
thumb_url = self._get_thumbnail_url(self._session.art) thumb_url = self._server.url(self._session.art)
self._media_image_url = thumb_url self._media_image_url = thumb_url
def _get_thumbnail_url(self, property_value):
"""Return full URL (if exists) for a thumbnail property."""
if self._convert_na_to_none(property_value) is None:
return None
if self._session is None or self._session.server is None:
return None
url = self._session.server.url(property_value)
response = requests.get(url, verify=False)
if response and response.status_code == 200:
return url
def force_idle(self): def force_idle(self):
"""Force client to idle.""" """Force client to idle."""
self._state = STATE_IDLE self._state = STATE_IDLE
@ -548,17 +517,6 @@ class PlexClient(MediaPlayerDevice):
self.update_devices(no_throttle=True) self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True) self.update_sessions(no_throttle=True)
# pylint: disable=no-self-use, singleton-comparison
def _convert_na_to_none(self, value):
"""Convert PlexAPI _NA() instances to None."""
# PlexAPI will return a "__NA__" object which can be compared to
# None, but isn't actually None - this converts it to a real None
# type so that lower layers don't think it's a URL and choke on it
if value is self.na_type:
return None
return value
@property @property
def _active_media_plexapi_type(self): def _active_media_plexapi_type(self):
"""Get the active media type required by PlexAPI commands.""" """Get the active media type required by PlexAPI commands."""
@ -685,32 +643,9 @@ class PlexClient(MediaPlayerDevice):
return None return None
def _local_client_control_fix(self):
"""Detect if local client and adjust url to allow control."""
if self.device is None:
return
# if this device's machineIdentifier matches an active client
# with a loopback address, the device must be local or casting
for client in self.device.server.clients():
if ("127.0.0.1" in client.baseurl and
client.machineIdentifier == self.device.machineIdentifier):
# point controls to server since that's where the
# playback is occurring
_LOGGER.debug(
"Local client detected, redirecting controls to "
"Plex server: %s", self.entity_id)
server_url = self.device.server.baseurl
client_url = self.device.baseurl
self.device.baseurl = "{}://{}:{}".format(
urlparse(client_url).scheme,
urlparse(server_url).hostname,
str(urlparse(client_url).port))
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
if self.device and 'playback' in self._device_protocol_capabilities: if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.setVolume( self.device.setVolume(
int(volume * 100), self._active_media_plexapi_type) int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve self._volume_level = volume # store since we can't retrieve
@ -749,19 +684,16 @@ class PlexClient(MediaPlayerDevice):
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
if self.device and 'playback' in self._device_protocol_capabilities: if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.play(self._active_media_plexapi_type) self.device.play(self._active_media_plexapi_type)
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
if self.device and 'playback' in self._device_protocol_capabilities: if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.pause(self._active_media_plexapi_type) self.device.pause(self._active_media_plexapi_type)
def media_stop(self): def media_stop(self):
"""Send stop command.""" """Send stop command."""
if self.device and 'playback' in self._device_protocol_capabilities: if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.stop(self._active_media_plexapi_type) self.device.stop(self._active_media_plexapi_type)
def turn_off(self): def turn_off(self):
@ -772,13 +704,11 @@ class PlexClient(MediaPlayerDevice):
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
if self.device and 'playback' in self._device_protocol_capabilities: if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.skipNext(self._active_media_plexapi_type) self.device.skipNext(self._active_media_plexapi_type)
def media_previous_track(self): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""
if self.device and 'playback' in self._device_protocol_capabilities: if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.skipPrevious(self._active_media_plexapi_type) self.device.skipPrevious(self._active_media_plexapi_type)
# pylint: disable=W0613 # pylint: disable=W0613
@ -874,8 +804,6 @@ class PlexClient(MediaPlayerDevice):
if delete: if delete:
media.delete() media.delete()
self._local_client_control_fix()
server_url = self.device.server.baseurl.split(':') server_url = self.device.server.baseurl.split(':')
self.device.sendCommand('playback/playMedia', **dict({ self.device.sendCommand('playback/playMedia', **dict({
'machineIdentifier': self.device.server.machineIdentifier, 'machineIdentifier': self.device.server.machineIdentifier,

View File

@ -10,12 +10,12 @@ import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT) CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['plexapi==2.0.2'] REQUIREMENTS = ['plexapi==3.0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,6 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TOKEN): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_SERVER): cv.string,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
@ -46,28 +47,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
plex_server = config.get(CONF_SERVER) plex_server = config.get(CONF_SERVER)
plex_host = config.get(CONF_HOST) plex_host = config.get(CONF_HOST)
plex_port = config.get(CONF_PORT) plex_port = config.get(CONF_PORT)
plex_token = config.get(CONF_TOKEN)
plex_url = 'http://{}:{}'.format(plex_host, plex_port) plex_url = 'http://{}:{}'.format(plex_host, plex_port)
add_devices([PlexSensor( add_devices([PlexSensor(
name, plex_url, plex_user, plex_password, plex_server)], True) name, plex_url, plex_user, plex_password, plex_server,
plex_token)], True)
class PlexSensor(Entity): class PlexSensor(Entity):
"""Representation of a Plex now playing sensor.""" """Representation of a Plex now playing sensor."""
def __init__(self, name, plex_url, plex_user, plex_password, plex_server): def __init__(self, name, plex_url, plex_user, plex_password,
plex_server, plex_token):
"""Initialize the sensor.""" """Initialize the sensor."""
from plexapi.utils import NA
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer from plexapi.server import PlexServer
self._na_type = NA
self._name = name self._name = name
self._state = 0 self._state = 0
self._now_playing = [] self._now_playing = []
if plex_user and plex_password: if plex_token:
user = MyPlexAccount.signin(plex_user, plex_password) self._server = PlexServer(plex_url, plex_token)
elif plex_user and plex_password:
user = MyPlexAccount(plex_user, plex_password)
server = plex_server if plex_server else user.resources()[0].name server = plex_server if plex_server else user.resources()[0].name
self._server = user.resource(server).connect() self._server = user.resource(server).connect()
else: else:
@ -99,9 +103,9 @@ class PlexSensor(Entity):
sessions = self._server.sessions() sessions = self._server.sessions()
now_playing = [] now_playing = []
for sess in sessions: for sess in sessions:
user = sess.username if sess.username is not self._na_type else "" user = sess.usernames[0] if sess.usernames is not None else ""
title = sess.title if sess.title is not self._na_type else "" title = sess.title if sess.title is not None else ""
year = sess.year if sess.year is not self._na_type else "" year = sess.year if sess.year is not None else ""
now_playing.append((user, "{0} ({1})".format(title, year))) now_playing.append((user, "{0} ({1})".format(title, year)))
self._state = len(sessions) self._state = len(sessions)
self._now_playing = now_playing self._now_playing = now_playing

View File

@ -524,7 +524,7 @@ pilight==0.1.1
# homeassistant.components.media_player.plex # homeassistant.components.media_player.plex
# homeassistant.components.sensor.plex # homeassistant.components.sensor.plex
plexapi==2.0.2 plexapi==3.0.3
# homeassistant.components.sensor.mhz19 # homeassistant.components.sensor.mhz19
# homeassistant.components.sensor.serial_pm # homeassistant.components.sensor.serial_pm