core/homeassistant/components/media_player/plex.py

320 lines
10 KiB
Python
Raw Normal View History

"""
2016-03-08 09:34:33 +00:00
Support to interface with the Plex API.
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-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
from urllib.parse import urlparse
import homeassistant.util as util
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)
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
from homeassistant.helpers.event import (track_utc_time_change)
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'
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
2015-10-25 17:00:54 +00:00
def config_from_file(filename, config=None):
2016-03-08 09:34:33 +00:00
"""Small configuration file management function."""
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)
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
return False
else:
return {}
2015-10-25 17:00:54 +00:00
2016-02-02 08:31:36 +00:00
# pylint: disable=abstract-method
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
2016-03-08 09:34:33 +00:00
"""Setup the Plex platform."""
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']
# Via discovery
2015-10-25 17:54:48 +00:00
elif discovery_info is not None:
# Parse discovery data
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
if host in _CONFIGURING:
return
token = None
else:
2015-10-25 17:54:48 +00:00
return
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
def setup_plexserver(host, token, hass, add_devices_callback):
2016-03-08 09:34:33 +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
try:
2015-10-25 17:00:54 +00:00
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
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)
# No token or wrong token
request_configuration(host, hass, add_devices_callback)
return
2015-10-18 20:02:18 +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')
_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 = {}
track_utc_time_change(hass, lambda now: update_devices(), second=30)
2015-09-29 19:50:07 +00:00
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_devices():
2016-03-08 09:34:33 +00:00
"""Update the devices objects."""
2015-09-29 19:50:07 +00:00
try:
2015-10-13 21:59:13 +00:00
devices = plexserver.clients()
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
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:
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():
2016-03-08 09:34:33 +00:00
"""Update the sessions objects."""
2015-09-29 19:50:07 +00:00
try:
sessions = plexserver.sessions()
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()
def request_configuration(host, hass, add_devices_callback):
2016-03-08 09:34:33 +00:00
"""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):
2016-03-08 09:34:33 +00:00
"""The actions to do when our configuration callback is called."""
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Plex Media Server", plex_configuration_callback,
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': ''}]
)
class PlexClient(MediaPlayerDevice):
2016-03-08 09:34:33 +00:00
"""Representation of 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):
2016-03-08 09:34:33 +00:00
"""Initialize the Plex device."""
2015-09-29 19:50:07 +00:00
self.plex_sessions = plex_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
self.set_device(device)
def set_device(self, device):
2016-03-08 09:34:33 +00:00
"""Set the device property."""
2015-09-29 19:50:07 +00:00
self.device = device
@property
def unique_id(self):
2016-03-08 09:34:33 +00:00
"""Return 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)
@property
def name(self):
2016-03-08 09:34:33 +00:00
"""Return the name of the device."""
2015-10-25 17:00:54 +00:00
return self.device.name or DEVICE_DEFAULT_NAME
2015-09-29 19:50:07 +00:00
@property
def session(self):
2016-03-08 09:34:33 +00:00
"""Return 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]
@property
def state(self):
2016-03-08 09:34:33 +00:00
"""Return the state of the device."""
2015-09-29 19:50:07 +00:00
if self.session:
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
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
return STATE_UNKNOWN
def update(self):
2016-03-08 09:34:33 +00:00
"""Get the latest details."""
2015-09-29 19:50:07 +00:00
self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True)
@property
def media_content_id(self):
2016-03-08 09:34:33 +00:00
"""Content ID of current playing media."""
2015-09-29 19:50:07 +00:00
if self.session is not None:
return self.session.ratingKey
@property
def media_content_type(self):
2016-03-08 09:34:33 +00:00
"""Content type of current playing media."""
2015-09-29 19:50:07 +00:00
if self.session is None:
return None
2015-09-29 19:50:07 +00:00
media_type = self.session.type
if media_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif media_type == 'movie':
return MEDIA_TYPE_VIDEO
return None
@property
def media_duration(self):
2016-03-08 09:34:33 +00:00
"""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
@property
def media_image_url(self):
2016-03-08 09:34:33 +00:00
"""Image url of current playing media."""
2015-09-29 19:50:07 +00:00
if self.session is not None:
return self.session.thumbUrl
@property
def media_title(self):
2016-03-08 09:34:33 +00:00
"""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
@property
def media_season(self):
2016-03-08 09:34:33 +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
@property
def media_series_title(self):
2016-03-08 09:34:33 +00:00
"""The title of the series 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
@property
def media_episode(self):
2016-03-08 09:34:33 +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
@property
def supported_media_commands(self):
2016-03-08 09:34:33 +00:00
"""Flag of media commands that are supported."""
return SUPPORT_PLEX
def media_play(self):
2016-03-08 09:34:33 +00:00
"""Send play command."""
2015-10-13 21:59:13 +00:00
self.device.play()
def media_pause(self):
2016-03-08 09:34:33 +00:00
"""Send pause command."""
2015-10-13 21:59:13 +00:00
self.device.pause()
def media_next_track(self):
2016-03-08 09:34:33 +00:00
"""Send next track command."""
2015-10-13 21:59:13 +00:00
self.device.skipNext()
def media_previous_track(self):
2016-03-08 09:34:33 +00:00
"""Send previous track command."""
2015-10-13 21:59:13 +00:00
self.device.skipPrevious()