Add options to ignore shared/managed Plex clients (#31738)

* Add option to ignore shared/managed Plex clients

* Start to allow user selection

* hass object not ready during init

* Don't bother sorting

* Plex account multi-select, handle new users not matching config

* Fix/add tests

* Lint simplifications

* Review cleanup

* Oops

* Rename options attributes, fix/add tests
This commit is contained in:
jjlawren 2020-02-18 18:46:45 -06:00 committed by GitHub
parent 4b3f9ecc2d
commit 0213f43f10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 272 additions and 14 deletions

View file

@ -27,6 +27,7 @@ from homeassistant.helpers.dispatcher import (
)
from .const import (
CONF_IGNORE_NEW_SHARED_USERS,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_SHOW_ALL_CONTROLS,
@ -50,6 +51,7 @@ MEDIA_PLAYER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
vol.Optional(CONF_IGNORE_NEW_SHARED_USERS, default=False): cv.boolean,
}
)

View file

@ -14,12 +14,15 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json
from .const import ( # pylint: disable=unused-import
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
CONF_CLIENT_IDENTIFIER,
CONF_IGNORE_NEW_SHARED_USERS,
CONF_MONITORED_USERS,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_SHOW_ALL_CONTROLS,
@ -28,6 +31,7 @@ from .const import ( # pylint: disable=unused-import
DOMAIN,
PLEX_CONFIG_FILE,
PLEX_SERVER_CONFIG,
SERVERS,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
@ -254,6 +258,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry):
"""Initialize Plex options flow."""
self.options = copy.deepcopy(config_entry.options)
self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
async def async_step_init(self, user_input=None):
"""Manage the Plex options."""
@ -261,6 +266,8 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_plex_mp_settings(self, user_input=None):
"""Manage the Plex media_player options."""
plex_server = self.hass.data[DOMAIN][SERVERS][self.server_id]
if user_input is not None:
self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[
CONF_USE_EPISODE_ART
@ -268,19 +275,56 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[
CONF_SHOW_ALL_CONTROLS
]
self.options[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = user_input[
CONF_IGNORE_NEW_SHARED_USERS
]
account_data = {
user: {"enabled": bool(user in user_input[CONF_MONITORED_USERS])}
for user in plex_server.accounts
}
self.options[MP_DOMAIN][CONF_MONITORED_USERS] = account_data
return self.async_create_entry(title="", data=self.options)
available_accounts = {name: name for name in plex_server.accounts}
available_accounts[plex_server.owner] += " [Owner]"
default_accounts = plex_server.accounts
known_accounts = set(plex_server.option_monitored_users)
if known_accounts:
default_accounts = {
user
for user in plex_server.option_monitored_users
if plex_server.option_monitored_users[user]["enabled"]
}
for user in plex_server.accounts:
if user not in known_accounts:
available_accounts[user] += " [New]"
if not plex_server.option_ignore_new_shared_users:
for new_user in plex_server.accounts - known_accounts:
default_accounts.add(new_user)
return self.async_show_form(
step_id="plex_mp_settings",
data_schema=vol.Schema(
{
vol.Required(
CONF_USE_EPISODE_ART,
default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART],
default=plex_server.option_use_episode_art,
): bool,
vol.Required(
CONF_SHOW_ALL_CONTROLS,
default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS],
default=plex_server.option_show_all_controls,
): bool,
vol.Optional(
CONF_MONITORED_USERS, default=default_accounts
): cv.multi_select(available_accounts),
vol.Required(
CONF_IGNORE_NEW_SHARED_USERS,
default=plex_server.option_ignore_new_shared_users,
): bool,
}
),

View file

@ -29,6 +29,8 @@ CONF_SERVER = "server"
CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
CONF_IGNORE_NEW_SHARED_USERS = "ignore_new_shared_users"
CONF_MONITORED_USERS = "monitored_users"
AUTH_CALLBACK_PATH = "/auth/plex/callback"
AUTH_CALLBACK_NAME = "auth:plex:callback"

View file

@ -274,7 +274,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
thumb_url = self.session.thumbUrl
if (
self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.plex_server.use_episode_art
and not self.plex_server.option_use_episode_art
):
thumb_url = self.session.url(self.session.grandparentThumb)
@ -481,7 +481,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
def supported_features(self):
"""Flag media player features that are supported."""
# force show all controls
if self.plex_server.show_all_controls:
if self.plex_server.option_show_all_controls:
return (
SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK

View file

@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
CONF_CLIENT_IDENTIFIER,
CONF_IGNORE_NEW_SHARED_USERS,
CONF_MONITORED_USERS,
CONF_SERVER,
CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART,
@ -51,6 +53,7 @@ class PlexServer:
self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
self.options = options
self.server_choice = None
self._accounts = []
self._owner_username = None
self._version = None
@ -95,6 +98,12 @@ class PlexServer:
else:
_connect_with_token()
self._accounts = [
account.name
for account in self._plex_server.systemAccounts()
if account.name
]
owner_account = [
account.name
for account in self._plex_server.systemAccounts()
@ -121,8 +130,22 @@ class PlexServer:
_LOGGER.debug("Updating devices")
available_clients = {}
ignored_clients = set()
new_clients = set()
monitored_users = self.accounts
known_accounts = set(self.option_monitored_users)
if known_accounts:
monitored_users = {
user
for user in self.option_monitored_users
if self.option_monitored_users[user]["enabled"]
}
if not self.option_ignore_new_shared_users:
for new_user in self.accounts - known_accounts:
monitored_users.add(new_user)
try:
devices = self._plex_server.clients()
sessions = self._plex_server.sessions()
@ -147,7 +170,12 @@ class PlexServer:
if session.TYPE == "photo":
_LOGGER.debug("Photo session detected, skipping: %s", session)
continue
session_username = session.usernames[0]
for player in session.players:
if session_username not in monitored_users:
ignored_clients.add(player.machineIdentifier)
_LOGGER.debug("Ignoring Plex client owned by %s", session_username)
continue
self._known_idle.discard(player.machineIdentifier)
available_clients.setdefault(
player.machineIdentifier, {"device": player}
@ -160,6 +188,8 @@ class PlexServer:
new_entity_configs = []
for client_id, client_data in available_clients.items():
if client_id in ignored_clients:
continue
if client_id in new_clients:
new_entity_configs.append(client_data)
else:
@ -167,11 +197,11 @@ class PlexServer:
client_id, client_data["device"], client_data.get("session")
)
self._known_clients.update(new_clients)
self._known_clients.update(new_clients | ignored_clients)
idle_clients = (self._known_clients - self._known_idle).difference(
available_clients
)
idle_clients = (
self._known_clients - self._known_idle - ignored_clients
).difference(available_clients)
for client_id in idle_clients:
self.refresh_entity(client_id, None, None)
self._known_idle.add(client_id)
@ -194,6 +224,11 @@ class PlexServer:
"""Return the plexapi PlexServer instance."""
return self._plex_server
@property
def accounts(self):
"""Return accounts associated with the Plex server."""
return set(self._accounts)
@property
def owner(self):
"""Return the Plex server owner username."""
@ -220,15 +255,25 @@ class PlexServer:
return self._plex_server._baseurl # pylint: disable=protected-access
@property
def use_episode_art(self):
def option_ignore_new_shared_users(self):
"""Return ignore_new_shared_users option."""
return self.options[MP_DOMAIN].get(CONF_IGNORE_NEW_SHARED_USERS, False)
@property
def option_use_episode_art(self):
"""Return use_episode_art option."""
return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART]
@property
def show_all_controls(self):
def option_show_all_controls(self):
"""Return show_all_controls option."""
return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS]
@property
def option_monitored_users(self):
"""Return dict of monitored users option."""
return self.options[MP_DOMAIN].get(CONF_MONITORED_USERS, {})
@property
def library(self):
"""Return library attribute from server object."""

View file

@ -36,7 +36,9 @@
"description": "Options for Plex Media Players",
"data": {
"use_episode_art": "Use episode art",
"show_all_controls": "Show all controls"
"show_all_controls": "Show all controls",
"ignore_new_shared_users": "Ignore new managed/shared users",
"monitored_users": "Monitored users"
}
}
}

View file

@ -1,4 +1,6 @@
"""Mock classes used in tests."""
import itertools
from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER
from homeassistant.const import CONF_HOST, CONF_PORT
@ -17,6 +19,12 @@ MOCK_SERVERS = [
},
]
MOCK_MONITORED_USERS = {
"a": {"enabled": True},
"b": {"enabled": False},
"c": {"enabled": True},
}
class MockResource:
"""Mock a PlexAccount resource."""
@ -65,7 +73,14 @@ class MockPlexSystemAccount:
class MockPlexServer:
"""Mock a PlexServer instance."""
def __init__(self, index=0, ssl=True):
def __init__(
self,
index=0,
ssl=True,
load_users=True,
num_users=len(MOCK_MONITORED_USERS),
ignore_new_users=False,
):
"""Initialize the object."""
host = MOCK_SERVERS[index][CONF_HOST]
port = MOCK_SERVERS[index][CONF_PORT]
@ -78,11 +93,24 @@ class MockPlexServer:
prefix = "https" if ssl else "http"
self._baseurl = f"{prefix}://{host}:{port}"
self._systemAccount = MockPlexSystemAccount()
self._ignore_new_users = ignore_new_users
self._load_users = load_users
self._num_users = num_users
def systemAccounts(self):
"""Mock the systemAccounts lookup method."""
return [self._systemAccount]
@property
def accounts(self):
"""Mock the accounts property."""
return set(["a", "b", "c"])
@property
def owner(self):
"""Mock the owner property."""
return "a"
@property
def url_in_use(self):
"""Return URL used by PlexServer."""
@ -92,3 +120,24 @@ class MockPlexServer:
def version(self):
"""Mock version of PlexServer."""
return "1.0"
@property
def option_monitored_users(self):
"""Mock loaded config option for monitored users."""
userdict = dict(itertools.islice(MOCK_MONITORED_USERS.items(), self._num_users))
return userdict if self._load_users else {}
@property
def option_ignore_new_shared_users(self):
"""Mock loaded config option for ignoring new users."""
return self._ignore_new_users
@property
def option_show_all_controls(self):
"""Mock loaded config option for showing all controls."""
return False
@property
def option_use_episode_art(self):
"""Mock loaded config option for using episode art."""
return False

View file

@ -1,4 +1,5 @@
"""Tests for Plex config flow."""
import copy
from unittest.mock import patch
import asynctest
@ -26,6 +27,7 @@ DEFAULT_OPTIONS = {
config_flow.MP_DOMAIN: {
config_flow.CONF_USE_EPISODE_ART: False,
config_flow.CONF_SHOW_ALL_CONTROLS: False,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: False,
}
}
@ -457,9 +459,20 @@ async def test_all_available_servers_configured(hass):
async def test_option_flow(hass):
"""Test config flow selection of one of two bridges."""
"""Test config options flow selection."""
entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=DEFAULT_OPTIONS)
mock_plex_server = MockPlexServer(load_users=False)
MOCK_SERVER_ID = MOCK_SERVERS[0][config_flow.CONF_SERVER_IDENTIFIER]
hass.data[config_flow.DOMAIN] = {
config_flow.SERVERS: {MOCK_SERVER_ID: mock_plex_server}
}
entry = MockConfigEntry(
domain=config_flow.DOMAIN,
data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVER_ID},
options=DEFAULT_OPTIONS,
)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(
@ -473,6 +486,8 @@ async def test_option_flow(hass):
user_input={
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
config_flow.CONF_MONITORED_USERS: list(mock_plex_server.accounts),
},
)
assert result["type"] == "create_entry"
@ -480,6 +495,105 @@ async def test_option_flow(hass):
config_flow.MP_DOMAIN: {
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
config_flow.CONF_MONITORED_USERS: {
user: {"enabled": True} for user in mock_plex_server.accounts
},
}
}
async def test_option_flow_loading_saved_users(hass):
"""Test config options flow selection when loading existing user config."""
mock_plex_server = MockPlexServer(load_users=True)
MOCK_SERVER_ID = MOCK_SERVERS[0][config_flow.CONF_SERVER_IDENTIFIER]
hass.data[config_flow.DOMAIN] = {
config_flow.SERVERS: {MOCK_SERVER_ID: mock_plex_server}
}
entry = MockConfigEntry(
domain=config_flow.DOMAIN,
data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVER_ID},
options=DEFAULT_OPTIONS,
)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(
entry.entry_id, context={"source": "test"}, data=None
)
assert result["type"] == "form"
assert result["step_id"] == "plex_mp_settings"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
config_flow.CONF_MONITORED_USERS: list(mock_plex_server.accounts),
},
)
assert result["type"] == "create_entry"
assert result["data"] == {
config_flow.MP_DOMAIN: {
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
config_flow.CONF_MONITORED_USERS: {
user: {"enabled": True} for user in mock_plex_server.accounts
},
}
}
async def test_option_flow_new_users_available(hass):
"""Test config options flow selection when new Plex accounts available."""
mock_plex_server = MockPlexServer(load_users=True, num_users=2)
MOCK_SERVER_ID = MOCK_SERVERS[0][config_flow.CONF_SERVER_IDENTIFIER]
hass.data[config_flow.DOMAIN] = {
config_flow.SERVERS: {MOCK_SERVER_ID: mock_plex_server}
}
OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_WITH_USERS[config_flow.MP_DOMAIN][config_flow.CONF_MONITORED_USERS] = {
"a": {"enabled": True}
}
entry = MockConfigEntry(
domain=config_flow.DOMAIN,
data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVER_ID},
options=OPTIONS_WITH_USERS,
)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(
entry.entry_id, context={"source": "test"}, data=None
)
assert result["type"] == "form"
assert result["step_id"] == "plex_mp_settings"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
config_flow.CONF_MONITORED_USERS: list(mock_plex_server.accounts),
},
)
assert result["type"] == "create_entry"
assert result["data"] == {
config_flow.MP_DOMAIN: {
config_flow.CONF_USE_EPISODE_ART: True,
config_flow.CONF_SHOW_ALL_CONTROLS: True,
config_flow.CONF_IGNORE_NEW_SHARED_USERS: True,
config_flow.CONF_MONITORED_USERS: {
user: {"enabled": True} for user in mock_plex_server.accounts
},
}
}