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:
parent
4b3f9ecc2d
commit
0213f43f10
8 changed files with 272 additions and 14 deletions
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue