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 datetimepull/6660/head
parent
959dd29c90
commit
5714f156c3
|
@ -10,16 +10,35 @@ import os
|
|||
from datetime import timedelta
|
||||
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 (
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_PLAY, MediaPlayerDevice)
|
||||
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,
|
||||
STATE_UNKNOWN)
|
||||
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
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers.event import (track_utc_time_change)
|
||||
|
||||
|
||||
REQUIREMENTS = ['plexapi==2.0.2']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
@ -27,13 +46,24 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
|||
|
||||
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__)
|
||||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
"""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):
|
||||
"""Setup the Plex platform."""
|
||||
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
||||
if len(config):
|
||||
# get config from plex.conf
|
||||
file_config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
||||
|
||||
if len(file_config):
|
||||
# Setup a configured PlexServer
|
||||
host, token = config.popitem()
|
||||
host, token = file_config.popitem()
|
||||
token = token['token']
|
||||
# Via discovery
|
||||
elif discovery_info is not None:
|
||||
|
@ -79,22 +111,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
else:
|
||||
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."""
|
||||
import plexapi.server
|
||||
import plexapi.exceptions
|
||||
|
||||
try:
|
||||
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
||||
except (plexapi.exceptions.BadRequest,
|
||||
plexapi.exceptions.Unauthorized,
|
||||
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
|
||||
plexapi.exceptions.NotFound) as error:
|
||||
_LOGGER.info(error)
|
||||
# No token or wrong token
|
||||
request_configuration(host, hass, add_devices_callback)
|
||||
request_configuration(host, hass, config,
|
||||
add_devices_callback)
|
||||
return
|
||||
|
||||
# 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
|
||||
if not config_from_file(
|
||||
hass.config.path(PLEX_CONFIG_FILE),
|
||||
{host: {'token': token}}):
|
||||
hass.config.path(PLEX_CONFIG_FILE), {host: {
|
||||
'token': token
|
||||
}}):
|
||||
_LOGGER.error('failed to save config file')
|
||||
|
||||
_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)
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
# pylint: disable=too-many-branches
|
||||
def update_devices():
|
||||
"""Update the devices objects."""
|
||||
try:
|
||||
|
@ -125,8 +159,8 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
_LOGGER.exception('Error listing plex devices')
|
||||
return
|
||||
except OSError:
|
||||
_LOGGER.error(
|
||||
'Could not connect to plex server at http://%s', host)
|
||||
_LOGGER.error('Could not connect to plex server at http://%s',
|
||||
host)
|
||||
return
|
||||
|
||||
new_plex_clients = []
|
||||
|
@ -136,12 +170,31 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
continue
|
||||
|
||||
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)
|
||||
plex_clients[device.machineIdentifier] = new_client
|
||||
new_plex_clients.append(new_client)
|
||||
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:
|
||||
add_devices_callback(new_plex_clients)
|
||||
|
@ -157,99 +210,333 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
|
||||
plex_sessions.clear()
|
||||
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_devices()
|
||||
|
||||
|
||||
def request_configuration(host, hass, add_devices_callback):
|
||||
def request_configuration(host, hass, config, 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.')
|
||||
configurator.notify_errors(_CONFIGURING[host],
|
||||
'Failed to register, please try again.')
|
||||
|
||||
return
|
||||
|
||||
def plex_configuration_callback(data):
|
||||
"""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(
|
||||
hass, 'Plex Media Server', plex_configuration_callback,
|
||||
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': ''}]
|
||||
)
|
||||
fields=[{
|
||||
'id': 'token',
|
||||
'name': 'X-Plex-Token',
|
||||
'type': ''
|
||||
}])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
||||
class PlexClient(MediaPlayerDevice):
|
||||
"""Representation of a Plex device."""
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, config, device, session, plex_sessions,
|
||||
update_devices, update_sessions):
|
||||
"""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_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.config = config
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.set_device(device)
|
||||
self._season = None
|
||||
|
||||
def set_device(self, device):
|
||||
"""Set the device property."""
|
||||
self.device = device
|
||||
# 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)
|
||||
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
|
||||
def unique_id(self):
|
||||
"""Return the id of this plex client."""
|
||||
return '{}.{}'.format(
|
||||
self.__class__, self.device.machineIdentifier or self.device.name)
|
||||
return '{}.{}'.format(self.__class__, self.machine_identifier or
|
||||
self.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""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
|
||||
def session(self):
|
||||
"""Return the session, if any."""
|
||||
return self.plex_sessions.get(self.device.machineIdentifier, None)
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.session and self.session.player:
|
||||
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
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details."""
|
||||
from plexapi.video import Show
|
||||
|
||||
self.update_devices(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
|
||||
def _convert_na_to_none(self, value):
|
||||
"""Convert PlexAPI _NA() instances to None."""
|
||||
|
@ -272,93 +559,301 @@ class PlexClient(MediaPlayerDevice):
|
|||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
if self.session is not None:
|
||||
return self._convert_na_to_none(self.session.ratingKey)
|
||||
return self._media_content_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self.session is None:
|
||||
return None
|
||||
media_type = self.session.type
|
||||
if media_type == 'episode':
|
||||
if self._session_type == 'clip':
|
||||
_LOGGER.debug('Clip content type detected, '
|
||||
'compatibility may vary: %s',
|
||||
self.entity_id)
|
||||
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
|
||||
elif media_type == 'track':
|
||||
elif self._session_type == 'track':
|
||||
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
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self.session is not None:
|
||||
return self._convert_na_to_none(self.session.duration)
|
||||
return self._media_duration
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self.session is not None:
|
||||
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
|
||||
return self._media_image_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._convert_na_to_none(self.session.title)
|
||||
return self._media_title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of curent playing media (TV Show only)."""
|
||||
return self._season
|
||||
return self._media_season
|
||||
|
||||
@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._convert_na_to_none(self.session.grandparentTitle)
|
||||
return self._media_series_title
|
||||
|
||||
@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._convert_na_to_none(self.session.index)
|
||||
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."""
|
||||
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):
|
||||
"""Set volume level, range 0..1."""
|
||||
self.device.setVolume(int(volume * 100),
|
||||
self._active_media_plexapi_type)
|
||||
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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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))
|
||||
|
|
Loading…
Reference in New Issue