1 """Shared class to maintain Plex server instances."""
3 from __future__
import annotations
9 from urllib.parse
import urlparse
11 from plexapi.client
import PlexClient
12 from plexapi.exceptions
import BadRequest, NotFound, Unauthorized
14 import plexapi.playqueue
16 from requests
import Session
17 import requests.exceptions
26 CONF_IGNORE_NEW_SHARED_USERS,
27 CONF_IGNORE_PLEX_WEB_CLIENTS,
30 CONF_SERVER_IDENTIFIER,
38 PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL,
39 PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
40 PLEX_UPDATE_SENSOR_SIGNAL,
51 ShouldUpdateConfigEntry,
53 from .helpers
import get_plex_data
54 from .media_search
import search_media
55 from .models
import PlexSession
57 _LOGGER = logging.getLogger(__name__)
60 plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME
61 plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM
62 plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT
63 plexapi.X_PLEX_VERSION = X_PLEX_VERSION
67 """Manages a single Plex server connection."""
70 self, hass, server_config, known_server_id=None, options=None, entry_id=None
72 """Initialize a Plex server instance."""
81 self.
_url_url = server_config.get(CONF_URL)
82 self.
_token_token = server_config.get(CONF_TOKEN)
84 self.
_verify_ssl_verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
85 self.
_server_id_server_id = known_server_id
or server_config.get(CONF_SERVER_IDENTIFIER)
98 cooldown=DEBOUNCE_TIMEOUT,
106 if CONF_CLIENT_ID
in server_config:
107 plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_ID]
108 plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers()
109 plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()
113 """Return a MyPlexAccount instance."""
117 except (BadRequest, Unauthorized):
119 _LOGGER.error(
"Not authorized to access plex.tv with provided token")
124 """Return available clients linked to Plex account."""
125 if self.
accountaccount
is None:
133 for x
in self.
accountaccount.resources()
134 if "player" in x.provides
and x.presence
and x.publicAddressMatches
137 "Current available clients from plex.tv: %s", self.
_plextv_clients_plextv_clients
142 """Connect to a Plex server directly, obtaining direct URL if necessary."""
143 config_entry_update_needed =
False
145 def _connect_with_token():
147 x
for x
in self.
accountaccount.resources()
if "server" in x.provides
149 available_servers = [
150 (x.name, x.clientIdentifier, x.sourceTitle)
for x
in all_servers
155 if not self.
_server_id_server_id
and len(all_servers) > 1:
160 x.connect(timeout=10)
165 def _connect_with_url():
167 if self.
_url_url.startswith(
"https")
and not self.
_verify_ssl_verify_ssl:
169 session.verify =
False
170 self.
_plex_server_plex_server = plexapi.server.PlexServer(
174 def _update_plexdirect_hostname():
177 for x
in self.
accountaccount.resources()
178 if x.clientIdentifier == self.
_server_id_server_id
185 _LOGGER.error(
"Attempt to update plex.direct hostname failed")
191 except requests.exceptions.SSLError
as error:
192 while error
and not isinstance(error, ssl.SSLCertVerificationError):
193 error = error.__context__
194 if isinstance(error, ssl.SSLCertVerificationError):
195 domain = urlparse(self.
_url_url).netloc.split(
":")[0]
196 if domain.endswith(
"plex.direct")
and error.args[0].startswith(
197 f
"hostname '{domain}' doesn't match"
200 "Plex SSL certificate's hostname changed, updating"
202 if _update_plexdirect_hostname():
203 config_entry_update_needed =
True
207 "New certificate cannot be validated"
208 " with provided token"
215 _connect_with_token()
218 system_accounts = self.
_plex_server_plex_server.systemAccounts()
219 shared_users = self.
accountaccount.users()
if self.
accountaccount
else []
222 "Plex account has limited permissions,"
223 " shared account filtering will not be available"
227 for user
in shared_users:
228 for shared_server
in user.servers:
230 self.
_accounts_accounts.append(user.title)
232 _LOGGER.debug(
"Linked accounts: %s", self.
accountsaccounts)
234 owner_account = next(
235 (account.name
for account
in system_accounts
if account.accountID == 1),
240 self.
_accounts_accounts.append(owner_account)
241 _LOGGER.debug(
"Server owner found: '%s'", self.
_owner_username_owner_username)
245 if config_entry_update_needed:
246 raise ShouldUpdateConfigEntry
250 """Forward refresh dispatch to media_player."""
251 unique_id = f
"{self.machine_identifier}:{machine_identifier}"
252 _LOGGER.debug(
"Refreshing %s", unique_id)
255 PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id),
262 """Process a session payload received from a websocket callback."""
263 session_payload = payload[
"PlaySessionStateNotification"][0]
265 if (state := session_payload[
"state"]) ==
"buffering":
268 session_key =
int(session_payload[
"sessionKey"])
269 offset =
int(session_payload[
"viewOffset"])
270 rating_key =
int(session_payload[
"ratingKey"])
272 unique_id, active_session = next(
276 if session.session_key == session_key
281 if not active_session:
285 if state ==
"stopped":
288 active_session.state = state
289 active_session.media_position = offset
291 def update_with_new_media():
292 """Update an existing session with new media details."""
294 active_session.update_media(media)
296 if active_session.media_content_id != rating_key
and state
in (
300 await self.
hasshass.async_add_executor_job(update_with_new_media)
304 PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(unique_id),
314 """Fetch all data from the Plex server in a single method."""
322 """Update the platform entities."""
323 _LOGGER.debug(
"Updating devices")
327 available_clients = {}
328 ignored_clients = set()
331 monitored_users = self.
accountsaccounts
341 for new_user
in self.
accountsaccounts - known_accounts:
342 monitored_users.add(new_user)
345 devices, sessions, plextv_clients = await self.
hasshass.async_add_executor_job(
348 except plexapi.exceptions.Unauthorized:
350 "Token has expired for '%s', reloading integration", self.
friendly_namefriendly_name
352 await self.
hasshass.config_entries.async_reload(self.
entry_identry_id)
355 plexapi.exceptions.BadRequest,
356 requests.exceptions.RequestException,
359 "Could not connect to Plex server: %s (%s)", self.
friendly_namefriendly_name, ex
363 def process_device(source, device):
364 self.
_known_idle_known_idle.discard(device.machineIdentifier)
365 available_clients.setdefault(device.machineIdentifier, {
"device": device})
366 available_clients[device.machineIdentifier].setdefault(
367 PLAYER_SOURCE, source
371 device.machineIdentifier
not in ignored_clients
373 and device.product ==
"Plex Web"
375 ignored_clients.add(device.machineIdentifier)
376 if device.machineIdentifier
not in self.
_known_clients_known_clients:
378 "Ignoring %s %s: %s",
381 device.machineIdentifier,
385 if device.machineIdentifier
not in (
388 new_clients.add(device.machineIdentifier)
390 "New %s from %s: %s",
393 device.machineIdentifier,
396 def connect_to_client(source, baseurl, machine_identifier, name="Unknown"):
397 """Connect to a Plex client and return a PlexClient instance."""
402 identifier=machine_identifier,
405 except (NotFound, requests.exceptions.ConnectionError):
407 "Direct client connection failed, will try again: %s (%s)",
413 "Direct client connection unauthorized, ignoring: %s (%s)",
420 process_device(source, client)
422 def connect_to_resource(resource):
423 """Connect to a plex.tv resource and return a Plex client."""
425 client = resource.connect(timeout=3)
426 _LOGGER.debug(
"Resource connection successful to plex.tv: %s", client)
429 "Resource connection failed to plex.tv: %s", resource.name
432 client.proxyThroughServer(value=
False, server=self.
_plex_server_plex_server)
434 process_device(
"plex.tv", client)
436 def connect_new_clients():
437 """Create connections to newly discovered clients."""
439 machine_identifier = gdm_entry[
"data"][
"Resource-Identifier"]
442 if client
is not None:
443 process_device(
"GDM", client)
444 elif machine_identifier
not in available_clients:
446 f
"http://{gdm_entry['from'][0]}:{gdm_entry['data']['Port']}"
448 name = gdm_entry[
"data"][
"Name"]
449 connect_to_client(
"GDM", baseurl, machine_identifier, name)
451 for plextv_client
in plextv_clients:
454 if client
is not None:
455 process_device(
"plex.tv", client)
456 elif plextv_client.clientIdentifier
not in available_clients:
457 connect_to_resource(plextv_client)
459 def process_sessions():
460 live_session_keys = {x.sessionKey
for x
in sessions}
462 if session.session_key
not in live_session_keys:
463 _LOGGER.debug(
"Purging unknown session: %s", session.session_key)
466 for session
in sessions:
467 if session.TYPE ==
"photo":
468 _LOGGER.debug(
"Photo session detected, skipping: %s", session)
471 session_username = next(iter(session.usernames),
None)
472 player = session.player
473 unique_id = f
"{self.machine_identifier}:{player.machineIdentifier}"
475 _LOGGER.debug(
"Creating new Plex session: %s", session)
477 if session_username
and session_username
not in monitored_users:
478 ignored_clients.add(player.machineIdentifier)
480 "Ignoring %s client owned by '%s'",
486 process_device(
"session", player)
487 available_clients[player.machineIdentifier][
"session"] = (
491 for device
in devices:
492 process_device(
"PMS", device)
495 connect_new_clients()
498 await self.
hasshass.async_add_executor_job(sync_tasks)
500 new_entity_configs = []
501 for client_id, client_data
in available_clients.items():
502 if client_id
in ignored_clients:
504 if client_id
in new_clients:
505 new_entity_configs.append(client_data)
510 client_data[
"device"],
511 client_data.get(
"session"),
512 client_data.get(PLAYER_SOURCE),
519 ).difference(available_clients)
520 for client_id
in idle_clients:
525 if new_entity_configs:
539 """Return the plexapi PlexServer instance."""
544 """Return if a token is used to connect to this Plex server."""
545 return self.
_token_token
is not None
549 """Return accounts associated with the Plex server."""
554 """Return the Plex server owner username."""
559 """Return the version of the Plex server."""
564 """Return name of connected Plex server."""
569 """Return unique identifier of connected Plex server."""
574 """Return URL used for connected Plex server."""
579 """Return ignore_new_shared_users option."""
580 return self.
optionsoptions[MP_DOMAIN].
get(CONF_IGNORE_NEW_SHARED_USERS,
False)
584 """Return use_episode_art option."""
585 return self.
optionsoptions[MP_DOMAIN].
get(CONF_USE_EPISODE_ART,
False)
589 """Return dict of monitored users option."""
590 return self.
optionsoptions[MP_DOMAIN].
get(CONF_MONITORED_USERS, {})
594 """Return ignore_plex_web_clients option."""
595 return self.
optionsoptions[MP_DOMAIN].
get(CONF_IGNORE_PLEX_WEB_CLIENTS,
False)
599 """Return library attribute from server object."""
603 """Return playlist from server object."""
607 """Return available playlists from server object."""
611 """Create playqueue on Plex server."""
612 return plexapi.playqueue.PlayQueue.create(self.
_plex_server_plex_server, media, **kwargs)
615 """Create playqueue on Plex server using a radio station key."""
616 return plexapi.playqueue.PlayQueue.fromStationKey(self.
_plex_server_plex_server, key)
619 """Retrieve existing playqueue from Plex server."""
620 return plexapi.playqueue.PlayQueue.get(self.
_plex_server_plex_server, playqueue_id)
623 """Fetch item from Plex server."""
627 """Lookup a piece of media."""
628 media_type = media_type.lower()
630 if isinstance(kwargs.get(
"plex_key"), int):
631 key = kwargs[
"plex_key"]
634 except NotFound
as err:
635 raise MediaNotFound(f
"Media for key {key} not found")
from err
637 if media_type == MediaType.PLAYLIST:
639 playlist_name = kwargs[
"playlist_name"]
640 return self.
playlistplaylist(playlist_name)
641 except KeyError
as err:
643 "Must specify 'playlist_name' for this search"
645 except NotFound
as err:
646 raise MediaNotFound(f
"Playlist '{playlist_name}' not found")
from err
649 library_name = kwargs.pop(
"library_name")
650 library_section = self.
librarylibrary.section(library_name)
651 except KeyError
as err:
652 raise MediaNotFound(
"Must specify 'library_name' for this search")
from err
653 except NotFound
as err:
654 library_sections = [section.title
for section
in self.
librarylibrary.sections()]
656 f
"Library '{library_name}' not found in {library_sections}"
660 "Searching for %s in %s using: %s", media_type, library_section, kwargs
662 return search_media(media_type, library_section, **kwargs)
666 """Return active session information for use in activity sensor."""
667 return {x.sensor_user: x.sensor_title
for x
in self.
active_sessionsactive_sessions.values()}
670 """Set the PlexServer instance."""
674 """Return a shallow copy of a PlexServer as the provided user."""
675 new_server = copy(self)
676 new_server.set_plex_server(self.
plex_serverplex_server.switchUser(username))
def fetch_item(self, item)
def create_station_playqueue(self, key)
def sensor_attributes(self)
def option_use_episode_art(self)
None set_plex_server(self, PlexServer plex_server)
def option_ignore_plexweb_clients(self)
def create_playqueue(self, media, **kwargs)
def playlist(self, title)
def _fetch_platform_data(self)
def get_playqueue(self, playqueue_id)
PlexServer switch_user(self, str username)
def option_monitored_users(self)
def async_refresh_entity(self, machine_identifier, device, session, source)
def __init__(self, hass, server_config, known_server_id=None, options=None, entry_id=None)
def option_ignore_new_shared_users(self)
def _async_update_platforms(self)
def lookup_media(self, media_type, **kwargs)
def async_update_session(self, payload)
def machine_identifier(self)
bool add(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
PlexData get_plex_data(HomeAssistant hass)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)