Support for non-clients, NVidia shield, dynamic grouping, extra metad… (#6054)

* Support for non-clients, NVidia shield, dynamic grouping, extra metadata, and more

* Fixed import orderdering, line lengths, and comment violations

* Local player check and season fixes

* Honor entity_namespace when using custom entity ids

* Semi compatibility with channels, force controls option added

* media_position_updated_at fix - only update when media is playing

* Fix: controls now work as expected on 1) casted sessions and 2) client sessions when client and PMS reside on the same sever

* Made PEP8 compliant

* Made PEP8 compliant

* Made PEP8 compliant, hopefully

* Better Tivo compatibility

* Added frozen detection and remediation

* PlayMedia EPISODE now supports season number and episode number (instead of episode index)

* Fix: Dynamic groups now exclude hidden devices

* Fix: Dynamic groups now exclude hidden devices

* Implemented arsaboo suggested formatting

* Implemented pylint command line suggested fixes

* Implemented Travis CI build suggested fixes

* Sorted Imports using Importanize

* Grouped request imports

* Removed dynamic groups, network calls from properties, and other cleanup

* Balloob recommendations and Plex Protocol Capabilities checks

* Remove deprecated disable-msg in favor of disable

* Removed position related items (seek, frozen detection, etc)

* Removed unused datetime
This commit is contained in:
JesseWebDotCom 2017-03-16 12:09:46 -04:00 committed by Paulus Schoutsen
parent 959dd29c90
commit 5714f156c3

View File

@ -10,16 +10,35 @@ import os
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
import homeassistant.util as util import requests
import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC,
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET, MEDIA_TYPE_TVSHOW,
SUPPORT_PLAY, MediaPlayerDevice) MEDIA_TYPE_VIDEO,
PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
MediaPlayerDevice,
)
from homeassistant.const import ( from homeassistant.const import (
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, DEVICE_DEFAULT_NAME,
STATE_UNKNOWN) STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers.event import (track_utc_time_change)
REQUIREMENTS = ['plexapi==2.0.2'] REQUIREMENTS = ['plexapi==2.0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -27,13 +46,24 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PLEX_CONFIG_FILE = 'plex.conf' PLEX_CONFIG_FILE = 'plex.conf'
CONF_INCLUDE_NON_CLIENTS = 'include_non_clients'
CONF_USE_EPISODE_ART = 'use_episode_art'
CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids'
CONF_SHOW_ALL_CONTROLS = 'show_all_controls'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False):
cv.boolean,
vol.Optional(CONF_USE_EPISODE_ART, default=False):
cv.boolean,
vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False):
cv.boolean,
})
# Map ip to request id for configuring # Map ip to request id for configuring
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY
def config_from_file(filename, config=None): def config_from_file(filename, config=None):
"""Small configuration file management function.""" """Small configuration file management function."""
@ -62,10 +92,12 @@ def config_from_file(filename, config=None):
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Plex platform.""" """Setup the Plex platform."""
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE)) # get config from plex.conf
if len(config): file_config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
if len(file_config):
# Setup a configured PlexServer # Setup a configured PlexServer
host, token = config.popitem() host, token = file_config.popitem()
token = token['token'] token = token['token']
# Via discovery # Via discovery
elif discovery_info is not None: elif discovery_info is not None:
@ -79,22 +111,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
else: else:
return return
setup_plexserver(host, token, hass, add_devices_callback) setup_plexserver(host, token, hass, config, add_devices_callback)
def setup_plexserver(host, token, hass, add_devices_callback): def setup_plexserver(host, token, hass, config, add_devices_callback):
"""Setup a plexserver based on host parameter.""" """Setup a plexserver based on host parameter."""
import plexapi.server import plexapi.server
import plexapi.exceptions import plexapi.exceptions
try: try:
plexserver = plexapi.server.PlexServer('http://%s' % host, token) plexserver = plexapi.server.PlexServer('http://%s' % host, token)
except (plexapi.exceptions.BadRequest, except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error: plexapi.exceptions.NotFound) as error:
_LOGGER.info(error) _LOGGER.info(error)
# No token or wrong token # No token or wrong token
request_configuration(host, hass, add_devices_callback) request_configuration(host, hass, config,
add_devices_callback)
return return
# If we came here and configuring this host, mark as done # If we came here and configuring this host, mark as done
@ -106,8 +138,9 @@ def setup_plexserver(host, token, hass, add_devices_callback):
# Save config # Save config
if not config_from_file( if not config_from_file(
hass.config.path(PLEX_CONFIG_FILE), hass.config.path(PLEX_CONFIG_FILE), {host: {
{host: {'token': token}}): 'token': token
}}):
_LOGGER.error('failed to save config file') _LOGGER.error('failed to save config file')
_LOGGER.info('Connected to: http://%s', host) _LOGGER.info('Connected to: http://%s', host)
@ -117,6 +150,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
track_utc_time_change(hass, lambda now: update_devices(), second=30) track_utc_time_change(hass, lambda now: update_devices(), second=30)
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
# pylint: disable=too-many-branches
def update_devices(): def update_devices():
"""Update the devices objects.""" """Update the devices objects."""
try: try:
@ -125,8 +159,8 @@ def setup_plexserver(host, token, hass, add_devices_callback):
_LOGGER.exception('Error listing plex devices') _LOGGER.exception('Error listing plex devices')
return return
except OSError: except OSError:
_LOGGER.error( _LOGGER.error('Could not connect to plex server at http://%s',
'Could not connect to plex server at http://%s', host) host)
return return
new_plex_clients = [] new_plex_clients = []
@ -136,12 +170,31 @@ def setup_plexserver(host, token, hass, add_devices_callback):
continue continue
if device.machineIdentifier not in plex_clients: if device.machineIdentifier not in plex_clients:
new_client = PlexClient(device, plex_sessions, update_devices, new_client = PlexClient(config, device, None,
plex_sessions, update_devices,
update_sessions) update_sessions)
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:
plex_clients[device.machineIdentifier].set_device(device) plex_clients[device.machineIdentifier].refresh(device, None)
# add devices with a session and no client (ex. PlexConnect Apple TV's)
if config.get(CONF_INCLUDE_NON_CLIENTS):
for machine_identifier, session in plex_sessions.items():
if (machine_identifier not in plex_clients
and machine_identifier is not None):
new_client = PlexClient(config, None, session,
plex_sessions, update_devices,
update_sessions)
plex_clients[machine_identifier] = new_client
new_plex_clients.append(new_client)
else:
plex_clients[machine_identifier].refresh(None, session)
for machine_identifier, client in plex_clients.items():
# force devices to idle that do not have a valid session
if client.session is None:
client.force_idle()
if new_plex_clients: if new_plex_clients:
add_devices_callback(new_plex_clients) add_devices_callback(new_plex_clients)
@ -157,99 +210,333 @@ def setup_plexserver(host, token, hass, add_devices_callback):
plex_sessions.clear() plex_sessions.clear()
for session in sessions: for session in sessions:
plex_sessions[session.player.machineIdentifier] = session if (session.player is not None and
session.player.machineIdentifier is not None):
plex_sessions[session.player.machineIdentifier] = session
update_devices()
update_sessions() update_sessions()
update_devices()
def request_configuration(host, hass, add_devices_callback): def request_configuration(host, hass, config, add_devices_callback):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = get_component('configurator') configurator = get_component('configurator')
# We got an error if this method is called while we are configuring # We got an error if this method is called while we are configuring
if host in _CONFIGURING: if host in _CONFIGURING:
configurator.notify_errors( configurator.notify_errors(_CONFIGURING[host],
_CONFIGURING[host], 'Failed to register, please try again.') 'Failed to register, please try again.')
return return
def plex_configuration_callback(data): def plex_configuration_callback(data):
"""The actions to do when our configuration callback is called.""" """The actions to do when our configuration callback is called."""
setup_plexserver(host, data.get('token'), hass, add_devices_callback) setup_plexserver(host,
data.get('token'), hass, config,
add_devices_callback)
_CONFIGURING[host] = configurator.request_config( _CONFIGURING[host] = configurator.request_config(
hass, 'Plex Media Server', plex_configuration_callback, hass,
'Plex Media Server',
plex_configuration_callback,
description=('Enter the X-Plex-Token'), description=('Enter the X-Plex-Token'),
entity_picture='/static/images/logo_plex_mediaserver.png', entity_picture='/static/images/logo_plex_mediaserver.png',
submit_caption='Confirm', submit_caption='Confirm',
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}] fields=[{
) 'id': 'token',
'name': 'X-Plex-Token',
'type': ''
}])
# pylint: disable=too-many-instance-attributes, too-many-public-methods
class PlexClient(MediaPlayerDevice): class PlexClient(MediaPlayerDevice):
"""Representation of a Plex device.""" """Representation of a Plex device."""
# pylint: disable=attribute-defined-outside-init # pylint: disable=too-many-arguments
def __init__(self, device, plex_sessions, update_devices, update_sessions): def __init__(self, config, device, session, plex_sessions,
update_devices, update_sessions):
"""Initialize the Plex device.""" """Initialize the Plex device."""
from plexapi.utils import NA from plexapi.utils import NA
self._app_name = ''
self._device = None
self._device_protocol_capabilities = None
self._is_player_active = False
self._is_player_available = False
self._machine_identifier = None
self._make = ''
self._media_content_id = None
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
self._media_title = None
self._name = None
self._player_state = 'idle'
self._previous_volume_level = 1 # Used in fake muting
self._session = None
self._session_type = 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.na_type = NA self.na_type = NA
self.config = config
self.plex_sessions = plex_sessions self.plex_sessions = plex_sessions
self.update_devices = update_devices self.update_devices = update_devices
self.update_sessions = update_sessions self.update_sessions = update_sessions
self.set_device(device)
self._season = None
def set_device(self, device): # Music
"""Set the device property.""" self._media_album_artist = None
self.device = device self._media_album_name = None
self._media_artist = None
self._media_track = None
# TV Show
self._media_episode = None
self._media_season = None
self._media_series_title = None
self.refresh(device, session)
# Assign custom entity ID if desired
if self.config.get(CONF_USE_CUSTOM_ENTITY_IDS):
prefix = ''
# allow for namespace prefixing when using custom entity names
if config.get("entity_namespace"):
prefix = config.get("entity_namespace") + '_'
# rename the entity id
if self.machine_identifier:
self.entity_id = "%s.%s%s" % (
'media_player', prefix,
self.machine_identifier.lower().replace('-', '_'))
else:
if self.name:
self.entity_id = "%s.%s%s" % (
'media_player', prefix,
self.name.lower().replace('-', '_'))
# pylint: disable=too-many-branches, too-many-statements
def refresh(self, device, session):
"""Refresh key device data."""
# new data refresh
if session:
self._session = session
if device:
self._device = device
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._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))
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)
else:
self._media_position = None
self._media_content_id = None
# 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._make = self._convert_na_to_none(self._session.player.device)
else:
self._is_player_available = False
if self._player_state == 'playing':
self._is_player_active = True
self._state = STATE_PLAYING
elif self._player_state == 'paused':
self._is_player_active = True
self._state = STATE_PAUSED
elif self.device:
self._is_player_active = False
self._state = STATE_IDLE
else:
self._is_player_active = False
self._state = STATE_OFF
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)
else:
self._session_type = None
self._media_duration = None
# media type
if self._session_type == 'clip':
_LOGGER.debug('Clip content type detected, '
'compatibility may vary: %s',
self.entity_id)
self._media_content_type = MEDIA_TYPE_TVSHOW
elif self._session_type == 'episode':
self._media_content_type = MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
self._media_content_type = MEDIA_TYPE_VIDEO
elif self._session_type == 'track':
self._media_content_type = MEDIA_TYPE_MUSIC
else:
self._media_content_type = None
# title (movie name, tv episode name, music song name)
if self._session:
self._media_title = self._convert_na_to_none(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._media_title += ' (' + str(self._session.year) + ')'
# TV Show
if (self._is_player_active and
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:
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)
# episode number (00)
if self._convert_na_to_none(
self._session.index) is not None:
self._media_episode = str(self._session.index).zfill(2)
else:
self._media_season = None
self._media_series_title = None
self._media_episode = None
# Music
if (self._is_player_active and
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)
# 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)
self._media_artist = self._media_album_artist
else:
self._media_album_name = None
self._media_album_artist = None
self._media_track = None
self._media_artist = None
# 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)
else:
self._app_name = ''
# media image url
if self._session is not None:
thumb_url = self._get_thumbnail_url(self._session.thumb)
if (self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.config.get(CONF_USE_EPISODE_ART)):
thumb_url = self._get_thumbnail_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)
self._media_image_url = thumb_url
else:
self._media_image_url = None
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
self._session = None
@property @property
def unique_id(self): def unique_id(self):
"""Return the id of this plex client.""" """Return the id of this plex client."""
return '{}.{}'.format( return '{}.{}'.format(self.__class__, self.machine_identifier or
self.__class__, self.device.machineIdentifier or self.device.name) self.name)
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self.device.title or DEVICE_DEFAULT_NAME 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 @property
def session(self): def session(self):
"""Return the session, if any.""" """Return the session, if any."""
return self.plex_sessions.get(self.device.machineIdentifier, None) return self._session
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self.session and self.session.player: return self._state
state = self.session.player.state
if state == 'playing':
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
# This is nasty. Need to find a way to determine alive
elif self.device:
return STATE_IDLE
else:
return STATE_OFF
return STATE_UNKNOWN
def update(self): def update(self):
"""Get the latest details.""" """Get the latest details."""
from plexapi.video import Show
self.update_devices(no_throttle=True) self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True) self.update_sessions(no_throttle=True)
if isinstance(self.session, Show):
self._season = self._convert_na_to_none(
self.session.seasons()[0].index)
# pylint: disable=no-self-use, singleton-comparison # pylint: disable=no-self-use, singleton-comparison
def _convert_na_to_none(self, value): def _convert_na_to_none(self, value):
"""Convert PlexAPI _NA() instances to None.""" """Convert PlexAPI _NA() instances to None."""
@ -272,93 +559,301 @@ class PlexClient(MediaPlayerDevice):
@property @property
def media_content_id(self): def media_content_id(self):
"""Content ID of current playing media.""" """Content ID of current playing media."""
if self.session is not None: return self._media_content_id
return self._convert_na_to_none(self.session.ratingKey)
@property @property
def media_content_type(self): def media_content_type(self):
"""Content type of current playing media.""" """Content type of current playing media."""
if self.session is None: if self._session_type == 'clip':
return None _LOGGER.debug('Clip content type detected, '
media_type = self.session.type 'compatibility may vary: %s',
if media_type == 'episode': self.entity_id)
return MEDIA_TYPE_TVSHOW return MEDIA_TYPE_TVSHOW
elif media_type == 'movie': elif self._session_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
return MEDIA_TYPE_VIDEO return MEDIA_TYPE_VIDEO
elif media_type == 'track': elif self._session_type == 'track':
return MEDIA_TYPE_MUSIC return MEDIA_TYPE_MUSIC
return None else:
return None
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._media_artist
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self._media_album_name
@property
def media_album_artist(self):
"""Album artist of current playing media, music track only."""
return self._media_album_artist
@property
def media_track(self):
"""Track number of current playing media, music track only."""
return self._media_track
@property @property
def media_duration(self): def media_duration(self):
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
if self.session is not None: return self._media_duration
return self._convert_na_to_none(self.session.duration)
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
if self.session is not None: return self._media_image_url
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 @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
# find a string we can use as a title return self._media_title
if self.session is not None:
return self._convert_na_to_none(self.session.title)
@property @property
def media_season(self): def media_season(self):
"""Season of curent playing media (TV Show only).""" """Season of curent playing media (TV Show only)."""
return self._season return self._media_season
@property @property
def media_series_title(self): def media_series_title(self):
"""The title of the series of current playing media (TV Show only).""" """The title of the series of current playing media (TV Show only)."""
from plexapi.video import Show return self._media_series_title
if isinstance(self.session, Show):
return self._convert_na_to_none(self.session.grandparentTitle)
@property @property
def media_episode(self): def media_episode(self):
"""Episode of current playing media (TV Show only).""" """Episode of current playing media (TV Show only)."""
from plexapi.video import Show return self._media_episode
if isinstance(self.session, Show):
return self._convert_na_to_none(self.session.index) @property
def make(self):
"""The make of the device (ex. SHIELD Android TV)."""
return self._make
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
return SUPPORT_PLEX features = None
if not self._is_player_active:
features = None
# force show all controls
if self.config.get(CONF_SHOW_ALL_CONTROLS):
features = (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
# only show controls when we know what device is connecting
if not self._make:
features = None
# no mute support
elif self.make.lower() == "shield android tv":
_LOGGER.debug(
'Shield Android TV client detected, disabling mute '
'controls: %s', self.entity_id)
features = (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF)
# Only supports play,pause,stop (and off which really is stop)
elif self.make.lower().startswith("tivo"):
_LOGGER.debug(
'Tivo client detected, only enabling pause, play, '
'stop, and off controls: %s', self.entity_id)
features = (SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP |
SUPPORT_TURN_OFF)
# Not all devices support playback functionality
# Playback includes volume, stop/play/pause, etc.
elif self.device and 'playback' in self._device_protocol_capabilities:
features = (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
else:
features = None
return features
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 occuring
_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."""
self.device.setVolume(int(volume * 100), if self.device and 'playback' in self._device_protocol_capabilities:
self._active_media_plexapi_type) 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
@property
def volume_level(self):
"""Return the volume level of the client (0..1)."""
if (self._is_player_active and self.device and
'playback' in self._device_protocol_capabilities):
return self._volume_level
@property
def is_volume_muted(self):
"""Return boolean if volume is currently muted."""
if self._is_player_active and self.device:
return self._volume_muted
def mute_volume(self, mute):
"""Mute the volume.
Since we can't actually mute, we'll:
- On mute, store volume and set volume to 0
- On unmute, set volume to previously stored volume
"""
if not (self.device and
'playback' in self._device_protocol_capabilities):
return
self._volume_muted = mute
if mute:
self._previous_volume_level = self._volume_level
self.set_volume_level(0)
else:
self.set_volume_level(self._previous_volume_level)
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
self.device.play(self._active_media_plexapi_type) 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): def media_pause(self):
"""Send pause command.""" """Send pause command."""
self.device.pause(self._active_media_plexapi_type) 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): def media_stop(self):
"""Send stop command.""" """Send stop command."""
self.device.stop(self._active_media_plexapi_type) 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):
"""Turn the client off."""
# Fake it since we can't turn the client off
self.media_stop()
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
self.device.skipNext(self._active_media_plexapi_type) 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): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""
self.device.skipPrevious(self._active_media_plexapi_type) 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
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
if not (self.device and
'playback' in self._device_protocol_capabilities):
return
src = json.loads(media_id)
media = None
if media_type == 'MUSIC':
media = self.device.server.library.section(
src['library_name']).get(src['artist_name']).album(
src['album_name']).get(src['track_name'])
elif media_type == 'EPISODE':
media = self._get_episode(
src['library_name'], src['show_name'],
src['season_number'], src['episode_number'])
elif media_type == 'PLAYLIST':
media = self.device.server.playlist(src['playlist_name'])
elif media_type == 'VIDEO':
media = self.device.server.library.section(
src['library_name']).get(src['video_name'])
if media:
self._client_play_media(media, shuffle=src['shuffle'])
def _get_episode(self, library_name, show_name, season_number,
episode_number):
"""Find TV episode and return a Plex media object."""
target_season = None
target_episode = None
seasons = self.device.server.library.section(library_name).get(
show_name).seasons()
for season in seasons:
if int(season.seasonNumber) == int(season_number):
target_season = season
break
if target_season is None:
_LOGGER.error('Season not found: %s\\%s - S%sE%s', library_name,
show_name,
str(season_number).zfill(2),
str(episode_number).zfill(2))
else:
for episode in target_season.episodes():
if int(episode.index) == int(episode_number):
target_episode = episode
break
if target_episode is None:
_LOGGER.error('Episode not found: %s\\%s - S%sE%s',
library_name, show_name,
str(season_number).zfill(2),
str(episode_number).zfill(2))
return target_episode
def _client_play_media(self, media, **params):
"""Instruct Plex client to play a piece of media."""
if not (self.device and
'playback' in self._device_protocol_capabilities):
_LOGGER.error('Client cannot play media: %s', self.entity_id)
return
import plexapi.playqueue
server_url = media.server.baseurl.split(':')
playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,
media, **params)
self._local_client_control_fix()
self.device.sendCommand('playback/playMedia', **dict({
'machineIdentifier':
self.device.server.machineIdentifier,
'address':
server_url[1].strip('/'),
'port':
server_url[-1],
'key':
media.key,
'containerKey':
'/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params))