mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-16 02:02:58 -07:00
Update plexapi to 4.3.0
This commit is contained in:
parent
6b013da697
commit
f497c11d73
18 changed files with 2365 additions and 1061 deletions
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue