core/homeassistant/components/media_player/plex.py

897 lines
32 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 requests
import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW,
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 (
DEVICE_DEFAULT_NAME,
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
2016-02-19 05:27:50 +00:00
from homeassistant.loader import get_component
REQUIREMENTS = ['plexapi==2.0.2']
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'
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
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
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
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
2016-03-08 09:34:33 +00:00
"""Setup the Plex platform."""
# get config from plex.conf
file_config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
if len(file_config):
2015-10-25 17:54:48 +00:00
# Setup a configured PlexServer
host, token = file_config.popitem()
2015-10-25 17:54:48 +00:00
token = token['token']
# Via discovery
2015-10-25 17:54:48 +00:00
elif discovery_info is not None:
# Parse discovery data
host = discovery_info.get('host')
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, config, add_devices_callback)
2015-10-18 20:02:18 +00:00
def setup_plexserver(host, token, hass, config, 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, plexapi.exceptions.Unauthorized,
2015-10-25 17:54:48 +00:00
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, config,
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)
# pylint: disable=too-many-branches
2015-09-29 19:50:07 +00:00
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:
_LOGGER.exception('Error listing plex devices')
return
except OSError:
_LOGGER.error('Could not connect to plex server at http://%s',
host)
2015-09-29 19:50:07 +00:00
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:
new_client = PlexClient(config, device, None,
plex_sessions, update_devices,
2015-09-29 19:50:07 +00:00
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:
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()
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:
_LOGGER.exception('Error listing plex sessions')
2015-09-29 19:50:07 +00:00
return
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
2015-09-29 19:50:07 +00:00
update_sessions()
update_devices()
def request_configuration(host, hass, config, 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, config,
add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass,
'Plex Media Server',
plex_configuration_callback,
description=('Enter the X-Plex-Token'),
entity_picture='/static/images/logo_plex_mediaserver.png',
submit_caption='Confirm',
fields=[{
'id': 'token',
'name': 'X-Plex-Token',
'type': ''
}])
# pylint: disable=too-many-instance-attributes, too-many-public-methods
class PlexClient(MediaPlayerDevice):
2016-03-08 09:34:33 +00:00
"""Representation of a Plex device."""
# pylint: disable=too-many-arguments
def __init__(self, config, device, session, plex_sessions,
update_devices, update_sessions):
2016-03-08 09:34:33 +00:00
"""Initialize the Plex device."""
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_rating = 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._session_username = 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.config = config
2015-09-29 19:50:07 +00:00
self.plex_sessions = plex_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
# Music
self._media_album_artist = None
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)
self._media_content_rating = self._convert_na_to_none(
self._session.contentRating)
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._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._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
2015-09-29 19:50:07 +00:00
@property
def unique_id(self):
2016-03-08 09:34:33 +00:00
"""Return the id of this plex client."""
return '{}.{}'.format(self.__class__, self.machine_identifier or
self.name)
@property
def name(self):
2016-03-08 09:34:33 +00:00
"""Return the name of the device."""
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
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."""
return self._session
@property
def state(self):
2016-03-08 09:34:33 +00:00
"""Return the state of the device."""
return self._state
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)
# 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):
2016-03-08 09:34:33 +00:00
"""Content ID of current playing media."""
return self._media_content_id
@property
def media_content_type(self):
2016-03-08 09:34:33 +00:00
"""Content type of current playing media."""
if self._session_type == 'clip':
_LOGGER.debug('Clip content type detected, '
'compatibility may vary: %s',
self.entity_id)
return MEDIA_TYPE_TVSHOW
elif self._session_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
return MEDIA_TYPE_VIDEO
elif self._session_type == 'track':
return MEDIA_TYPE_MUSIC
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
def media_duration(self):
2016-03-08 09:34:33 +00:00
"""Duration of current playing media in seconds."""
return self._media_duration
@property
def media_image_url(self):
2016-03-08 09:34:33 +00:00
"""Image url of current playing media."""
return self._media_image_url
@property
def media_title(self):
2016-03-08 09:34:33 +00:00
"""Title of current playing media."""
return self._media_title
@property
def media_season(self):
2016-03-08 09:34:33 +00:00
"""Season of curent playing media (TV Show only)."""
return self._media_season
@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)."""
return self._media_series_title
@property
def media_episode(self):
2016-03-08 09:34:33 +00:00
"""Episode of current playing media (TV Show only)."""
return self._media_episode
@property
def make(self):
"""The make of the device (ex. SHIELD Android TV)."""
return self._make
@property
def supported_features(self):
"""Flag media player features that are supported."""
if not self._is_player_active:
2017-03-17 00:46:47 +00:00
return None
# force show all controls
if self.config.get(CONF_SHOW_ALL_CONTROLS):
2017-03-17 00:46:47 +00:00
return (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:
2017-03-17 00:46:47 +00:00
return 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)
2017-03-17 00:46:47 +00:00
return (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)
2017-03-17 00:46:47 +00:00
return (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:
2017-03-17 00:46:47 +00:00
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
else:
2017-03-17 00:46:47 +00:00
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 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):
"""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
@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):
2016-03-08 09:34:33 +00:00
"""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):
2016-03-08 09:34:33 +00:00
"""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):
"""Turn the client off."""
# Fake it since we can't turn the client off
self.media_stop()
def media_next_track(self):
2016-03-08 09:34:33 +00:00
"""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):
2016-03-08 09:34:33 +00:00
"""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
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_tv_media(
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'])
import plexapi.playlist
if (media and media_type == 'EPISODE' and
isinstance(media, plexapi.playlist.Playlist)):
# delete episode playlist after being loaded into a play queue
self._client_play_media(media=media, delete=True,
shuffle=src['shuffle'])
elif media:
self._client_play_media(media=media, shuffle=src['shuffle'])
def _get_tv_media(self, library_name, show_name, season_number,
episode_number):
"""Find TV media and return a Plex media object."""
target_season = None
target_episode = None
show = self.device.server.library.section(library_name).get(
show_name)
if not season_number:
playlist_name = "{} - {} Episodes".format(
self.entity_id, show_name)
return self.device.server.createPlaylist(
playlist_name, show.episodes())
for season in show.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:
if not episode_number:
playlist_name = "{} - {} Season {} Episodes".format(
self.entity_id, show_name, str(season_number))
return self.device.server.createPlaylist(
playlist_name, target_season.episodes())
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, delete=False, **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
playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,
media, **params)
# delete dynamic playlists used to build playqueue (ex. play tv season)
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,
'address':
server_url[1].strip('/'),
'port':
server_url[-1],
'key':
media.key,
'containerKey':
'/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params))
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
attr = {}
attr['media_content_rating'] = self._media_content_rating
attr['session_username'] = self._session_username
attr['media_library_name'] = self._app_name
return attr