diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 66498d41..003d0cbb 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -49,9 +49,11 @@ import gzip from threading import Thread import Queue import traceback +import requests import re import json +import uuid try: import xml.etree.cElementTree as etree @@ -112,7 +114,7 @@ class PlexAPI(): Returns (myplexlogin, plexLogin, plexToken) from the Kodi file settings. Returns empty strings if not found. - myplexlogin is 'true' if user opted to log into plex.tv + myplexlogin is 'true' if user opted to log into plex.tv (the default) """ plexLogin = utils.settings('plexLogin') plexToken = utils.settings('plexToken') @@ -163,6 +165,153 @@ class PlexAPI(): self.SetPlexLoginToSettings(retrievedPlexLogin, authtoken) return (retrievedPlexLogin, authtoken) + def PlexTvSignInWithPin(self): + """ + Prompts user to sign in by visiting https://plex.tv/pin + + Writes username and token to Kodi settings file. Returns: + { + 'home': '1' if Plex Home, '0' otherwise + 'username': + 'avatar': URL to user avator + 'token': + } + Returns False if authentication did not work. + """ + code, identifier = self.GetPlexPin() + dialog = xbmcgui.Dialog() + if not code: + dialog.ok(self.addonName, + 'Problems trying to contact plex.tv', + 'Try again later') + return False + answer = dialog.yesno(self.addonName, + 'Go to https://plex.tv/pin and enter the code:', + '', + code) + if not answer: + return False + count = 0 + # Wait for approx 30 seconds (since the PIN is not visible anymore :-)) + while count < 6: + xml = self.CheckPlexTvSignin(identifier) + if xml: + break + # Wait for 5 seconds + xbmc.sleep(5000) + count += 1 + if not xml: + dialog.ok(self.addonName, + 'Could not sign in to plex.tv', + 'Try again later') + return False + # Parse xml + home = xml.get('home', '0') + username = xml.get('username', '') + avatar = xml.get('thumb') + token = xml.findtext('authentication-token') + result = { + 'home': home, + 'username': username, + 'avatar': avatar, + 'token': token + } + self.SetPlexLoginToSettings(username, token) + return result + + def CheckPlexTvSignin(self, identifier): + """ + Checks with plex.tv whether user entered the correct PIN on plex.tv/pin + + 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") + try: + temp_token = xml.find('auth_token').text + except: + self.logMsg("Error: Could not find token in plex.tv answer.", -1) + return False + self.logMsg("temp token from plex.tv is: %s" % temp_token, 2) + 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") + 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 not xml: + return code, identifier + try: + code = xml.find('code').text + identifier = xml.find('id').text + except: + self.logMsg("Error, no PIN from plex.tv provided", -1) + self.logMsg("plex.tv/pin: Code is: %s" % code, 2) + self.logMsg("plex.tv/pin: Identifier is: %s" % identifier, 2) + return code, identifier + + def TalkToPlexServer(self, url, talkType="GET", verify=True): + """ + Start request with PMS with url. + + Returns the parsed XML answer as an etree object. Or False. + """ + header = self.getXArgsDeviceInfo() + timeout = (3, 10) + try: + if talkType == "GET": + answer = requests.get(url, + headers={}, + params=header, + verify=verify, + timeout=timeout) + if talkType == "GET2": + answer = requests.get(url, + headers=header, + params={}, + verify=verify, + timeout=timeout) + elif talkType == "POST": + answer = requests.post(url, + data='', + headers=header, + params={}, + verify=verify, + timeout=timeout) + except requests.exceptions.ConnectionError as e: + self.logMsg("Server is offline or cannot be reached. Url: %s." + "Header: %s. Error message: %s" + % (url, header, e), -1) + return False + except requests.exceptions.ReadTimeout: + self.logMsg("Server timeout reached for Url %s with header %s" + % (url, header), -1) + return False + # We received an answer from the server, but not as expected. + if answer.status_code >= 400: + self.logMsg("Error, answer from server %s was not as expected. " + "HTTP status code: %s" % (url, answer.status_code), -1) + return False + xml = answer.text.encode('utf-8') + self.logMsg("xml received from server %s: %s" % (url, xml), 2) + try: + xml = etree.fromstring(xml) + except: + self.logMsg("Error parsing XML answer from %s" % url, -1) + return False + return xml + def CheckConnection(self, url, token): """ Checks connection to a Plex server, available at url. Can also be used @@ -635,24 +784,21 @@ class PlexAPI(): """ # Get addon infos xargs = { - 'User-agent': self.addonName, - 'X-Plex-Device': self.deviceName, - 'X-Plex-Platform': self.platform, + 'X-Plex-Language': 'en', + 'X-Plex-Device': self.addonName, 'X-Plex-Client-Platform': self.platform, + 'X-Plex-Device-Name': self.deviceName, + 'X-Plex-Platform': self.addonName, + 'X-Plex-Platform-Version': 'no idea', + 'X-Plex-Model': 'unknown', 'X-Plex-Product': self.addonName, 'X-Plex-Version': self.plexversion, 'X-Plex-Client-Identifier': self.clientId, - 'machineIdentifier': self.machineIdentifier, - 'Connection': 'keep-alive', 'X-Plex-Provides': 'player', - 'Accept': 'application/xml' } - try: + if self.token: xargs['X-Plex-Token'] = self.token - except NameError: - # no token needed/saved yet - pass if JSON: xargs['Accept'] = 'application/json' if options: diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 2b311476..8d5b6593 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -69,30 +69,26 @@ class ClientInfo(): return "Unknown" def getDeviceId(self): + """ + Returns a unique Plex client id "X-Plex-Client-Identifier" from Kodi + settings file. + Also loads Kodi window property 'plex_client_Id' - clientId = utils.window('emby_deviceId') + If id does not exist, create one and save in Kodi settings file. + """ + clientId = utils.window('plex_client_Id') if clientId: return clientId - addon_path = self.addon.getAddonInfo('path').decode('utf-8') - GUID_file = xbmc.translatePath(os.path.join(addon_path, "machine_guid")).decode('utf-8') + clientId = utils.settings('plex_client_Id') + if clientId: + utils.window('plex_client_Id', value=clientId) + self.logMsg("Unique device Id plex_client_Id loaded: %s" % clientId, 1) + return clientId - try: - GUID = open(GUID_file) - - except Exception as e: # machine_guid does not exists. - self.logMsg("Generating a new deviceid: %s" % e, 1) - clientId = str("%012X" % uuid4()) - GUID = open(GUID_file, 'w') - GUID.write(clientId) - - else: # machine_guid already exists. Get guid. - clientId = GUID.read() - - finally: - GUID.close() - - self.logMsg("DeviceId loaded: %s" % clientId, 1) - utils.window('emby_deviceId', value=clientId) - - return clientId \ No newline at end of file + self.logMsg("Generating a new deviceid.", 0) + clientId = str(uuid4()) + utils.settings('plex_client_Id', value=clientId) + utils.window('plex_client_Id', value=clientId) + self.logMsg("Unique device Id plex_client_Id loaded: %s" % clientId, 1) + return clientId diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 8bf899f7..843f769d 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -46,7 +46,7 @@ class InitialSetup(): ##### SERVER INFO ##### - self.logMsg("Initial setup called.", 2) + self.logMsg("Initial setup called.", 0) server = self.userClient.getServer() clientId = self.clientInfo.getDeviceId() serverid = self.userClient.getServerId() @@ -63,7 +63,10 @@ class InitialSetup(): 'Could not login to plex.tv.', 'Please try signing in again.' ) - plexLogin, plexToken = self.plx.GetPlexLoginAndPassword() + result = self.plx.PlexTvSignInWithPin() + if result: + plexLogin = result['username'] + plexToken = result['token'] elif chk == "": dialog = xbmcgui.Dialog() dialog.ok( @@ -73,18 +76,20 @@ class InitialSetup(): ) # If a Plex server IP has already been set, return. if server: - self.logMsg("Server is already set.", 2) + self.logMsg("Server is already set.", 0) self.logMsg( "url: %s, Plex machineIdentifier: %s" % (server, serverid), - 2 - ) + 0) return # If not already retrieved myplex info, optionally let user sign in # to plex.tv. if not plexToken and myplexlogin == 'true': - plexLogin, plexToken = self.plx.GetPlexLoginAndPassword() + result = self.plx.PlexTvSignInWithPin() + if result: + plexLogin = result['username'] + plexToken = result['token'] # Get g_PMS list of servers (saved to plx.g_PMS) serverNum = 1 while serverNum > 0: @@ -110,10 +115,14 @@ class InitialSetup(): if serverNum == 0: break for server in serverlist: - dialoglist.append(str(server['name']) + ' (IP: ' + str(server['ip']) + ')') + if server['local'] == '1': + # server is in the same network as client + dialoglist.append(str(server['name']) + ' (nearby)') + else: + dialoglist.append(str(server['name'])) dialog = xbmcgui.Dialog() resp = dialog.select( - 'What Plex server would you like to connect to?', + 'Plex server to connect to?', dialoglist) server = serverlist[resp] activeServer = server['machineIdentifier'] @@ -129,15 +138,17 @@ class InitialSetup(): chk = self.plx.CheckConnection(url, server['accesstoken']) # Unauthorized if chk == 401: - dialog = xbmcgui.Dialog() dialog.ok( self.addonName, 'Not yet authorized for Plex server %s' % str(server['name']), 'Please sign in to plex.tv.' ) - plexLogin, plexToken = self.plx.GetPlexLoginAndPassword() - # Exit while loop if user cancels - if plexLogin == '': + result = self.plx.PlexTvSignInWithPin() + if result: + plexLogin = result['username'] + plexToken = result['token'] + else: + # Exit while loop if user cancels break # Problems connecting elif chk == '':