From 5ff9e59b79308bc4860d249e75410b216383e839 Mon Sep 17 00:00:00 2001 From: Dean Camera Date: Tue, 9 Aug 2016 01:55:58 +1000 Subject: [PATCH] Update to latest Plex API, add music support. (#2739) * Update to latest Plex API, add music support. * Fix PyLint errors. * Update Plex sensor module to latest PlexAPI. * Oops - update Python sensor import. * According to PlexAPI docs, this is the new API for Plex Pass members. * More pylint STFUs. * Move pylint suppression. * Use plexapi NA type directly. * Pylint objects to short variable names. --- .../components/media_player/__init__.py | 6 +- homeassistant/components/media_player/plex.py | 80 ++++++++++++++----- homeassistant/components/sensor/plex.py | 19 +++-- requirements_all.txt | 2 +- 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f3d4321e751..7efbbe01653 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -688,7 +688,11 @@ class MediaPlayerImageView(HomeAssistantView): if not authenticated: return self.Response(status=401) - response = requests.get(player.media_image_url) + image_url = player.media_image_url + if image_url: + response = requests.get(image_url) + else: + response = None if response is None: return self.Response(status=500) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 3db8e5ace29..29d332646bb 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -12,15 +12,16 @@ from urllib.parse import urlparse import homeassistant.util as util from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice) + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import ( DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) from homeassistant.loader import get_component from homeassistant.helpers.event import (track_utc_time_change) -REQUIREMENTS = ['plexapi==1.1.0'] +REQUIREMENTS = ['plexapi==2.0.2'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) @@ -30,7 +31,8 @@ PLEX_CONFIG_FILE = 'plex.conf' _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK +SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_STOP | SUPPORT_VOLUME_SET def config_from_file(filename, config=None): @@ -193,6 +195,9 @@ class PlexClient(MediaPlayerDevice): # pylint: disable=too-many-public-methods, attribute-defined-outside-init def __init__(self, device, plex_sessions, update_devices, update_sessions): """Initialize the Plex device.""" + from plexapi.utils import NA + + self.na_type = NA self.plex_sessions = plex_sessions self.update_devices = update_devices self.update_sessions = update_sessions @@ -211,20 +216,17 @@ class PlexClient(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - return self.device.name or DEVICE_DEFAULT_NAME + return self.device.title or DEVICE_DEFAULT_NAME @property def session(self): """Return the session, if any.""" - if self.device.machineIdentifier not in self.plex_sessions: - return None - - return self.plex_sessions[self.device.machineIdentifier] + return self.plex_sessions.get(self.device.machineIdentifier, None) @property def state(self): """Return the state of the device.""" - if self.session: + if self.session and self.session.player: state = self.session.player.state if state == 'playing': return STATE_PLAYING @@ -243,11 +245,30 @@ 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 + else: + return value + + @property + def _active_media_plexapi_type(self): + """Get the active media type required by PlexAPI commands.""" + if self.media_content_type is MEDIA_TYPE_MUSIC: + return 'music' + else: + return 'video' + @property def media_content_id(self): """Content ID of current playing media.""" if self.session is not None: - return self.session.ratingKey + return self._convert_na_to_none(self.session.ratingKey) @property def media_content_type(self): @@ -259,65 +280,82 @@ class PlexClient(MediaPlayerDevice): return MEDIA_TYPE_TVSHOW elif media_type == 'movie': return MEDIA_TYPE_VIDEO + elif media_type == 'track': + return MEDIA_TYPE_MUSIC return None @property def media_duration(self): """Duration of current playing media in seconds.""" if self.session is not None: - return self.session.duration + return self._convert_na_to_none(self.session.duration) @property def media_image_url(self): """Image url of current playing media.""" if self.session is not None: - return self.session.thumbUrl + thumb_url = self._convert_na_to_none(self.session.thumbUrl) + if str(self.na_type) in thumb_url: + # Audio tracks build their thumb urls internally before passing + # back a URL with the PlexAPI _NA type already converted to a + # string and embedded into a malformed URL + thumb_url = None + return thumb_url @property def media_title(self): """Title of current playing media.""" # find a string we can use as a title if self.session is not None: - return self.session.title + return self._convert_na_to_none(self.session.title) @property def media_season(self): """Season of curent playing media (TV Show only).""" from plexapi.video import Show if isinstance(self.session, Show): - return self.session.seasons()[0].index + return self._convert_na_to_none(self.session.seasons()[0].index) @property def media_series_title(self): """The title of the series of current playing media (TV Show only).""" from plexapi.video import Show if isinstance(self.session, Show): - return self.session.grandparentTitle + return self._convert_na_to_none(self.session.grandparentTitle) @property def media_episode(self): """Episode of current playing media (TV Show only).""" from plexapi.video import Show if isinstance(self.session, Show): - return self.session.index + return self._convert_na_to_none(self.session.index) @property def supported_media_commands(self): """Flag of media commands that are supported.""" return SUPPORT_PLEX + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self.device.setVolume(int(volume * 100), + self._active_media_plexapi_type) + def media_play(self): """Send play command.""" - self.device.play() + self.device.play(self._active_media_plexapi_type) def media_pause(self): """Send pause command.""" - self.device.pause() + self.device.pause(self._active_media_plexapi_type) + + def media_stop(self): + """Send stop command.""" + self.device.stop(self._active_media_plexapi_type) def media_next_track(self): """Send next track command.""" - self.device.skipNext() + self.device.skipNext(self._active_media_plexapi_type) def media_previous_track(self): """Send previous track command.""" - self.device.skipPrevious() + self.device.skipPrevious(self._active_media_plexapi_type) diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index e35cb54cab8..89ffd020bdd 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['plexapi==1.1.0'] +REQUIREMENTS = ['plexapi==2.0.2'] CONF_SERVER = 'server' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) @@ -54,15 +54,18 @@ class PlexSensor(Entity): # pylint: disable=too-many-arguments def __init__(self, name, plex_url, plex_user, plex_password, plex_server): """Initialize the sensor.""" + from plexapi.utils import NA + + self._na_type = NA self._name = name self._state = 0 self._now_playing = [] if plex_user and plex_password: - from plexapi.myplex import MyPlexUser - user = MyPlexUser.signin(plex_user, plex_password) + from plexapi.myplex import MyPlexAccount + user = MyPlexAccount.signin(plex_user, plex_password) server = plex_server if plex_server else user.resources()[0].name - self._server = user.getResource(server).connect() + self._server = user.resource(server).connect() else: from plexapi.server import PlexServer self._server = PlexServer(plex_url) @@ -93,7 +96,11 @@ class PlexSensor(Entity): def update(self): """Update method for plex sensor.""" sessions = self._server.sessions() - now_playing = [(s.user.title, "{0} ({1})".format(s.title, s.year)) - for s in sessions] + now_playing = [] + for sess in sessions: + user = sess.user.title if sess.user 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 "" + now_playing.append((user, "{0} ({1})".format(title, year))) self._state = len(sessions) self._now_playing = now_playing diff --git a/requirements_all.txt b/requirements_all.txt index 7ab6efbc800..8f67114db84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -228,7 +228,7 @@ phue==0.8 # homeassistant.components.media_player.plex # homeassistant.components.sensor.plex -plexapi==1.1.0 +plexapi==2.0.2 # homeassistant.components.sensor.serial_pm pmsensor==0.2