2015-09-19 17:48:45 +00:00
|
|
|
"""
|
|
|
|
homeassistant.components.media_player.plex
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-10-09 21:33:59 +00:00
|
|
|
Provides an interface to the Plex API.
|
2015-09-19 17:48:45 +00:00
|
|
|
|
2015-10-09 21:33:59 +00:00
|
|
|
For more details about this platform, please refer to the documentation at
|
2015-11-09 12:12:18 +00:00
|
|
|
https://home-assistant.io/components/media_player.plex/
|
2015-09-19 17:48:45 +00:00
|
|
|
"""
|
2015-10-25 17:00:54 +00:00
|
|
|
import json
|
|
|
|
import logging
|
2016-02-19 05:27:50 +00:00
|
|
|
import os
|
2015-09-29 19:50:07 +00:00
|
|
|
from datetime import timedelta
|
2015-10-20 16:59:22 +00:00
|
|
|
from urllib.parse import urlparse
|
2015-09-19 17:48:45 +00:00
|
|
|
|
2015-10-22 21:16:04 +00:00
|
|
|
import homeassistant.util as util
|
2015-09-19 17:48:45 +00:00
|
|
|
from homeassistant.components.media_player import (
|
2016-02-19 05:27:50 +00:00
|
|
|
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
|
|
|
SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice)
|
2015-09-19 17:48:45 +00:00
|
|
|
from homeassistant.const import (
|
2016-02-19 05:27:50 +00:00
|
|
|
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
|
|
|
STATE_UNKNOWN)
|
|
|
|
from homeassistant.loader import get_component
|
2015-09-19 17:48:45 +00:00
|
|
|
|
2015-10-13 21:59:13 +00:00
|
|
|
REQUIREMENTS = ['plexapi==1.1.0']
|
2015-09-29 19:50:07 +00:00
|
|
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
|
|
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
2015-09-19 18:16:57 +00:00
|
|
|
|
2015-10-18 20:02:18 +00:00
|
|
|
PLEX_CONFIG_FILE = 'plex.conf'
|
|
|
|
|
2015-10-20 16:59:22 +00:00
|
|
|
# Map ip to request id for configuring
|
|
|
|
_CONFIGURING = {}
|
2015-09-19 17:48:45 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
|
|
|
|
2015-10-25 17:00:54 +00:00
|
|
|
|
2015-10-25 10:45:15 +00:00
|
|
|
def config_from_file(filename, config=None):
|
2016-01-07 10:57:45 +00:00
|
|
|
""" Small configuration file management function. """
|
2015-10-25 10:45:15 +00:00
|
|
|
if config:
|
|
|
|
# We're writing configuration
|
|
|
|
try:
|
2015-10-25 17:00:54 +00:00
|
|
|
with open(filename, 'w') as fdesc:
|
|
|
|
fdesc.write(json.dumps(config))
|
|
|
|
except IOError as error:
|
|
|
|
_LOGGER.error('Saving config file failed: %s', error)
|
2015-10-25 10:45:15 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
# We're reading config
|
|
|
|
if os.path.isfile(filename):
|
|
|
|
try:
|
2015-10-25 17:00:54 +00:00
|
|
|
with open(filename, 'r') as fdesc:
|
|
|
|
return json.loads(fdesc.read())
|
|
|
|
except IOError as error:
|
|
|
|
_LOGGER.error('Reading config file failed: %s', error)
|
2015-10-25 17:54:48 +00:00
|
|
|
# This won't work yet
|
2015-10-25 10:45:15 +00:00
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return {}
|
2015-10-25 17:00:54 +00:00
|
|
|
|
2015-09-20 20:13:26 +00:00
|
|
|
|
2016-02-02 08:31:36 +00:00
|
|
|
# pylint: disable=abstract-method
|
2015-10-22 21:16:04 +00:00
|
|
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
2015-09-19 17:48:45 +00:00
|
|
|
""" Sets up the plex platform. """
|
2015-10-18 20:02:18 +00:00
|
|
|
|
2015-10-25 17:54:48 +00:00
|
|
|
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
|
|
|
if len(config):
|
|
|
|
# Setup a configured PlexServer
|
|
|
|
host, token = config.popitem()
|
|
|
|
token = token['token']
|
2015-10-25 10:45:15 +00:00
|
|
|
# Via discovery
|
2015-10-25 17:54:48 +00:00
|
|
|
elif discovery_info is not None:
|
2015-10-22 21:16:04 +00:00
|
|
|
# Parse discovery data
|
2015-10-20 16:59:22 +00:00
|
|
|
host = urlparse(discovery_info[1]).netloc
|
2015-10-25 17:00:54 +00:00
|
|
|
_LOGGER.info('Discovered PLEX server: %s', host)
|
2015-10-18 20:02:18 +00:00
|
|
|
|
2015-10-25 10:45:15 +00:00
|
|
|
if host in _CONFIGURING:
|
|
|
|
return
|
|
|
|
token = None
|
|
|
|
else:
|
2015-10-25 17:54:48 +00:00
|
|
|
return
|
2015-10-25 10:45:15 +00:00
|
|
|
|
|
|
|
setup_plexserver(host, token, hass, add_devices_callback)
|
2015-10-18 20:02:18 +00:00
|
|
|
|
|
|
|
|
2015-10-25 17:00:54 +00:00
|
|
|
# pylint: disable=too-many-branches
|
2015-10-25 10:45:15 +00:00
|
|
|
def setup_plexserver(host, token, hass, add_devices_callback):
|
2016-01-07 10:57:45 +00:00
|
|
|
""" Setup a plexserver based on host parameter. """
|
2015-10-25 17:54:48 +00:00
|
|
|
import plexapi.server
|
|
|
|
import plexapi.exceptions
|
2015-09-29 19:50:07 +00:00
|
|
|
|
2015-10-22 21:16:04 +00:00
|
|
|
try:
|
2015-10-25 17:00:54 +00:00
|
|
|
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
2015-10-25 10:45:15 +00:00
|
|
|
except (plexapi.exceptions.BadRequest,
|
2015-10-25 17:54:48 +00:00
|
|
|
plexapi.exceptions.Unauthorized,
|
|
|
|
plexapi.exceptions.NotFound) as error:
|
2015-10-25 17:00:54 +00:00
|
|
|
_LOGGER.info(error)
|
2015-10-25 10:45:15 +00:00
|
|
|
# No token or wrong token
|
2015-10-22 21:16:04 +00:00
|
|
|
request_configuration(host, hass, add_devices_callback)
|
|
|
|
return
|
2015-10-18 20:02:18 +00:00
|
|
|
|
2015-10-25 10:45:15 +00:00
|
|
|
# If we came here and configuring this host, mark as done
|
|
|
|
if host in _CONFIGURING:
|
|
|
|
request_id = _CONFIGURING.pop(host)
|
|
|
|
configurator = get_component('configurator')
|
|
|
|
configurator.request_done(request_id)
|
|
|
|
_LOGGER.info('Discovery configuration done!')
|
|
|
|
|
|
|
|
# Save config
|
|
|
|
if not config_from_file(
|
|
|
|
hass.config.path(PLEX_CONFIG_FILE),
|
|
|
|
{host: {'token': token}}):
|
|
|
|
_LOGGER.error('failed to save config file')
|
|
|
|
|
2016-01-23 21:06:50 +00:00
|
|
|
_LOGGER.info('Connected to: http://%s', host)
|
2015-10-18 20:02:18 +00:00
|
|
|
|
2015-09-29 19:50:07 +00:00
|
|
|
plex_clients = {}
|
|
|
|
plex_sessions = {}
|
|
|
|
|
|
|
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
|
|
|
def update_devices():
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Updates the devices objects. """
|
2015-09-29 19:50:07 +00:00
|
|
|
try:
|
2015-10-13 21:59:13 +00:00
|
|
|
devices = plexserver.clients()
|
2015-10-25 10:45:15 +00:00
|
|
|
except plexapi.exceptions.BadRequest:
|
2015-09-29 19:50:07 +00:00
|
|
|
_LOGGER.exception("Error listing plex devices")
|
|
|
|
return
|
|
|
|
|
|
|
|
new_plex_clients = []
|
|
|
|
for device in devices:
|
2015-10-13 21:59:13 +00:00
|
|
|
# For now, let's allow all deviceClass types
|
2015-10-16 18:15:04 +00:00
|
|
|
if device.deviceClass in ['badClient']:
|
2015-09-29 19:50:07 +00:00
|
|
|
continue
|
|
|
|
|
2015-10-13 21:59:13 +00:00
|
|
|
if device.machineIdentifier not in plex_clients:
|
2015-09-29 19:50:07 +00:00
|
|
|
new_client = PlexClient(device, plex_sessions, update_devices,
|
|
|
|
update_sessions)
|
2015-10-13 21:59:13 +00:00
|
|
|
plex_clients[device.machineIdentifier] = new_client
|
2015-09-29 19:50:07 +00:00
|
|
|
new_plex_clients.append(new_client)
|
|
|
|
else:
|
2015-10-13 21:59:13 +00:00
|
|
|
plex_clients[device.machineIdentifier].set_device(device)
|
2015-09-29 19:50:07 +00:00
|
|
|
|
|
|
|
if new_plex_clients:
|
2015-10-25 10:45:15 +00:00
|
|
|
add_devices_callback(new_plex_clients)
|
2015-09-29 19:50:07 +00:00
|
|
|
|
|
|
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
|
|
|
def update_sessions():
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Updates the sessions objects. """
|
2015-09-29 19:50:07 +00:00
|
|
|
try:
|
|
|
|
sessions = plexserver.sessions()
|
2015-10-25 10:45:15 +00:00
|
|
|
except plexapi.exceptions.BadRequest:
|
2015-09-29 19:50:07 +00:00
|
|
|
_LOGGER.exception("Error listing plex sessions")
|
|
|
|
return
|
|
|
|
|
|
|
|
plex_sessions.clear()
|
|
|
|
for session in sessions:
|
|
|
|
plex_sessions[session.player.machineIdentifier] = session
|
|
|
|
|
|
|
|
update_devices()
|
|
|
|
update_sessions()
|
2015-09-19 17:48:45 +00:00
|
|
|
|
2015-09-20 20:13:26 +00:00
|
|
|
|
2015-10-20 16:59:22 +00:00
|
|
|
def request_configuration(host, hass, add_devices_callback):
|
|
|
|
""" Request configuration steps from the user. """
|
|
|
|
configurator = get_component('configurator')
|
|
|
|
|
|
|
|
# We got an error if this method is called while we are configuring
|
|
|
|
if host in _CONFIGURING:
|
|
|
|
configurator.notify_errors(
|
|
|
|
_CONFIGURING[host], "Failed to register, please try again.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def plex_configuration_callback(data):
|
|
|
|
""" Actions to do when our configuration callback is called. """
|
2015-10-25 10:45:15 +00:00
|
|
|
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
|
2015-10-20 16:59:22 +00:00
|
|
|
|
|
|
|
_CONFIGURING[host] = configurator.request_config(
|
|
|
|
hass, "Plex Media Server", plex_configuration_callback,
|
2015-10-22 21:16:04 +00:00
|
|
|
description=('Enter the X-Plex-Token'),
|
|
|
|
description_image="/static/images/config_plex_mediaserver.png",
|
|
|
|
submit_caption="Confirm",
|
2015-10-25 17:00:54 +00:00
|
|
|
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
|
2015-10-20 16:59:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2015-09-19 17:48:45 +00:00
|
|
|
class PlexClient(MediaPlayerDevice):
|
|
|
|
""" Represents a Plex device. """
|
|
|
|
|
2015-10-25 17:00:54 +00:00
|
|
|
# pylint: disable=too-many-public-methods, attribute-defined-outside-init
|
2015-09-29 19:50:07 +00:00
|
|
|
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
|
|
|
self.plex_sessions = plex_sessions
|
|
|
|
self.update_devices = update_devices
|
|
|
|
self.update_sessions = update_sessions
|
|
|
|
self.set_device(device)
|
|
|
|
|
|
|
|
def set_device(self, device):
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Sets the device property. """
|
2015-09-29 19:50:07 +00:00
|
|
|
self.device = device
|
|
|
|
|
2015-10-25 10:45:15 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
""" Returns the id of this plex client """
|
|
|
|
return "{}.{}".format(
|
2015-10-25 17:00:54 +00:00
|
|
|
self.__class__, self.device.machineIdentifier or self.device.name)
|
2015-10-25 10:45:15 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
""" Returns the name of the device. """
|
2015-10-25 17:00:54 +00:00
|
|
|
return self.device.name or DEVICE_DEFAULT_NAME
|
2015-10-25 10:45:15 +00:00
|
|
|
|
2015-09-29 19:50:07 +00:00
|
|
|
@property
|
|
|
|
def session(self):
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Returns the session, if any. """
|
2015-10-13 21:59:13 +00:00
|
|
|
if self.device.machineIdentifier not in self.plex_sessions:
|
2015-09-29 19:50:07 +00:00
|
|
|
return None
|
|
|
|
|
2015-10-13 21:59:13 +00:00
|
|
|
return self.plex_sessions[self.device.machineIdentifier]
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self):
|
|
|
|
""" Returns the state of the device. """
|
2015-09-29 19:50:07 +00:00
|
|
|
if self.session:
|
|
|
|
state = self.session.player.state
|
2015-09-19 17:48:45 +00:00
|
|
|
if state == 'playing':
|
|
|
|
return STATE_PLAYING
|
|
|
|
elif state == 'paused':
|
|
|
|
return STATE_PAUSED
|
2015-10-25 10:45:15 +00:00
|
|
|
# This is nasty. Need to find a way to determine alive
|
2015-10-13 21:59:13 +00:00
|
|
|
elif self.device:
|
2015-09-29 19:50:07 +00:00
|
|
|
return STATE_IDLE
|
|
|
|
else:
|
|
|
|
return STATE_OFF
|
|
|
|
|
2015-09-21 14:44:24 +00:00
|
|
|
return STATE_UNKNOWN
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
def update(self):
|
2015-09-29 19:50:07 +00:00
|
|
|
self.update_devices(no_throttle=True)
|
|
|
|
self.update_sessions(no_throttle=True)
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_id(self):
|
|
|
|
""" Content ID of current playing media. """
|
2015-09-29 19:50:07 +00:00
|
|
|
if self.session is not None:
|
|
|
|
return self.session.ratingKey
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_type(self):
|
|
|
|
""" Content type of current playing media. """
|
2015-09-29 19:50:07 +00:00
|
|
|
if self.session is None:
|
2015-09-19 17:48:45 +00:00
|
|
|
return None
|
2015-09-29 19:50:07 +00:00
|
|
|
media_type = self.session.type
|
2015-09-21 14:44:24 +00:00
|
|
|
if media_type == 'episode':
|
|
|
|
return MEDIA_TYPE_TVSHOW
|
|
|
|
elif media_type == 'movie':
|
|
|
|
return MEDIA_TYPE_VIDEO
|
|
|
|
return None
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_duration(self):
|
|
|
|
""" Duration of current playing media in seconds. """
|
2015-09-29 19:50:07 +00:00
|
|
|
if self.session is not None:
|
|
|
|
return self.session.duration
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_image_url(self):
|
|
|
|
""" Image url of current playing media. """
|
2015-09-29 19:50:07 +00:00
|
|
|
if self.session is not None:
|
|
|
|
return self.session.thumbUrl
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_title(self):
|
|
|
|
""" Title of current playing media. """
|
|
|
|
# find a string we can use as a title
|
2015-09-29 19:50:07 +00:00
|
|
|
if self.session is not None:
|
|
|
|
return self.session.title
|
2015-09-20 20:13:26 +00:00
|
|
|
|
2015-09-19 17:48:45 +00:00
|
|
|
@property
|
|
|
|
def media_season(self):
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Season of curent playing media (TV Show only). """
|
2015-09-29 19:50:07 +00:00
|
|
|
from plexapi.video import Show
|
|
|
|
if isinstance(self.session, Show):
|
|
|
|
return self.session.seasons()[0].index
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_series_title(self):
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Series title of current playing media (TV Show only). """
|
2015-09-29 19:50:07 +00:00
|
|
|
from plexapi.video import Show
|
|
|
|
if isinstance(self.session, Show):
|
|
|
|
return self.session.grandparentTitle
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_episode(self):
|
2015-10-09 21:33:59 +00:00
|
|
|
""" Episode of current playing media (TV Show only). """
|
2015-09-29 19:50:07 +00:00
|
|
|
from plexapi.video import Show
|
|
|
|
if isinstance(self.session, Show):
|
|
|
|
return self.session.index
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def supported_media_commands(self):
|
|
|
|
""" Flags of media commands that are supported. """
|
|
|
|
return SUPPORT_PLEX
|
|
|
|
|
|
|
|
def media_play(self):
|
|
|
|
""" media_play media player. """
|
2015-10-13 21:59:13 +00:00
|
|
|
self.device.play()
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
def media_pause(self):
|
|
|
|
""" media_pause media player. """
|
2015-10-13 21:59:13 +00:00
|
|
|
self.device.pause()
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
def media_next_track(self):
|
|
|
|
""" Send next track command. """
|
2015-10-13 21:59:13 +00:00
|
|
|
self.device.skipNext()
|
2015-09-19 17:48:45 +00:00
|
|
|
|
|
|
|
def media_previous_track(self):
|
|
|
|
""" Send previous track command. """
|
2015-10-13 21:59:13 +00:00
|
|
|
self.device.skipPrevious()
|