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 os
from datetime import timedelta
from urllib.parse import urlparse
import requests
import voluptuous as vol
@ -24,7 +23,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['plexapi==2.0.2']
REQUIREMENTS = ['plexapi==3.0.3']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
@ -183,7 +182,7 @@ def setup_plexserver(
if device.machineIdentifier not in plex_clients:
new_client = PlexClient(config, device, None,
plex_sessions, update_devices,
update_sessions)
update_sessions, plexserver)
plex_clients[device.machineIdentifier] = new_client
new_plex_clients.append(new_client)
else:
@ -196,7 +195,7 @@ def setup_plexserver(
and machine_identifier is not None):
new_client = PlexClient(config, None, session,
plex_sessions, update_devices,
update_sessions)
update_sessions, plexserver)
plex_clients[machine_identifier] = new_client
new_plex_clients.append(new_client)
else:
@ -225,9 +224,8 @@ def setup_plexserver(
plex_sessions.clear()
for session in sessions:
if (session.player is not None and
session.player.machineIdentifier is not None):
plex_sessions[session.player.machineIdentifier] = session
for player in session.players:
plex_sessions[player.machineIdentifier] = session
update_sessions()
update_devices()
@ -277,14 +275,15 @@ class PlexClient(MediaPlayerDevice):
"""Representation of a Plex device."""
def __init__(self, config, device, session, plex_sessions,
update_devices, update_sessions):
update_devices, update_sessions, plex_server):
"""Initialize the Plex device."""
from plexapi.utils import NA
self._app_name = ''
self._server = plex_server
self._device = None
self._device_protocol_capabilities = None
self._is_player_active = False
self._is_player_available = False
self._player = None
self._machine_identifier = None
self._make = ''
self._name = None
@ -296,7 +295,6 @@ class PlexClient(MediaPlayerDevice):
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.na_type = NA
self.config = config
self.plex_sessions = plex_sessions
self.update_devices = update_devices
@ -353,42 +351,35 @@ class PlexClient(MediaPlayerDevice):
self._session = session
if device:
self._device = device
if "127.0.0.1" in self._device.url("/"):
self._device.proxyThroughServer()
self._session = None
if self._device:
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._machine_identifier = self._device.machineIdentifier
self._name = self._device.title or DEVICE_DEFAULT_NAME
self._device_protocol_capabilities = (
self._device.protocolCapabilities)
# set valid session, preferring device session
if self._device and self.plex_sessions.get(
self._device.machineIdentifier, None):
self._session = self._convert_na_to_none(self.plex_sessions.get(
self._device.machineIdentifier, None))
# set valid session, preferring device session
if self.plex_sessions.get(self._device.machineIdentifier, None):
self._session = self.plex_sessions.get(
self._device.machineIdentifier, None)
if self._session:
self._media_position = self._convert_na_to_none(
self._session.viewOffset)
self._media_content_id = self._convert_na_to_none(
self._session.ratingKey)
self._media_content_rating = self._convert_na_to_none(
self._session.contentRating)
# player dependent data
if self._session and self._session.player:
self._is_player_available = True
self._machine_identifier = self._convert_na_to_none(
self._session.player.machineIdentifier)
self._name = self._convert_na_to_none(self._session.player.title)
self._player_state = self._session.player.state
self._session_username = self._convert_na_to_none(
self._session.username)
self._make = self._convert_na_to_none(self._session.player.device)
else:
self._is_player_available = False
if self._device.machineIdentifier is not None and \
self._session.players:
self._is_player_available = True
self._player = [p for p in self._session.players
if p.machineIdentifier ==
self._device.machineIdentifier][0]
self._name = self._player.title
self._player_state = self._player.state
self._session_username = self._session.usernames[0]
self._make = self._player.device
else:
self._is_player_available = False
self._media_position = self._session.viewOffset
self._media_content_id = self._session.ratingKey
self._media_content_rating = self._session.contentRating
if self._player_state == 'playing':
self._is_player_active = True
@ -405,8 +396,7 @@ class PlexClient(MediaPlayerDevice):
if self._is_player_active and self._session is not None:
self._session_type = self._session.type
self._media_duration = self._convert_na_to_none(
self._session.duration)
self._media_duration = self._session.duration
else:
self._session_type = None
@ -424,40 +414,34 @@ class PlexClient(MediaPlayerDevice):
# title (movie name, tv episode name, music song name)
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
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) + ')'
# TV Show
if self._media_content_type is MEDIA_TYPE_TVSHOW:
# season number (00)
if callable(self._convert_na_to_none(self._session.seasons)):
self._media_season = self._convert_na_to_none(
self._session.seasons()[0].index).zfill(2)
elif self._convert_na_to_none(
self._session.parentIndex) is not None:
if callable(self._session.seasons):
self._media_season = self._session.seasons()[0].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._convert_na_to_none(
self._session.grandparentTitle)
self._media_series_title = self._session.grandparentTitle
# 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)
# Music
if self._media_content_type == MEDIA_TYPE_MUSIC:
self._media_album_name = self._convert_na_to_none(
self._session.parentTitle)
self._media_album_artist = self._convert_na_to_none(
self._session.grandparentTitle)
self._media_track = self._convert_na_to_none(self._session.index)
self._media_artist = self._convert_na_to_none(
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 "
@ -466,41 +450,26 @@ class PlexClient(MediaPlayerDevice):
# set app name to library name
if (self._session is not None
and self._session.librarySectionID is not None):
self._app_name = self._convert_na_to_none(
self._session.server.library.sectionByID(
self._session.librarySectionID).title)
and self._session.section() is not None):
self._app_name = self._session.section().title
else:
self._app_name = ''
# media image url
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
and not self.config.get(CONF_USE_EPISODE_ART)):
thumb_url = self._get_thumbnail_url(
thumb_url = self._server.url(
self._session.grandparentThumb)
if thumb_url is None:
_LOGGER.debug("Using media art because media thumb "
"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
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):
"""Force client to idle."""
self._state = STATE_IDLE
@ -548,17 +517,6 @@ class PlexClient(MediaPlayerDevice):
self.update_devices(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
def _active_media_plexapi_type(self):
"""Get the active media type required by PlexAPI commands."""
@ -685,32 +643,9 @@ class PlexClient(MediaPlayerDevice):
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):
"""Set volume level, range 0..1."""
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.setVolume(
int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve
@ -749,19 +684,16 @@ class PlexClient(MediaPlayerDevice):
def media_play(self):
"""Send play command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.play(self._active_media_plexapi_type)
def media_pause(self):
"""Send pause command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.pause(self._active_media_plexapi_type)
def media_stop(self):
"""Send stop command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.stop(self._active_media_plexapi_type)
def turn_off(self):
@ -772,13 +704,11 @@ class PlexClient(MediaPlayerDevice):
def media_next_track(self):
"""Send next track command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.skipNext(self._active_media_plexapi_type)
def media_previous_track(self):
"""Send previous track command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self._local_client_control_fix()
self.device.skipPrevious(self._active_media_plexapi_type)
# pylint: disable=W0613
@ -874,8 +804,6 @@ class PlexClient(MediaPlayerDevice):
if delete:
media.delete()
self._local_client_control_fix()
server_url = self.device.server.baseurl.split(':')
self.device.sendCommand('playback/playMedia', **dict({
'machineIdentifier': self.device.server.machineIdentifier,

View File

@ -10,12 +10,12 @@ import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
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.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['plexapi==2.0.2']
REQUIREMENTS = ['plexapi==3.0.3']
_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_NAME, default=DEFAULT_NAME): 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_SERVER): 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_host = config.get(CONF_HOST)
plex_port = config.get(CONF_PORT)
plex_token = config.get(CONF_TOKEN)
plex_url = 'http://{}:{}'.format(plex_host, plex_port)
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):
"""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."""
from plexapi.utils import NA
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer
self._na_type = NA
self._name = name
self._state = 0
self._now_playing = []
if plex_user and plex_password:
user = MyPlexAccount.signin(plex_user, plex_password)
if plex_token:
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
self._server = user.resource(server).connect()
else:
@ -99,9 +103,9 @@ class PlexSensor(Entity):
sessions = self._server.sessions()
now_playing = []
for sess in sessions:
user = sess.username if sess.username is not self._na_type else ""
title = sess.title if sess.title is not self._na_type else ""
year = sess.year if sess.year 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 None else ""
year = sess.year if sess.year is not None else ""
now_playing.append((user, "{0} ({1})".format(title, year)))
self._state = len(sessions)
self._now_playing = now_playing

View File

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