Update plexapi to 4.3.0

This commit is contained in:
JonnyWong16 2021-01-24 19:53:39 -08:00
parent 6b013da697
commit f497c11d73
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
18 changed files with 2365 additions and 1061 deletions

View file

@ -2,14 +2,14 @@
import copy
import threading
import time
from xml.etree import ElementTree
import requests
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_ENABLE_FAST_CONNECT,
X_PLEX_IDENTIFIER, log, logfilter, utils)
from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.client import PlexClient
from plexapi.compat import ElementTree
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.library import LibrarySection
from plexapi.server import PlexServer
from plexapi.sonos import PlexSonosClient
@ -43,7 +43,7 @@ class MyPlexAccount(PlexObject):
guest (bool): Unknown.
home (bool): Unknown.
homeSize (int): Unknown.
id (str): Your Plex account ID.
id (int): Your Plex account ID.
locale (str): Your Plex locale
mailing_list_status (str): Your current mailing list status.
maxHomeSize (int): Unknown.
@ -71,11 +71,12 @@ class MyPlexAccount(PlexObject):
PLEXSERVERS = 'https://plex.tv/api/servers/{machineId}' # get
FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
REMOVEINVITE = 'https://plex.tv/api/invites/requested/{userId}?friend=0&server=1&home=0' # delete
REMOVEINVITE = 'https://plex.tv/api/invites/requested/{userId}?friend=1&server=1&home=1' # delete
REQUESTED = 'https://plex.tv/api/invites/requested' # get
REQUESTS = 'https://plex.tv/api/invites/requests' # get
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
LINK = 'https://plex.tv/api/v2/pins/link' # put
# Hub sections
VOD = 'https://vod.provider.plex.tv/' # get
WEBSHOWS = 'https://webshows.provider.plex.tv/' # get
@ -87,7 +88,7 @@ class MyPlexAccount(PlexObject):
key = 'https://plex.tv/users/account'
def __init__(self, username=None, password=None, token=None, session=None, timeout=None):
self._token = token
self._token = token or CONFIG.get('auth.server_token')
self._session = session or requests.Session()
self._sonos_cache = []
self._sonos_cache_timestamp = 0
@ -114,7 +115,7 @@ class MyPlexAccount(PlexObject):
self.guest = utils.cast(bool, data.attrib.get('guest'))
self.home = utils.cast(bool, data.attrib.get('home'))
self.homeSize = utils.cast(int, data.attrib.get('homeSize'))
self.id = data.attrib.get('id')
self.id = utils.cast(int, data.attrib.get('id'))
self.locale = data.attrib.get('locale')
self.mailing_list_status = data.attrib.get('mailing_list_status')
self.maxHomeSize = utils.cast(int, data.attrib.get('maxHomeSize'))
@ -139,7 +140,7 @@ class MyPlexAccount(PlexObject):
roles = data.find('roles')
self.roles = []
if roles:
if roles is not None:
for role in roles.iter('role'):
self.roles.append(role.attrib.get('id'))
@ -153,14 +154,15 @@ class MyPlexAccount(PlexObject):
self.services = None
self.joined_at = None
def device(self, name):
def device(self, name=None, clientId=None):
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified.
Parameters:
name (str): Name to match against.
clientId (str): clientIdentifier to match against.
"""
for device in self.devices():
if device.name.lower() == name.lower():
if (name and device.name.lower() == name.lower() or device.clientIdentifier == clientId):
return device
raise NotFound('Unable to find device %s' % name)
@ -217,7 +219,7 @@ class MyPlexAccount(PlexObject):
return []
t = time.time()
if t - self._sonos_cache_timestamp > 60:
if t - self._sonos_cache_timestamp > 5:
self._sonos_cache_timestamp = t
data = self.query('https://sonos.plex.tv/resources')
self._sonos_cache = [PlexSonosClient(self, elem) for elem in data]
@ -225,10 +227,10 @@ class MyPlexAccount(PlexObject):
return self._sonos_cache
def sonos_speaker(self, name):
return [x for x in self.sonos_speakers() if x.title == name][0]
return next((x for x in self.sonos_speakers() if x.title.split("+")[0].strip() == name), None)
def sonos_speaker_by_id(self, identifier):
return [x for x in self.sonos_speakers() if x.machineIdentifier == identifier][0]
return next((x for x in self.sonos_speakers() if x.machineIdentifier.startswith(identifier)), None)
def inviteFriend(self, user, server, sections=None, allowSync=False, allowCameraUpload=False,
allowChannels=False, filterMovies=None, filterTelevision=None, filterMusic=None):
@ -578,8 +580,8 @@ class MyPlexAccount(PlexObject):
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
Raises:
:exc:`plexapi.exceptions.BadRequest`: when client with provided clientId wasn`t found.
:exc:`plexapi.exceptions.BadRequest`: provided client doesn`t provides `sync-target`.
:exc:`~plexapi.exceptions.BadRequest`: When client with provided clientId wasn`t found.
:exc:`~plexapi.exceptions.BadRequest`: Provided client doesn`t provides `sync-target`.
"""
if not client and not clientId:
clientId = X_PLEX_IDENTIFIER
@ -683,6 +685,19 @@ class MyPlexAccount(PlexObject):
elem = ElementTree.fromstring(req.text)
return self.findItems(elem)
def link(self, pin):
""" Link a device to the account using a pin code.
Parameters:
pin (str): The 4 digit link pin code.
"""
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Plex-Product': 'Plex SSO'
}
data = {'code': pin}
self.query(self.LINK, self._session.put, headers=headers, data=data)
class MyPlexUser(PlexObject):
""" This object represents non-signed in users such as friends and linked
@ -942,7 +957,7 @@ class MyPlexResource(PlexObject):
HTTP or HTTPS connection.
Raises:
:exc:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
"""
# Sort connections from (https, local) to (http, remote)
# Only check non-local connections unless we own the resource
@ -958,7 +973,7 @@ class MyPlexResource(PlexObject):
# Try connecting to all known resource connections in parellel, but
# only return the first server (in order) that provides a response.
listargs = [[cls, url, self.accessToken, timeout] for url in connections]
log.info('Testing %s resource connections..', len(listargs))
log.debug('Testing %s resource connections..', len(listargs))
results = utils.threaded(_connect, listargs)
return _chooseConnection('Resource', self.name, results)
@ -1049,11 +1064,11 @@ class MyPlexDevice(PlexObject):
at least one connection was successful, the PlexClient object is built and returned.
Raises:
:exc:`plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
"""
cls = PlexServer if 'server' in self.provides else PlexClient
listargs = [[cls, url, self.token, timeout] for url in self.connections]
log.info('Testing %s device connections..', len(listargs))
log.debug('Testing %s device connections..', len(listargs))
results = utils.threaded(_connect, listargs)
return _chooseConnection('Device', self.name, results)
@ -1066,7 +1081,7 @@ class MyPlexDevice(PlexObject):
""" Returns an instance of :class:`~plexapi.sync.SyncList` for current device.
Raises:
:exc:`plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`.
:exc:`~plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`.
"""
if 'sync-target' not in self.provides:
raise BadRequest('Requested syncList for device which do not provides sync-target')
@ -1098,33 +1113,40 @@ class MyPlexPinLogin(object):
requestTimeout (int): timeout in seconds on initial connect to plex.tv (default config.TIMEOUT).
Attributes:
PINS (str): 'https://plex.tv/pins.xml'
CHECKPINS (str): 'https://plex.tv/pins/{pinid}.xml'
PINS (str): 'https://plex.tv/api/v2/pins'
CHECKPINS (str): 'https://plex.tv/api/v2/pins/{pinid}'
LINK (str): 'https://plex.tv/api/v2/pins/link'
POLLINTERVAL (int): 1
finished (bool): Whether the pin login has finished or not.
expired (bool): Whether the pin login has expired or not.
token (str): Token retrieved through the pin login.
pin (str): Pin to use for the login on https://plex.tv/link.
"""
PINS = 'https://plex.tv/pins.xml' # get
CHECKPINS = 'https://plex.tv/pins/{pinid}.xml' # get
PINS = 'https://plex.tv/api/v2/pins' # get
CHECKPINS = 'https://plex.tv/api/v2/pins/{pinid}' # get
POLLINTERVAL = 1
def __init__(self, session=None, requestTimeout=None):
def __init__(self, session=None, requestTimeout=None, headers=None):
super(MyPlexPinLogin, self).__init__()
self._session = session or requests.Session()
self._requestTimeout = requestTimeout or TIMEOUT
self.headers = headers
self._loginTimeout = None
self._callback = None
self._thread = None
self._abort = False
self._id = None
self._code = None
self._getCode()
self.finished = False
self.expired = False
self.token = None
self.pin = self._getPin()
@property
def pin(self):
return self._code
def run(self, callback=None, timeout=None):
""" Starts the thread which monitors the PIN login state.
@ -1133,8 +1155,8 @@ class MyPlexPinLogin(object):
timeout (int): Timeout in seconds waiting for the PIN login to succeed (optional).
Raises:
:class:`RuntimeError`: if the thread is already running.
:class:`RuntimeError`: if the PIN login for the current PIN has expired.
:class:`RuntimeError`: If the thread is already running.
:class:`RuntimeError`: If the PIN login for the current PIN has expired.
"""
if self._thread and not self._abort:
raise RuntimeError('MyPlexPinLogin thread is already running')
@ -1187,19 +1209,16 @@ class MyPlexPinLogin(object):
return False
def _getPin(self):
if self.pin:
return self.pin
def _getCode(self):
url = self.PINS
response = self._query(url, self._session.post)
if not response:
return None
self._id = response.find('id').text
self.pin = response.find('code').text
self._id = response.attrib.get('id')
self._code = response.attrib.get('code')
return self.pin
return self._code
def _checkLogin(self):
if not self._id:
@ -1213,7 +1232,7 @@ class MyPlexPinLogin(object):
if not response:
return False
token = response.find('auth_token').text
token = response.attrib.get('authToken')
if not token:
return False
@ -1241,11 +1260,19 @@ class MyPlexPinLogin(object):
finally:
self.finished = True
def _query(self, url, method=None):
def _headers(self, **kwargs):
""" Returns dict containing base headers for all requests for pin login. """
headers = BASE_HEADERS.copy()
if self.headers:
headers.update(self.headers)
headers.update(kwargs)
return headers
def _query(self, url, method=None, headers=None, **kwargs):
method = method or self._session.get
log.debug('%s %s', method.__name__.upper(), url)
headers = BASE_HEADERS.copy()
response = method(url, headers=headers, timeout=self._requestTimeout)
headers = headers or self._headers()
response = method(url, headers=headers, timeout=self._requestTimeout, **kwargs)
if not response.ok: # pragma: no cover
codename = codes.get(response.status_code)[0]
errtext = response.text.replace('\n', ' ')
@ -1288,9 +1315,9 @@ def _chooseConnection(ctype, name, results):
# or (url, token, None, runtime) in the case a connection could not be established.
for url, token, result, runtime in results:
okerr = 'OK' if result else 'ERR'
log.info('%s connection %s (%ss): %s?X-Plex-Token=%s', ctype, okerr, runtime, url, token)
log.debug('%s connection %s (%ss): %s?X-Plex-Token=%s', ctype, okerr, runtime, url, token)
results = [r[2] for r in results if r and r[2] is not None]
if results:
log.info('Connecting to %s: %s?X-Plex-Token=%s', ctype, results[0]._baseurl, results[0]._token)
log.debug('Connecting to %s: %s?X-Plex-Token=%s', ctype, results[0]._baseurl, results[0]._token)
return results[0]
raise NotFound('Unable to connect to %s: %s' % (ctype.lower(), name))