diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index b28b8a90..96bbe544 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -37,7 +37,6 @@ import socket import StringIO import gzip from threading import Thread -import Queue import traceback import requests import xml.etree.ElementTree as etree @@ -71,7 +70,7 @@ class PlexAPI(): def __init__(self): self.__language__ = xbmcaddon.Addon().getLocalizedString self.g_PMS = {} - self.doUtils = downloadutils.DownloadUtils() + self.doUtils = downloadutils.DownloadUtils().downloadUrl def GetPlexLoginFromSettings(self): """ @@ -227,8 +226,9 @@ class PlexAPI(): Returns False if not yet done so, or the XML response file as etree """ # Try to get a temporary token - url = 'https://plex.tv/pins/%s.xml' % identifier - xml = self.TalkToPlexServer(url, talkType="GET2") + xml = self.doUtils('https://plex.tv/pins/%s.xml' % identifier, + authenticate=False, + type="GET") try: temp_token = xml.find('auth_token').text except: @@ -238,29 +238,31 @@ class PlexAPI(): if not temp_token: return False # Use temp token to get the final plex credentials - url = 'https://plex.tv/users/account?X-Plex-Token=%s' % temp_token - xml = self.TalkToPlexServer(url, talkType="GET") + xml = self.doUtils('https://plex.tv/users/account?X-Plex-Token=%s' + % temp_token, + authenticate=False, + type="GET") return xml def GetPlexPin(self): """ For plex.tv sign-in: returns 4-digit code and identifier as 2 str """ - url = 'https://plex.tv/pins.xml' code = None identifier = None # Download - xml = self.TalkToPlexServer(url, talkType="POST") - if xml is False: - return code, identifier + xml = self.doUtils('https://plex.tv/pins.xml', + authenticate=False, + type="POST") try: - code = xml.find('code').text - identifier = xml.find('id').text - self.logMsg('Successfully retrieved code and id from plex.tv', 1) - return code, identifier + xml.attrib except: self.logMsg("Error, no PIN from plex.tv provided", -1) return None, None + code = xml.find('code').text + identifier = xml.find('id').text + self.logMsg('Successfully retrieved code and id from plex.tv', 1) + return code, identifier def TalkToPlexServer(self, url, talkType="GET", verify=True, token=None): """ @@ -674,137 +676,95 @@ class PlexAPI(): get Plex media Server List from plex.tv/pms/resources """ - # dprint(__name__, 0, "***") - # dprint(__name__, 0, "poke plex.tv - request Plex Media Server list") - # dprint(__name__, 0, "***") - XML = self.getXMLFromPMS('https://plex.tv', '/api/resources?includeHttps=1', {}, authtoken) - if XML==False: - pass # no data from MyPlex - else: - queue = Queue.Queue() - threads = [] - - for Dir in XML.getiterator('Device'): - if Dir.get('product','') == "Plex Media Server" and Dir.get('provides','') == "server": - uuid = Dir.get('clientIdentifier') - name = Dir.get('name') - token = Dir.get('accessToken', authtoken) - owned = Dir.get('owned', '0') - local = Dir.get('publicAddressMatches') - - if Dir.find('Connection') == None: - continue # no valid connection - skip - - uri = "" # flag to set first connection, possibly overwrite later with more suitable - for Con in Dir.getiterator('Connection'): - if uri=="" or Con.get('local','') == local: - protocol = Con.get('protocol') - ip = Con.get('address') - port = Con.get('port') - uri = Con.get('uri') - # todo: handle unforeseen - like we get multiple suitable connections. how to choose one? - - # check MyPlex data age - skip if >1 days - infoAge = time.time() - int(Dir.get('lastSeenAt')) - oneDayInSec = 60*60*24 - if infoAge > 1*oneDayInSec: - self.logMsg("Server %s not updated for 1 day - " - "skipping." % name, 0) - continue - - # poke PMS, own thread for each poke - PMSInfo = { 'uuid': uuid, 'name': name, 'token': token, 'owned': owned, 'local': local, \ - 'protocol': protocol, 'ip': ip, 'port': port, 'uri': uri } - PMS = { 'baseURL': uri, 'path': '/', 'options': None, 'token': token, \ - 'data': PMSInfo } - t = Thread(target=self.getXMLFromPMSToQueue, args=(PMS, queue)) - t.start() - threads.append(t) - - # wait for requests being answered - for t in threads: - t.join() - - # declare new PMSs - while not queue.empty(): - (PMSInfo, PMS) = queue.get() - - if PMS==False: - continue - - uuid = PMSInfo['uuid'] - name = PMSInfo['name'] - token = PMSInfo['token'] - owned = PMSInfo['owned'] - local = PMSInfo['local'] - protocol = PMSInfo['protocol'] - ip = PMSInfo['ip'] - port = PMSInfo['port'] - uri = PMSInfo['uri'] - - self.declarePMS(ATV_udid, uuid, name, protocol, ip, port) # dflt: token='', local, owned - updated later - self.updatePMSProperty(ATV_udid, uuid, 'accesstoken', token) - self.updatePMSProperty(ATV_udid, uuid, 'owned', owned) - self.updatePMSProperty(ATV_udid, uuid, 'local', local) - self.updatePMSProperty(ATV_udid, uuid, 'baseURL', uri) # set in declarePMS, overwrite for https encryption - - def getXMLFromPMS(self, baseURL, path, options={}, authtoken='', enableGzip=False): - """ - Plex Media Server communication - - parameters: - host - path - options - dict() of PlexConnect-options as received from aTV - None for no - std. X-Plex-Args - authtoken - authentication answer from MyPlex Sign In - result: - returned XML or 'False' in case of error - """ - xargs = {} - if options is not None: - xargs = self.getXArgsDeviceInfo(options) - if not authtoken == '': - xargs['X-Plex-Token'] = authtoken - - self.logMsg("URL for XML download: %s%s" % (baseURL, path), 1) - - request = urllib2.Request(baseURL+path, None, xargs) - request.add_header('User-agent', 'PlexDB') - if enableGzip: - request.add_header('Accept-encoding', 'gzip') - + xml = self.doUtils('https://plex.tv/api/resources?includeHttps=1', + authenticate=False, + headerOptions={'X-Plex-Token': authtoken}) try: - response = urllib2.urlopen(request, timeout=20) - except (urllib2.URLError, httplib.HTTPException) as e: - self.logMsg("No Response from Plex Media Server", 0) - if hasattr(e, 'reason'): - self.logMsg("We failed to reach a server. Reason: %s" % e.reason, 0) - elif hasattr(e, 'code'): - self.logMsg("The server couldn't fulfill the request. Error code: %s" % e.code, 0) - self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0) - return False - except IOError: - self.logMsg("Error loading response XML from Plex Media Server:\n%s" % traceback.format_exc(), 0) - return False + xml.attrib + except: + self.logMsg('Could not get list of PMS from plex.tv', -1) + return - if response.info().get('Content-Encoding') == 'gzip': - buf = StringIO.StringIO(response.read()) - file = gzip.GzipFile(fileobj=buf) - XML = etree.parse(file) + import Queue + queue = Queue.Queue() + threads = [] + + for Dir in xml.iter(tag='Device'): + if "server" in Dir.get('provides'): + if Dir.find('Connection') is None: + # no valid connection - skip + continue + + # check MyPlex data age - skip if >2 days + PMS = {} + PMS['name'] = Dir.get('name') + infoAge = time.time() - int(Dir.get('lastSeenAt')) + oneDayInSec = 2*60*60*24 + if infoAge > 1*oneDayInSec: + self.logMsg("Server %s not seen for 1 day - " + "skipping." % PMS['name'], 0) + continue + + PMS['uuid'] = Dir.get('clientIdentifier') + PMS['token'] = Dir.get('accessToken', authtoken) + PMS['owned'] = Dir.get('owned', '0') + PMS['local'] = Dir.get('publicAddressMatches') + PMS['ownername'] = Dir.get('sourceTitle', '') + PMS['path'] = '/' + PMS['options'] = None + + # flag to set first connection, possibly overwrite later with + # more suitable + PMS['baseURL'] = "" + for Con in Dir.iter(tag='Connection'): + if (PMS['baseURL'] == "" or + Con.get('local') == PMS['local']): + PMS['protocol'] = Con.get('protocol') + PMS['ip'] = Con.get('address') + PMS['port'] = Con.get('port') + PMS['baseURL'] = Con.get('baseURL') + # todo: handle unforeseen - like we get multiple suitable + # connections. how to choose one? + + # poke PMS, own thread for each poke + t = Thread(target=self.pokePMS, + args=(PMS['baseURL'], PMS['token'], PMS, queue)) + t.start() + threads.append(t) + + # wait for requests being answered + for t in threads: + t.join() + + # declare new PMSs + while not queue.empty(): + PMS = queue.get() + self.declarePMS(ATV_udid, PMS['uuid'], PMS['name'], + PMS['protocol'], PMS['ip'], PMS['port']) + # dflt: token='', local, owned - updated later + self.updatePMSProperty( + ATV_udid, PMS['uuid'], 'accesstoken', PMS['token']) + self.updatePMSProperty( + ATV_udid, PMS['uuid'], 'owned', PMS['owned']) + self.updatePMSProperty( + ATV_udid, PMS['uuid'], 'local', PMS['local']) + # set in declarePMS, overwrite for https encryption + self.updatePMSProperty( + ATV_udid, PMS['uuid'], 'baseURL', PMS['baseURL']) + self.updatePMSProperty( + ATV_udid, PMS['uuid'], 'ownername', PMS['ownername']) + queue.task_done() + + def pokePMS(self, url, token, PMS, queue): + xml = self.doUtils(url, + authenticate=False, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except: + return else: - # parse into etree - XML = etree.parse(response) - # Log received XML if debugging enabled. - self.logMsg("====== received PMS-XML ======", 1) - self.logMsg(XML, 1) - self.logMsg("====== PMS-XML finished ======", 1) - return XML - - def getXMLFromPMSToQueue(self, PMS, queue): - XML = self.getXMLFromPMS(PMS['baseURL'],PMS['path'],PMS['options'],PMS['token']) - queue.put( (PMS['data'], XML) ) + queue.put(PMS) def getURL(self, baseURL, path, key): if key.startswith('http://') or key.startswith('https://'): # external server @@ -1046,7 +1006,10 @@ class PlexAPI(): if pin: url += '?pin=' + pin self.logMsg('Switching to user %s' % userId, 0) - answer = self.TalkToPlexServer(url, talkType="POST", token=token) + answer = self.doUtils(url, + authenticate=False, + type="POST", + headerOptions={'X-Plex-Token': token}) try: answer.attrib except: @@ -1064,7 +1027,10 @@ class PlexAPI(): # Get final token to the PMS we've chosen url = 'https://plex.tv/api/resources?includeHttps=1' - xml = self.TalkToPlexServer(url, talkType="GET", token=token) + xml = self.doUtils(url, + authenticate=False, + type="GET", + headerOptions={'X-Plex-Token': token}) try: xml.attrib except: @@ -1096,12 +1062,12 @@ class PlexAPI(): % username, 0) return result - def MyPlexListHomeUsers(self, authtoken): + def MyPlexListHomeUsers(self, token): """ Returns a list for myPlex home users for the current plex.tv account. Input: - authtoken for plex.tv + token for plex.tv Output: List of users, where one entry is of the form: "id": userId, @@ -1117,16 +1083,16 @@ class PlexAPI(): If any value is missing, None is returned instead (or "" from plex.tv) If an error is encountered, False is returned """ - XML = self.getXMLFromPMS( - 'https://plex.tv', '/api/home/users/', {}, authtoken) - if not XML: + xml = self.doUtils('https://plex.tv/api/home/users/', + authenticate=False, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except: self.logMsg('Download of Plex home users failed.', -1) - self.logMsg('plex.tv xml received was: %s' % XML, -1) return False - # analyse response - root = XML.getroot() users = [] - for user in root: + for user in xml: users.append(user.attrib) return users diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 434170dc..be34ac3b 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -2,12 +2,9 @@ ############################################################################### -# import json import requests import xml.etree.ElementTree as etree -# import logging -# import xbmc import xbmcgui import utils @@ -36,8 +33,6 @@ class DownloadUtils(): def __init__(self): self.__dict__ = self._shared_state - self.clientInfo = clientinfo.ClientInfo() - def setUsername(self, username): # Reserved for userclient only self.username = username @@ -65,7 +60,6 @@ class DownloadUtils(): self.logMsg("Verify SSL host certificate: %s" % ssl, 2) self.logMsg("SSL client side certificate: %s" % sslclient, 2) - def postCapabilities(self, deviceId): # Post settings to session @@ -136,17 +130,12 @@ class DownloadUtils(): # ) # self.downloadUrl(url, postBody={}, type="POST") - def startSession(self): + # User should be authenticated when this method is called + client = clientinfo.ClientInfo() - log = self.logMsg - - self.deviceId = self.clientInfo.getDeviceId() - - # User is identified from this point - # Attach authenticated header to the session + self.deviceId = client.getDeviceId() verify = False - header = self.getHeader() # If user enabled host certificate verification try: @@ -154,17 +143,17 @@ class DownloadUtils(): if self.sslclient is not None: verify = self.sslclient except: - log("Could not load SSL settings.", 1) - + self.logMsg("Could not load SSL settings.", -1) # Start session self.s = requests.Session() - self.s.headers = header + # Attach authenticated header to the session + self.s.headers = client.getXArgsDeviceInfo() self.s.verify = verify # Retry connections to the server self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - log("Requests session started on: %s" % self.server, 1) + self.logMsg("Requests session started on: %s" % self.server, 1) def stopSession(self): try: @@ -172,15 +161,17 @@ class DownloadUtils(): except: self.logMsg("Requests session could not be terminated.", 1) - def getHeader(self, authenticate=True, options={}): - if authenticate: - header = self.clientInfo.getXArgsDeviceInfo(options=options) - else: - header = self.clientInfo.getXArgsDeviceInfo(options=options) + def getHeader(self, options=None): + try: + header = self.s.headers.copy() + except: + header = clientinfo.ClientInfo().getXArgsDeviceInfo() + if options is not None: + header.update(options) return header def downloadUrl(self, url, postBody=None, type="GET", parameters=None, - authenticate=True, headerOptions={}): + authenticate=True, headerOptions=None): timeout = self.timeout default_link = "" @@ -271,8 +262,7 @@ class DownloadUtils(): # If user is not authenticated elif not authenticate: - header = self.getHeader(authenticate=False, - options=headerOptions) + header = self.getHeader(options=headerOptions) # If user enables ssl verification try: @@ -285,7 +275,6 @@ class DownloadUtils(): else: verifyssl = False self.logMsg("Set SSL verification to: %s" % verifyssl, 2) - # Prepare request if type == "GET": r = requests.get(url, diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 559fe2f1..224b2bf5 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -22,7 +22,7 @@ class InitialSetup(): def __init__(self): self.clientInfo = clientinfo.ClientInfo() self.addonId = self.clientInfo.getAddonId() - self.doUtils = downloadutils.DownloadUtils() + self.doUtils = downloadutils.DownloadUtils().downloadUrl self.userClient = userclient.UserClient() self.plx = PlexAPI.PlexAPI() @@ -76,11 +76,14 @@ class InitialSetup(): else: self.logMsg('plex.tv connection with token successful', 0) # Refresh the info from Plex.tv - url = 'https://plex.tv/' - path = 'users/account' - xml = self.plx.getXMLFromPMS(url, path, authtoken=plexToken) - if xml: - xml = xml.getroot() + xml = self.doUtils('https://plex.tv/users/account', + authenticate=False, + headerOptions={'X-Plex-Token': plexToken}) + try: + xml.attrib + except: + self.logMsg('Failed to update Plex info from plex.tv', -1) + else: plexLogin = xml.attrib.get('title') utils.settings('plexLogin', value=plexLogin) home = 'true' if xml.attrib.get('home') == '1' else 'false' @@ -89,8 +92,6 @@ class InitialSetup(): utils.settings( 'plexHomeSize', value=xml.attrib.get('homeSize', '1')) self.logMsg('Updated Plex info from plex.tv', 0) - else: - self.logMsg('Failed to update Plex info from plex.tv', -1) # If a Plex server IP has already been set, return. if server and forcePlexTV is False and chooseServer is False: diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 09d0a98c..736c1051 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -142,7 +142,7 @@ class plexgdm: else: if "M-SEARCH * HTTP/1." in data: self.logMsg("Detected client discovery request from %s. " - " Replying" % addr, 2) + " Replying" % str(addr), 2) try: update_sock.sendto("HTTP/1.0 200 OK\r\n%s" % self.client_data,