From 32ace844aaca70b78e3b872637a09694780e0c26 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 1 Jul 2017 12:32:23 +0200 Subject: [PATCH] Connection manager, part 1 --- resources/lib/PlexAPI.py | 1072 +----------------------- resources/lib/PlexFunctions.py | 44 +- resources/lib/connect/__init__.py | 1 + resources/lib/connect/plex_tv.py | 449 ++++++++++ resources/lib/connectmanager.py | 1010 ++++++++++++++++++++++ resources/lib/dialogs/serverconnect.py | 22 +- resources/lib/downloadutils.py | 7 +- resources/lib/entrypoint.py | 12 +- resources/lib/initialsetup.py | 598 +++---------- resources/lib/userclient.py | 11 +- resources/lib/utils.py | 44 +- resources/lib/variables.py | 1 + service.py | 15 +- 13 files changed, 1677 insertions(+), 1609 deletions(-) create mode 100644 resources/lib/connect/__init__.py create mode 100644 resources/lib/connect/plex_tv.py create mode 100644 resources/lib/connectmanager.py diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 03f360f6..01154bff 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -29,29 +29,22 @@ http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-python (and others...) """ - import logging -from time import time -import urllib2 -import socket -from threading import Thread -import xml.etree.ElementTree as etree from re import compile as re_compile, sub from json import dumps -from urllib import urlencode, quote_plus, unquote +from urllib import urlencode, unquote from os.path import basename, join from os import makedirs import xbmcgui -from xbmc import sleep, executebuiltin from xbmcvfs import exists import clientinfo as client from downloadutils import DownloadUtils from utils import window, settings, language as lang, tryDecode, tryEncode, \ DateToKodi, exists_dir -from PlexFunctions import PMSHttpsEnabled import plexdb_functions as plexdb +from PlexFunctions import get_transcode_image_path import variables as v import state @@ -64,1065 +57,6 @@ REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') ############################################################################### -class PlexAPI(): - def __init__(self): - self.g_PMS = {} - self.doUtils = DownloadUtils().downloadUrl - - def GetPlexLoginFromSettings(self): - """ - Returns a dict: - 'plexLogin': settings('plexLogin'), - 'plexToken': settings('plexToken'), - 'plexhome': settings('plexhome'), - 'plexid': settings('plexid'), - 'myplexlogin': settings('myplexlogin'), - 'plexAvatar': settings('plexAvatar'), - 'plexHomeSize': settings('plexHomeSize') - - Returns strings or unicode - - Returns empty strings '' for a setting if not found. - - myplexlogin is 'true' if user opted to log into plex.tv (the default) - plexhome is 'true' if plex home is used (the default) - """ - return { - 'plexLogin': settings('plexLogin'), - 'plexToken': settings('plexToken'), - 'plexhome': settings('plexhome'), - 'plexid': settings('plexid'), - 'myplexlogin': settings('myplexlogin'), - 'plexAvatar': settings('plexAvatar'), - 'plexHomeSize': settings('plexHomeSize') - } - - def GetPlexLoginAndPassword(self): - """ - Signs in to plex.tv. - - plexLogin, authtoken = GetPlexLoginAndPassword() - - Input: nothing - Output: - plexLogin plex.tv username - authtoken token for plex.tv - - Also writes 'plexLogin' and 'token_plex.tv' to Kodi settings file - If not logged in, empty strings are returned for both. - """ - retrievedPlexLogin = '' - plexLogin = 'dummy' - authtoken = '' - dialog = xbmcgui.Dialog() - while retrievedPlexLogin == '' and plexLogin != '': - # Enter plex.tv username. Or nothing to cancel. - plexLogin = dialog.input(lang(29999) + lang(39300), - type=xbmcgui.INPUT_ALPHANUM) - if plexLogin != "": - # Enter password for plex.tv user - plexPassword = dialog.input( - lang(39301) + plexLogin, - type=xbmcgui.INPUT_ALPHANUM, - option=xbmcgui.ALPHANUM_HIDE_INPUT) - retrievedPlexLogin, authtoken = self.MyPlexSignIn( - plexLogin, - plexPassword, - {'X-Plex-Client-Identifier': window('plex_client_Id')}) - log.debug("plex.tv username and token: %s, %s" - % (plexLogin, authtoken)) - if plexLogin == '': - # Could not sign in user - dialog.ok(lang(29999), lang(39302) + plexLogin) - # Write to Kodi settings file - settings('plexLogin', value=retrievedPlexLogin) - settings('plexToken', value=authtoken) - return (retrievedPlexLogin, authtoken) - - def PlexTvSignInWithPin(self): - """ - Prompts user to sign in by visiting https://plex.tv/pin - - Writes to Kodi settings file. Also returns: - { - 'plexhome': 'true' if Plex Home, 'false' otherwise - 'username': - 'avatar': URL to user avator - 'token': - 'plexid': Plex user ID - 'homesize': Number of Plex home users (defaults to '1') - } - Returns False if authentication did not work. - """ - code, identifier = self.GetPlexPin() - dialog = xbmcgui.Dialog() - if not code: - # Problems trying to contact plex.tv. Try again later - dialog.ok(lang(29999), lang(39303)) - return False - # Go to https://plex.tv/pin and enter the code: - # Or press No to cancel the sign in. - answer = dialog.yesno(lang(29999), - lang(39304) + "\n\n", - code + "\n\n", - lang(39311)) - if not answer: - return False - count = 0 - # Wait for approx 30 seconds (since the PIN is not visible anymore :-)) - while count < 30: - xml = self.CheckPlexTvSignin(identifier) - if xml is not False: - break - # Wait for 1 seconds - sleep(1000) - count += 1 - if xml is False: - # Could not sign in to plex.tv Try again later - dialog.ok(lang(29999), lang(39305)) - return False - # Parse xml - userid = xml.attrib.get('id') - home = xml.get('home', '0') - if home == '1': - home = 'true' - else: - home = 'false' - username = xml.get('username', '') - avatar = xml.get('thumb', '') - token = xml.findtext('authentication-token') - homeSize = xml.get('homeSize', '1') - result = { - 'plexhome': home, - 'username': username, - 'avatar': avatar, - 'token': token, - 'plexid': userid, - 'homesize': homeSize - } - settings('plexLogin', username) - settings('plexToken', token) - settings('plexhome', home) - settings('plexid', userid) - settings('plexAvatar', avatar) - settings('plexHomeSize', homeSize) - # Let Kodi log into plex.tv on startup from now on - settings('myplexlogin', 'true') - settings('plex_status', value=lang(39227)) - 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 - xml = self.doUtils('https://plex.tv/pins/%s.xml' % identifier, - authenticate=False) - try: - temp_token = xml.find('auth_token').text - except: - log.error("Could not find token in plex.tv answer") - return False - if not temp_token: - return False - # Use temp token to get the final plex credentials - xml = self.doUtils('https://plex.tv/users/account', - authenticate=False, - parameters={'X-Plex-Token': temp_token}) - return xml - - def GetPlexPin(self): - """ - For plex.tv sign-in: returns 4-digit code and identifier as 2 str - """ - code = None - identifier = None - # Download - xml = self.doUtils('https://plex.tv/pins.xml', - authenticate=False, - action_type="POST") - try: - xml.attrib - except: - log.error("Error, no PIN from plex.tv provided") - return None, None - code = xml.find('code').text - identifier = xml.find('id').text - log.info('Successfully retrieved code and id from plex.tv') - return code, identifier - - def CheckConnection(self, url, token=None, verifySSL=None): - """ - Checks connection to a Plex server, available at url. Can also be used - to check for connection with plex.tv. - - Override SSL to skip the check by setting verifySSL=False - if 'None', SSL will be checked (standard requests setting) - if 'True', SSL settings from file settings are used (False/True) - - Input: - url URL to Plex server (e.g. https://192.168.1.1:32400) - token appropriate token to access server. If None is passed, - the current token is used - Output: - False if server could not be reached or timeout occured - 200 if connection was successfull - int or other HTML status codes as received from the server - """ - # Add '/clients' to URL because then an authentication is necessary - # If a plex.tv URL was passed, this does not work. - headerOptions = None - if token is not None: - headerOptions = {'X-Plex-Token': token} - if verifySSL is True: - verifySSL = None if settings('sslverify') == 'true' \ - else False - if 'plex.tv' in url: - url = 'https://plex.tv/api/home/users' - else: - url = url + '/library/onDeck' - log.debug("Checking connection to server %s with verifySSL=%s" - % (url, verifySSL)) - count = 0 - while count < 1: - answer = self.doUtils(url, - authenticate=False, - headerOptions=headerOptions, - verifySSL=verifySSL, - timeout=10) - if answer is None: - log.debug("Could not connect to %s" % url) - count += 1 - sleep(500) - continue - try: - # xml received? - answer.attrib - except: - if answer is True: - # Maybe no xml but connection was successful nevertheless - answer = 200 - else: - # Success - we downloaded an xml! - answer = 200 - # We could connect but maybe were not authenticated. No worries - log.debug("Checking connection successfull. Answer: %s" % answer) - return answer - log.debug('Failed to connect to %s too many times. PMS is dead' % url) - return False - - def GetgPMSKeylist(self): - """ - Returns a list of all keys that are saved for every entry in the - g_PMS variable. - """ - keylist = [ - 'address', - 'baseURL', - 'enableGzip', - 'ip', - 'local', - 'name', - 'owned', - 'port', - 'scheme' - ] - return keylist - - def declarePMS(self, uuid, name, scheme, ip, port): - """ - Plex Media Server handling - - parameters: - uuid - PMS ID - name, scheme, ip, port, type, owned, token - """ - address = ip + ':' + port - baseURL = scheme + '://' + ip + ':' + port - self.g_PMS[uuid] = { - 'name': name, - 'scheme': scheme, - 'ip': ip, - 'port': port, - 'address': address, - 'baseURL': baseURL, - 'local': '1', - 'owned': '1', - 'accesstoken': '', - 'enableGzip': False - } - - def updatePMSProperty(self, uuid, tag, value): - # set property element of PMS by UUID - try: - self.g_PMS[uuid][tag] = value - except: - log.error('%s has not yet been declared ' % uuid) - return False - - def getPMSProperty(self, uuid, tag): - # get name of PMS by UUID - try: - answ = self.g_PMS[uuid].get(tag, '') - except: - log.error('%s not found in PMS catalogue' % uuid) - answ = False - return answ - - def PlexGDM(self): - """ - PlexGDM - - parameters: - none - result: - PMS_list - dict() of PMSs found - """ - import struct - - IP_PlexGDM = '239.0.0.250' # multicast to PMS - Port_PlexGDM = 32414 - Msg_PlexGDM = 'M-SEARCH * HTTP/1.0' - - # setup socket for discovery -> multicast message - GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - GDM.settimeout(2.0) - - # Set the time-to-live for messages to 2 for local network - ttl = struct.pack('b', 2) - GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - - returnData = [] - try: - # Send data to the multicast group - GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM)) - - # Look for responses from all recipients - while True: - try: - data, server = GDM.recvfrom(1024) - returnData.append({'from': server, - 'data': data}) - except socket.timeout: - break - except Exception as e: - # Probably error: (101, 'Network is unreachable') - log.error(e) - import traceback - log.error("Traceback:\n%s" % traceback.format_exc()) - finally: - GDM.close() - - pmsList = {} - for response in returnData: - update = {'ip': response.get('from')[0]} - # Check if we had a positive HTTP response - if "200 OK" not in response.get('data'): - continue - for each in response.get('data').split('\n'): - # decode response data - update['discovery'] = "auto" - # update['owned']='1' - # update['master']= 1 - # update['role']='master' - - if "Content-Type:" in each: - update['content-type'] = each.split(':')[1].strip() - elif "Resource-Identifier:" in each: - update['uuid'] = each.split(':')[1].strip() - elif "Name:" in each: - update['serverName'] = tryDecode( - each.split(':')[1].strip()) - elif "Port:" in each: - update['port'] = each.split(':')[1].strip() - elif "Updated-At:" in each: - update['updated'] = each.split(':')[1].strip() - elif "Version:" in each: - update['version'] = each.split(':')[1].strip() - pmsList[update['uuid']] = update - return pmsList - - def discoverPMS(self, IP_self, plexToken=None): - """ - parameters: - IP_self Own IP - optional: - plexToken token for plex.tv - result: - self.g_PMS dict set - """ - self.g_PMS = {} - - # Look first for local PMS in the LAN - pmsList = self.PlexGDM() - log.debug('PMS found in the local LAN via GDM: %s' % pmsList) - - # Get PMS from plex.tv - if plexToken: - log.info('Checking with plex.tv for more PMS to connect to') - self.getPMSListFromMyPlex(plexToken) - else: - log.info('No plex token supplied, only checked LAN for PMS') - - for uuid in pmsList: - PMS = pmsList[uuid] - if PMS['uuid'] in self.g_PMS: - log.debug('We already know of PMS %s from plex.tv' - % PMS['serverName']) - # Update with GDM data - potentially more reliable than plex.tv - self.updatePMSProperty(PMS['uuid'], 'ip', PMS['ip']) - self.updatePMSProperty(PMS['uuid'], 'port', PMS['port']) - self.updatePMSProperty(PMS['uuid'], 'local', '1') - self.updatePMSProperty(PMS['uuid'], 'scheme', 'http') - self.updatePMSProperty(PMS['uuid'], - 'baseURL', - 'http://%s:%s' % (PMS['ip'], - PMS['port'])) - else: - self.declarePMS(PMS['uuid'], PMS['serverName'], 'http', - PMS['ip'], PMS['port']) - # Ping to check whether we need HTTPs or HTTP - https = PMSHttpsEnabled('%s:%s' % (PMS['ip'], PMS['port'])) - if https is None: - # Error contacting url. Skip for now - continue - elif https is True: - self.updatePMSProperty(PMS['uuid'], 'scheme', 'https') - self.updatePMSProperty( - PMS['uuid'], - 'baseURL', - 'https://%s:%s' % (PMS['ip'], PMS['port'])) - else: - # Already declared with http - pass - - # install plex.tv "virtual" PMS - for myPlex, PlexHome - # self.declarePMS('plex.tv', 'plex.tv', 'https', 'plex.tv', '443') - # self.updatePMSProperty('plex.tv', 'local', '-') - # self.updatePMSProperty('plex.tv', 'owned', '-') - # self.updatePMSProperty( - # 'plex.tv', 'accesstoken', plexToken) - # (remote and local) servers from plex.tv - - def getPMSListFromMyPlex(self, token): - """ - getPMSListFromMyPlex - - get Plex media Server List from plex.tv/pms/resources - """ - xml = self.doUtils('https://plex.tv/api/resources', - authenticate=False, - parameters={'includeHttps': 1}, - headerOptions={'X-Plex-Token': token}) - try: - xml.attrib - except AttributeError: - log.error('Could not get list of PMS from plex.tv') - return - - import Queue - queue = Queue.Queue() - threadQueue = [] - - maxAgeSeconds = 2*60*60*24 - for Dir in xml.findall('Device'): - if 'server' not in Dir.get('provides'): - # No PMS - skip - continue - 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() - int(Dir.get('lastSeenAt')) - if infoAge > maxAgeSeconds: - log.debug("Server %s not seen for 2 days - skipping." - % PMS['name']) - continue - - PMS['uuid'] = Dir.get('clientIdentifier') - PMS['token'] = Dir.get('accessToken', token) - PMS['owned'] = Dir.get('owned', '1') - PMS['local'] = Dir.get('publicAddressMatches') - PMS['ownername'] = Dir.get('sourceTitle', '') - PMS['path'] = '/' - PMS['options'] = None - - # Try a local connection first - # Backup to remote connection, if that failes - PMS['connections'] = [] - for Con in Dir.findall('Connection'): - if Con.get('local') == '1': - PMS['connections'].append(Con) - # Append non-local - for Con in Dir.findall('Connection'): - if Con.get('local') != '1': - PMS['connections'].append(Con) - - t = Thread(target=self.pokePMS, - args=(PMS, queue)) - threadQueue.append(t) - - maxThreads = 5 - threads = [] - # poke PMS, own thread for each PMS - while True: - # Remove finished threads - for t in threads: - if not t.isAlive(): - threads.remove(t) - if len(threads) < maxThreads: - try: - t = threadQueue.pop() - except IndexError: - # We have done our work - break - else: - t.start() - threads.append(t) - else: - sleep(50) - - # wait for requests being answered - for t in threads: - t.join() - - # declare new PMSs - while not queue.empty(): - PMS = queue.get() - self.declarePMS(PMS['uuid'], PMS['name'], - PMS['protocol'], PMS['ip'], PMS['port']) - self.updatePMSProperty( - PMS['uuid'], 'accesstoken', PMS['token']) - self.updatePMSProperty( - PMS['uuid'], 'owned', PMS['owned']) - self.updatePMSProperty( - PMS['uuid'], 'local', PMS['local']) - # set in declarePMS, overwrite for https encryption - self.updatePMSProperty( - PMS['uuid'], 'baseURL', PMS['baseURL']) - self.updatePMSProperty( - PMS['uuid'], 'ownername', PMS['ownername']) - log.debug('Found PMS %s: %s' - % (PMS['uuid'], self.g_PMS[PMS['uuid']])) - queue.task_done() - - def pokePMS(self, PMS, queue): - data = PMS['connections'][0].attrib - if data['local'] == '1': - protocol = data['protocol'] - address = data['address'] - port = data['port'] - url = '%s://%s:%s' % (protocol, address, port) - else: - url = data['uri'] - if url.count(':') == 1: - url = '%s:%s' % (url, data['port']) - protocol, address, port = url.split(':', 2) - address = address.replace('/', '') - - xml = self.doUtils('%s/identity' % url, - authenticate=False, - headerOptions={'X-Plex-Token': PMS['token']}, - verifySSL=False, - timeout=10) - try: - xml.attrib['machineIdentifier'] - except (AttributeError, KeyError): - # No connection, delete the one we just tested - del PMS['connections'][0] - if len(PMS['connections']) > 0: - # Still got connections left, try them - return self.pokePMS(PMS, queue) - return - else: - # Connection successful - correct PMS? - if xml.get('machineIdentifier') == PMS['uuid']: - # process later - PMS['baseURL'] = url - PMS['protocol'] = protocol - PMS['ip'] = address - PMS['port'] = port - queue.put(PMS) - return - log.info('Found a PMS at %s, but the expected machineIdentifier of ' - '%s did not match the one we found: %s' - % (url, PMS['uuid'], xml.get('machineIdentifier'))) - - def MyPlexSignIn(self, username, password, options): - """ - MyPlex Sign In, Sign Out - - parameters: - username - Plex forum name, MyPlex login, or email address - password - options - dict() of PlexConnect-options as received from aTV - necessary: PlexConnectUDID - result: - username - authtoken - token for subsequent communication with MyPlex - """ - # MyPlex web address - MyPlexHost = 'plex.tv' - MyPlexSignInPath = '/users/sign_in.xml' - MyPlexURL = 'https://' + MyPlexHost + MyPlexSignInPath - - # create POST request - xargs = client.getXArgsDeviceInfo(options) - request = urllib2.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' - - passmanager = urllib2.HTTPPasswordMgr() - passmanager.add_password(MyPlexHost, MyPlexURL, username, password) - authhandler = urllib2.HTTPBasicAuthHandler(passmanager) - urlopener = urllib2.build_opener(authhandler) - - # sign in, get MyPlex response - try: - response = urlopener.open(request).read() - except urllib2.HTTPError as e: - if e.code == 401: - log.info("Authentication failed") - return ('', '') - else: - raise - # analyse response - XMLTree = etree.ElementTree(etree.fromstring(response)) - - el_username = XMLTree.find('username') - el_authtoken = XMLTree.find('authentication-token') - if el_username is None or \ - el_authtoken is None: - username = '' - authtoken = '' - else: - username = el_username.text - authtoken = el_authtoken.text - return (username, authtoken) - - def MyPlexSignOut(self, authtoken): - """ - TO BE DONE! - """ - # MyPlex web address - MyPlexHost = 'plex.tv' - MyPlexSignOutPath = '/users/sign_out.xml' - MyPlexURL = 'http://' + MyPlexHost + MyPlexSignOutPath - - # create POST request - xargs = {'X-Plex-Token': authtoken} - request = urllib2.Request(MyPlexURL, None, xargs) - # turn into 'POST' - done automatically with data!=None. But we don't - # have data. - request.get_method = lambda: 'POST' - - response = urllib2.urlopen(request).read() - - def GetUserArtworkURL(self, username): - """ - Returns the URL for the user's Avatar. Or False if something went - wrong. - """ - plexToken = settings('plexToken') - users = self.MyPlexListHomeUsers(plexToken) - url = '' - # If an error is encountered, set to False - if not users: - log.info("Couldnt get user from plex.tv. No URL for user avatar") - return False - for user in users: - if username in user['title']: - url = user['thumb'] - log.debug("Avatar url for user %s is: %s" % (username, url)) - return url - - def ChoosePlexHomeUser(self, plexToken): - """ - Let's user choose from a list of Plex home users. Will switch to that - user accordingly. - - Returns a dict: - { - 'username': Unicode - 'userid': '' Plex ID of the user - 'token': '' User's token - 'protected': True if PIN is needed, else False - } - - Will return False if something went wrong (wrong PIN, no connection) - """ - dialog = xbmcgui.Dialog() - - # Get list of Plex home users - users = self.MyPlexListHomeUsers(plexToken) - if not users: - log.error("User download failed.") - return False - - userlist = [] - userlistCoded = [] - for user in users: - username = user['title'] - userlist.append(username) - # To take care of non-ASCII usernames - userlistCoded.append(tryEncode(username)) - usernumber = len(userlist) - - username = '' - usertoken = '' - trials = 0 - while trials < 3: - if usernumber > 1: - # Select user - user_select = dialog.select( - lang(29999) + lang(39306), - userlistCoded) - if user_select == -1: - log.info("No user selected.") - settings('username', value='') - executebuiltin('Addon.OpenSettings(%s)' - % v.ADDON_ID) - return False - # Only 1 user received, choose that one - else: - user_select = 0 - selected_user = userlist[user_select] - log.info("Selected user: %s" % selected_user) - user = users[user_select] - # Ask for PIN, if protected: - pin = None - if user['protected'] == '1': - log.debug('Asking for users PIN') - pin = dialog.input( - lang(39307) + selected_user, - '', - xbmcgui.INPUT_NUMERIC, - xbmcgui.ALPHANUM_HIDE_INPUT) - # User chose to cancel - # Plex bug: don't call url for protected user with empty PIN - if not pin: - trials += 1 - continue - # Switch to this Plex Home user, if applicable - result = self.PlexSwitchHomeUser( - user['id'], - pin, - plexToken, - settings('plex_machineIdentifier')) - if result: - # Successfully retrieved username: break out of while loop - username = result['username'] - usertoken = result['usertoken'] - break - # Couldn't get user auth - else: - trials += 1 - # Could not login user, please try again - if not dialog.yesno(lang(29999), - lang(39308) + selected_user, - lang(39309)): - # User chose to cancel - break - if not username: - log.error('Failed signing in a user to plex.tv') - executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) - return False - return { - 'username': username, - 'userid': user['id'], - 'protected': True if user['protected'] == '1' else False, - 'token': usertoken - } - - def PlexSwitchHomeUser(self, userId, pin, token, machineIdentifier): - """ - Retrieves Plex home token for a Plex home user. - Returns False if unsuccessful - - Input: - userId id of the Plex home user - pin PIN of the Plex home user, if protected - token token for plex.tv - - Output: - { - 'username' - 'usertoken' Might be empty strings if no token found - for the machineIdentifier that was chosen - } - - settings('userid') and settings('username') with new plex token - """ - log.info('Switching to user %s' % userId) - url = 'https://plex.tv/api/home/users/' + userId + '/switch' - if pin: - url += '?pin=' + pin - answer = self.doUtils(url, - authenticate=False, - action_type="POST", - headerOptions={'X-Plex-Token': token}) - try: - answer.attrib - except: - log.error('Error: plex.tv switch HomeUser change failed') - return False - - username = answer.attrib.get('title', '') - token = answer.attrib.get('authenticationToken', '') - - # Write to settings file - settings('username', username) - settings('accessToken', token) - settings('userid', answer.attrib.get('id', '')) - settings('plex_restricteduser', - 'true' if answer.attrib.get('restricted', '0') == '1' - else 'false') - state.RESTRICTED_USER = True if \ - answer.attrib.get('restricted', '0') == '1' else False - - # Get final token to the PMS we've chosen - url = 'https://plex.tv/api/resources?includeHttps=1' - xml = self.doUtils(url, - authenticate=False, - headerOptions={'X-Plex-Token': token}) - try: - xml.attrib - except: - log.error('Answer from plex.tv not as excepted') - # Set to empty iterable list for loop - xml = [] - - found = 0 - log.debug('Our machineIdentifier is %s' % machineIdentifier) - for device in xml: - identifier = device.attrib.get('clientIdentifier') - log.debug('Found a Plex machineIdentifier: %s' % identifier) - if (identifier in machineIdentifier or - machineIdentifier in identifier): - found += 1 - token = device.attrib.get('accessToken') - - result = { - 'username': username, - } - if found == 0: - log.info('No tokens found for your server! Using empty string') - result['usertoken'] = '' - else: - result['usertoken'] = token - log.info('Plex.tv switch HomeUser change successfull for user %s' - % username) - return result - - def MyPlexListHomeUsers(self, token): - """ - Returns a list for myPlex home users for the current plex.tv account. - - Input: - token for plex.tv - Output: - List of users, where one entry is of the form: - "id": userId, - "admin": '1'/'0', - "guest": '1'/'0', - "restricted": '1'/'0', - "protected": '1'/'0', - "email": email, - "title": title, - "username": username, - "thumb": thumb_url - } - If any value is missing, None is returned instead (or "" from plex.tv) - If an error is encountered, False is returned - """ - xml = self.doUtils('https://plex.tv/api/home/users/', - authenticate=False, - headerOptions={'X-Plex-Token': token}) - try: - xml.attrib - except: - log.error('Download of Plex home users failed.') - return False - users = [] - for user in xml: - users.append(user.attrib) - return users - - def getDirectVideoPath(self, key, AuthToken): - """ - Direct Video Play support - - parameters: - path - AuthToken - Indirect - media indirect specified, grab child XML to gain real path - options - result: - final path to media file - """ - if key.startswith('http://') or key.startswith('https://'): # external address - keep - path = key - else: - if AuthToken == '': - path = key - else: - xargs = dict() - xargs['X-Plex-Token'] = AuthToken - if key.find('?') == -1: - path = key + '?' + urlencode(xargs) - else: - path = key + '&' + urlencode(xargs) - - return path - - def getTranscodeImagePath(self, key, AuthToken, path, width, height): - """ - Transcode Image support - - parameters: - key - AuthToken - path - source path of current XML: path[srcXML] - width - height - result: - final path to image file - """ - if key.startswith('http://') or key.startswith('https://'): # external address - can we get a transcoding request for external images? - path = key - elif key.startswith('/'): # internal full path. - path = 'http://127.0.0.1:32400' + key - else: # internal path, add-on - path = 'http://127.0.0.1:32400' + path + '/' + key - path = tryEncode(path) - - # This is bogus (note the extra path component) but ATV is stupid when it comes to caching images, it doesn't use querystrings. - # Fortunately PMS is lenient... - transcodePath = '/photo/:/transcode/' + \ - str(width) + 'x' + str(height) + '/' + quote_plus(path) - - args = dict() - args['width'] = width - args['height'] = height - args['url'] = path - - if not AuthToken == '': - args['X-Plex-Token'] = AuthToken - - return transcodePath + '?' + urlencode(args) - - def getDirectImagePath(self, path, AuthToken): - """ - Direct Image support - - parameters: - path - AuthToken - result: - final path to image file - """ - if not AuthToken == '': - xargs = dict() - xargs['X-Plex-Token'] = AuthToken - if path.find('?') == -1: - path = path + '?' + urlencode(xargs) - else: - path = path + '&' + urlencode(xargs) - - return path - - def getTranscodeAudioPath(self, path, AuthToken, options, maxAudioBitrate): - """ - Transcode Audio support - - parameters: - path - AuthToken - options - dict() of PlexConnect-options as received from aTV - maxAudioBitrate - [kbps] - result: - final path to pull in PMS transcoder - """ - UDID = options['PlexConnectUDID'] - - transcodePath = '/music/:/transcode/universal/start.mp3?' - - args = dict() - args['path'] = path - args['session'] = UDID - args['protocol'] = 'http' - args['maxAudioBitrate'] = maxAudioBitrate - - xargs = client.getXArgsDeviceInfo(options) - if not AuthToken == '': - xargs['X-Plex-Token'] = AuthToken - - return transcodePath + urlencode(args) + '&' + urlencode(xargs) - - def getDirectAudioPath(self, path, AuthToken): - """ - Direct Audio support - - parameters: - path - AuthToken - result: - final path to audio file - """ - if not AuthToken == '': - xargs = dict() - xargs['X-Plex-Token'] = AuthToken - if path.find('?') == -1: - path = path + '?' + urlencode(xargs) - else: - path = path + '&' + urlencode(xargs) - - return path - - def returnServerList(self, data): - """ - Returns a nicer list of all servers found in data, where data is in - g_PMS format, for the client device with unique ID ATV_udid - - Input: - data e.g. self.g_PMS - - Output: List of all servers, with an entry of the form: - { - 'name': friendlyName, the Plex server's name - 'address': ip:port - 'ip': ip, without http/https - 'port': port - 'scheme': 'http'/'https', nice for checking for secure connections - 'local': '1'/'0', Is the server a local server? - 'owned': '1'/'0', Is the server owned by the user? - 'machineIdentifier': id, Plex server machine identifier - 'accesstoken': token Access token to this server - 'baseURL': baseURL scheme://ip:port - 'ownername' Plex username of PMS owner - } - """ - serverlist = [] - for key, value in data.items(): - serverlist.append({ - 'name': value.get('name'), - 'address': value.get('address'), - 'ip': value.get('ip'), - 'port': value.get('port'), - 'scheme': value.get('scheme'), - 'local': value.get('local'), - 'owned': value.get('owned'), - 'machineIdentifier': key, - 'accesstoken': value.get('accesstoken'), - 'baseURL': value.get('baseURL'), - 'ownername': value.get('ownername') - }) - return serverlist - - class API(): """ API(item) @@ -1217,7 +151,7 @@ class API(): extension not in v.KODI_SUPPORTED_IMAGES): # Let Plex transcode # max width/height supported by plex image transcoder is 1920x1080 - path = self.server + PlexAPI().getTranscodeImagePath( + path = self.server + get_transcode_image_path( self.item[0][0].attrib.get('key'), window('pms_token'), "%s%s" % (self.server, self.item[0][0].attrib.get('key')), diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index e6e9954e..146dd6e0 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -3,11 +3,12 @@ from logging import getLogger from urllib import urlencode from ast import literal_eval from urlparse import urlparse, parse_qsl +from urllib import quote_plus import re from copy import deepcopy import downloadutils -from utils import settings +from utils import settings, tryEncode from variables import PLEX_TO_KODI_TIMEFACTOR ############################################################################### @@ -434,7 +435,7 @@ def delete_item_from_pms(plexid): return False -def get_PMS_settings(url, token): +def get_pms_settings(url, token): """ Retrieve the PMS' settings via /:/ @@ -445,3 +446,42 @@ def get_PMS_settings(url, token): authenticate=False, verifySSL=False, headerOptions={'X-Plex-Token': token} if token else None) + + +def get_transcode_image_path(self, key, AuthToken, path, width, height): + """ + Transcode Image support + + parameters: + key + AuthToken + path - source path of current XML: path[srcXML] + width + height + result: + final path to image file + """ + # external address - can we get a transcoding request for external images? + if key.startswith('http://') or key.startswith('https://'): + path = key + elif key.startswith('/'): # internal full path. + path = 'http://127.0.0.1:32400' + key + else: # internal path, add-on + path = 'http://127.0.0.1:32400' + path + '/' + key + path = tryEncode(path) + + # This is bogus (note the extra path component) but ATV is stupid when it + # comes to caching images, it doesn't use querystrings. Fortunately PMS is + # lenient... + transcodePath = '/photo/:/transcode/' + \ + str(width) + 'x' + str(height) + '/' + quote_plus(path) + + args = dict() + args['width'] = width + args['height'] = height + args['url'] = path + + if not AuthToken == '': + args['X-Plex-Token'] = AuthToken + + return transcodePath + '?' + urlencode(args) diff --git a/resources/lib/connect/__init__.py b/resources/lib/connect/__init__.py new file mode 100644 index 00000000..b93054b3 --- /dev/null +++ b/resources/lib/connect/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/lib/connect/plex_tv.py b/resources/lib/connect/plex_tv.py new file mode 100644 index 00000000..f844785b --- /dev/null +++ b/resources/lib/connect/plex_tv.py @@ -0,0 +1,449 @@ +# -*- coding: utf-8 -*- +from logging import getLogger + +from xbmc import sleep, executebuiltin + +from utils import window, settings, dialog, language as lang, tryEncode +from clientinfo import getXArgsDeviceInfo +from downloadutils import DownloadUtils +import variables as v +import state + +############################################################################### +log = getLogger("PLEX."+__name__) + +############################################################################### + + +def my_plex_sign_in(username, password, options): + """ + MyPlex Sign In + + parameters: + username - Plex forum name, MyPlex login, or email address + password + options - dict() of PlexConnect-options as received from aTV - + necessary: PlexConnectUDID + result: + username + authtoken - token for subsequent communication with MyPlex + """ + # create POST request + xml = DownloadUtils().downloadUrl( + 'https://plex.tv/users/sign_in.xml', + action_type='POST', + headerOptions=getXArgsDeviceInfo(options), + authenticate=False, + auth=(username, password)) + + try: + xml.attrib + except AttributeError: + log.error('Could not sign in to plex.tv') + return ('', '') + + el_username = xml.find('username') + el_authtoken = xml.find('authentication-token') + if el_username is None or \ + el_authtoken is None: + username = '' + authtoken = '' + else: + username = el_username.text + authtoken = el_authtoken.text + return (username, authtoken) + + +def check_plex_tv_pin(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 + xml = DownloadUtils().downloadUrl( + 'https://plex.tv/pins/%s.xml' % identifier, + authenticate=False) + try: + temp_token = xml.find('auth_token').text + except: + log.error("Could not find token in plex.tv answer") + return False + if not temp_token: + return False + # Use temp token to get the final plex credentials + xml = DownloadUtils().downloadUrl('https://plex.tv/users/account', + authenticate=False, + parameters={'X-Plex-Token': temp_token}) + return xml + + +def get_plex_pin(): + """ + For plex.tv sign-in: returns 4-digit code and identifier as 2 str + """ + code = None + identifier = None + # Download + xml = DownloadUtils().downloadUrl('https://plex.tv/pins.xml', + authenticate=False, + action_type="POST") + try: + xml.attrib + except: + log.error("Error, no PIN from plex.tv provided") + return None, None + code = xml.find('code').text + identifier = xml.find('id').text + log.info('Successfully retrieved code and id from plex.tv') + return code, identifier + + +def get_plex_login_password(): + """ + Signs in to plex.tv. + + plexLogin, authtoken = get_plex_login_password() + + Input: nothing + Output: + plexLogin plex.tv username + authtoken token for plex.tv + + Also writes 'plexLogin' and 'token_plex.tv' to Kodi settings file + If not logged in, empty strings are returned for both. + """ + retrievedPlexLogin = '' + plexLogin = 'dummy' + authtoken = '' + while retrievedPlexLogin == '' and plexLogin != '': + # Enter plex.tv username. Or nothing to cancel. + plexLogin = dialog('input', + lang(29999) + lang(39300), + type='{alphanum}') + if plexLogin != "": + # Enter password for plex.tv user + plexPassword = dialog('input', + lang(39301) + plexLogin, + type='{alphanum}', + option='{hide_input}') + retrievedPlexLogin, authtoken = my_plex_sign_in( + plexLogin, + plexPassword, + {'X-Plex-Client-Identifier': window('plex_client_Id')}) + log.debug("plex.tv username and token: %s, %s" + % (plexLogin, authtoken)) + if plexLogin == '': + # Could not sign in user + dialog('ok', lang(29999), lang(39302) + plexLogin) + # Write to Kodi settings file + settings('plexLogin', value=retrievedPlexLogin) + settings('plexToken', value=authtoken) + return (retrievedPlexLogin, authtoken) + + +def plex_tv_sign_in_with_pin(): + """ + Prompts user to sign in by visiting https://plex.tv/pin + + Writes to Kodi settings file. Also returns: + { + 'plexhome': 'true' if Plex Home, 'false' otherwise + 'username': + 'avatar': URL to user avator + 'token': + 'plexid': Plex user ID + 'homesize': Number of Plex home users (defaults to '1') + } + Returns False if authentication did not work. + """ + code, identifier = get_plex_pin() + if not code: + # Problems trying to contact plex.tv. Try again later + dialog('ok', lang(29999), lang(39303)) + return False + # Go to https://plex.tv/pin and enter the code: + # Or press No to cancel the sign in. + answer = dialog('yesno', + lang(29999), + lang(39304) + "\n\n", + code + "\n\n", + lang(39311)) + if not answer: + return False + count = 0 + # Wait for approx 30 seconds (since the PIN is not visible anymore :-)) + while count < 30: + xml = check_plex_tv_pin(identifier) + if xml is not False: + break + # Wait for 1 seconds + sleep(1000) + count += 1 + if xml is False: + # Could not sign in to plex.tv Try again later + dialog('ok', lang(29999), lang(39305)) + return False + # Parse xml + userid = xml.attrib.get('id') + home = xml.get('home', '0') + if home == '1': + home = 'true' + else: + home = 'false' + username = xml.get('username', '') + avatar = xml.get('thumb', '') + token = xml.findtext('authentication-token') + homeSize = xml.get('homeSize', '1') + result = { + 'plexhome': home, + 'username': username, + 'avatar': avatar, + 'token': token, + 'plexid': userid, + 'homesize': homeSize + } + settings('plexLogin', username) + settings('plexToken', token) + settings('plexhome', home) + settings('plexid', userid) + settings('plexAvatar', avatar) + settings('plexHomeSize', homeSize) + # Let Kodi log into plex.tv on startup from now on + settings('myplexlogin', 'true') + settings('plex_status', value=lang(39227)) + return result + + +def list_plex_home_users(token): + """ + Returns a list for myPlex home users for the current plex.tv account. + + Input: + token for plex.tv + Output: + List of users, where one entry is of the form: + "id": userId, + "admin": '1'/'0', + "guest": '1'/'0', + "restricted": '1'/'0', + "protected": '1'/'0', + "email": email, + "title": title, + "username": username, + "thumb": thumb_url + } + If any value is missing, None is returned instead (or "" from plex.tv) + If an error is encountered, False is returned + """ + xml = DownloadUtils.downloadUrl('https://plex.tv/api/home/users/', + authenticate=False, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except: + log.error('Download of Plex home users failed.') + return False + users = [] + for user in xml: + users.append(user.attrib) + return users + + +def switch_home_user(userId, pin, token, machineIdentifier): + """ + Retrieves Plex home token for a Plex home user. + Returns False if unsuccessful + + Input: + userId id of the Plex home user + pin PIN of the Plex home user, if protected + token token for plex.tv + + Output: + { + 'username' + 'usertoken' Might be empty strings if no token found + for the machineIdentifier that was chosen + } + + settings('userid') and settings('username') with new plex token + """ + log.info('Switching to user %s' % userId) + url = 'https://plex.tv/api/home/users/' + userId + '/switch' + if pin: + url += '?pin=' + pin + answer = DownloadUtils.downloadUrl( + url, + authenticate=False, + action_type="POST", + headerOptions={'X-Plex-Token': token}) + try: + answer.attrib + except: + log.error('Error: plex.tv switch HomeUser change failed') + return False + + username = answer.attrib.get('title', '') + token = answer.attrib.get('authenticationToken', '') + + # Write to settings file + settings('username', username) + settings('accessToken', token) + settings('userid', answer.attrib.get('id', '')) + settings('plex_restricteduser', + 'true' if answer.attrib.get('restricted', '0') == '1' + else 'false') + state.RESTRICTED_USER = True if \ + answer.attrib.get('restricted', '0') == '1' else False + + # Get final token to the PMS we've chosen + url = 'https://plex.tv/api/resources?includeHttps=1' + xml = DownloadUtils.downloadUrl(url, + authenticate=False, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except: + log.error('Answer from plex.tv not as excepted') + # Set to empty iterable list for loop + xml = [] + + found = 0 + log.debug('Our machineIdentifier is %s' % machineIdentifier) + for device in xml: + identifier = device.attrib.get('clientIdentifier') + log.debug('Found a Plex machineIdentifier: %s' % identifier) + if (identifier in machineIdentifier or + machineIdentifier in identifier): + found += 1 + token = device.attrib.get('accessToken') + + result = { + 'username': username, + } + if found == 0: + log.info('No tokens found for your server! Using empty string') + result['usertoken'] = '' + else: + result['usertoken'] = token + log.info('Plex.tv switch HomeUser change successfull for user %s' + % username) + return result + + +def ChoosePlexHomeUser(plexToken): + """ + Let's user choose from a list of Plex home users. Will switch to that + user accordingly. + + Returns a dict: + { + 'username': Unicode + 'userid': '' Plex ID of the user + 'token': '' User's token + 'protected': True if PIN is needed, else False + } + + Will return False if something went wrong (wrong PIN, no connection) + """ + # Get list of Plex home users + users = list_plex_home_users(plexToken) + if not users: + log.error("User download failed.") + return False + + userlist = [] + userlistCoded = [] + for user in users: + username = user['title'] + userlist.append(username) + # To take care of non-ASCII usernames + userlistCoded.append(tryEncode(username)) + usernumber = len(userlist) + + username = '' + usertoken = '' + trials = 0 + while trials < 3: + if usernumber > 1: + # Select user + user_select = dialog('select', + lang(29999) + lang(39306), + userlistCoded) + if user_select == -1: + log.info("No user selected.") + settings('username', value='') + executebuiltin('Addon.OpenSettings(%s)' + % v.ADDON_ID) + return False + # Only 1 user received, choose that one + else: + user_select = 0 + selected_user = userlist[user_select] + log.info("Selected user: %s" % selected_user) + user = users[user_select] + # Ask for PIN, if protected: + pin = None + if user['protected'] == '1': + log.debug('Asking for users PIN') + pin = dialog('input', + lang(39307) + selected_user, + '', + type='{numeric}', + option='{hide_input}') + # User chose to cancel + # Plex bug: don't call url for protected user with empty PIN + if not pin: + trials += 1 + continue + # Switch to this Plex Home user, if applicable + result = switch_home_user( + user['id'], + pin, + plexToken, + settings('plex_machineIdentifier')) + if result: + # Successfully retrieved username: break out of while loop + username = result['username'] + usertoken = result['usertoken'] + break + # Couldn't get user auth + else: + trials += 1 + # Could not login user, please try again + if not dialog('yesno', + lang(29999), + lang(39308) + selected_user, + lang(39309)): + # User chose to cancel + break + if not username: + log.error('Failed signing in a user to plex.tv') + executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) + return False + return { + 'username': username, + 'userid': user['id'], + 'protected': True if user['protected'] == '1' else False, + 'token': usertoken + } + + +def get_user_artwork_url(username): + """ + Returns the URL for the user's Avatar. Or False if something went + wrong. + """ + plexToken = settings('plexToken') + users = list_plex_home_users(plexToken) + url = '' + # If an error is encountered, set to False + if not users: + log.info("Couldnt get user from plex.tv. No URL for user avatar") + return False + for user in users: + if username in user['title']: + url = user['thumb'] + log.debug("Avatar url for user %s is: %s" % (username, url)) + return url diff --git a/resources/lib/connectmanager.py b/resources/lib/connectmanager.py new file mode 100644 index 00000000..be69e9b0 --- /dev/null +++ b/resources/lib/connectmanager.py @@ -0,0 +1,1010 @@ +# -*- coding: utf-8 -*- +############################################################################### +from logging import getLogger + +# from connect.connectionmanager import ConnectionManager +from downloadutils import DownloadUtils +from dialogs.serverconnect import ServerConnect +from connect.plex_tv import plex_tv_sign_in_with_pin +from userclient import UserClient +from utils import window, settings, tryEncode, language as lang, dialog +from PlexFunctions import GetMachineIdentifier, get_pms_settings +import variables as v + +############################################################################### + +log = getLogger("PLEX."+__name__) + +# STATE = connectionmanager.ConnectionState + +############################################################################### + + +def check_connection(url, token=None, verifySSL=None): + """ + Checks connection to a Plex server, available at url. Can also be used + to check for connection with plex.tv. + + Override SSL to skip the check by setting verifySSL=False + if 'None', SSL will be checked (standard requests setting) + if 'True', SSL settings from file settings are used (False/True) + + Input: + url URL to Plex server (e.g. https://192.168.1.1:32400) + token appropriate token to access server. If None is passed, + the current token is used + Output: + False if server could not be reached or timeout occured + 200 if connection was successfull + int or other HTML status codes as received from the server + """ + headerOptions = None + if token is not None: + headerOptions = {'X-Plex-Token': token} + if verifySSL is True: + verifySSL = None if settings('sslverify') == 'true' \ + else False + if 'plex.tv' in url: + url = 'https://plex.tv/api/home/users' + else: + url = url + '/library/onDeck' + log.debug("Checking connection to server %s with verifySSL=%s" + % (url, verifySSL)) + answer = DownloadUtils().downloadUrl(url, + authenticate=False, + headerOptions=headerOptions, + verifySSL=verifySSL) + if answer is None: + log.debug("Could not connect to %s" % url) + return False + try: + # xml received? + answer.attrib + except: + if answer is True: + # Maybe no xml but connection was successful nevertheless + answer = 200 + else: + # Success - we downloaded an xml! + answer = 200 + # We could connect but maybe were not authenticated. No worries + log.debug("Checking connection successfull. Answer: %s" % answer) + return answer + + +def get_plex_login_from_settings(): + """ + Returns a dict: + 'plexLogin': settings('plexLogin'), + 'plexToken': settings('plexToken'), + 'plexhome': settings('plexhome'), + 'plexid': settings('plexid'), + 'myplexlogin': settings('myplexlogin'), + 'plexAvatar': settings('plexAvatar'), + 'plexHomeSize': settings('plexHomeSize') + + Returns strings or unicode + + Returns empty strings '' for a setting if not found. + + myplexlogin is 'true' if user opted to log into plex.tv (the default) + plexhome is 'true' if plex home is used (the default) + """ + return { + 'plexLogin': settings('plexLogin'), + 'plexToken': settings('plexToken'), + 'plexhome': settings('plexhome'), + 'plexid': settings('plexid'), + 'myplexlogin': settings('myplexlogin'), + 'plexAvatar': settings('plexAvatar'), + 'plexHomeSize': settings('plexHomeSize') + } + + +class ConnectManager(object): + # Borg + __shared_state = {} + + def __init__(self): + # Borg + self.__dict__ = self.__shared_state + + log.debug('Instantiating') + self.doUtils = DownloadUtils().downloadUrl + self.server = UserClient().getServer() + self.serverid = settings('plex_machineIdentifier') + # Get Plex credentials from settings file, if they exist + plexdict = get_plex_login_from_settings() + self.myplexlogin = plexdict['myplexlogin'] == 'true' + self.plexLogin = plexdict['plexLogin'] + self.plexToken = plexdict['plexToken'] + self.plexid = plexdict['plexid'] + # Token for the PMS, not plex.tv + self.pms_token = settings('accessToken') + if self.plexToken: + log.debug('Found a plex.tv token in the settings') + + def update_state(self): + self.state = self.__connect.connect({'updateDateLastAccessed': False}) + return self.get_state() + + def get_sate(self): + window('emby_state.json', value=self.state) + return self.state + + def get_server(self, server, options={}): + self.state = self.__connect.connectToAddress(server, options) + return self.get_state() + + @classmethod + def get_address(cls, server): + return connectionmanager.getServerAddress(server, server['LastConnectionMode']) + + def clear_data(self): + self.__connect.clearData() + + def select_servers(self): + """ + Will return selected server or raise RuntimeError + """ + dia = ServerConnect("script-emby-connect-server.xml", + tryEncode(v.ADDON_PATH), + "default", + "1080i") + dia.doModal() + + if dia.is_server_selected(): + log.debug("Server selected") + return dia.get_server() + + elif dia._is_connect_login(): + log.debug("Login to plex.tv") + try: + # Login to emby connect + self._login_connect() + except RuntimeError: + pass + return self.select_servers() + + elif dia.is_manual_server(): + log.debug("Add manual server") + try: + # Add manual server address + return self.manual_server() + except RuntimeError: + return self.select_servers() + else: + raise RuntimeError("No server selected") + + def manual_server(self): + # Return server or raise error + dia = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH) + dia._set_connect_manager(self.__connect) + dia.doModal() + + if dia._is_connected(): + return dia.get_server() + else: + raise RuntimeError("Server is not connected") + + def _login_connect(self): + # Return connect user or raise error + dia = LoginConnect("script-emby-connect-login.xml", *XML_PATH) + dia._set_connect_manager(self.__connect) + dia.doModal() + + self.update_state() + + if dia.is_logged_in(): + return dia.get_user() + else: + raise RuntimeError("Connect user is not logged in") + + def login(self, server=None): + # Return user or raise error + server = server or self.state['Servers'][0] + server_address = connectionmanager.getServerAddress(server, server['LastConnectionMode']) + + users = ""; + try: + users = self.emby.getUsers(server_address) + except Exception as error: + log.info("Error getting users from server: " + str(error)) + + if not users: + try: + return self.login_manual(server_address) + except RuntimeError: + raise RuntimeError("No user selected") + + dia = UsersConnect("script-emby-connect-users.xml", *XML_PATH) + dia.set_server(server_address) + dia.set_users(users) + dia.doModal() + + if dia.is_user_selected(): + + user = dia.get_user() + username = user['Name'] + + if user['HasPassword']: + log.debug("User has password, present manual login") + try: + return self.login_manual(server_address, username) + except RuntimeError: + return self.login(server) + else: + try: + user = self.emby.loginUser(server_address, username) + except Exception as error: + log.info("Error logging in user: " + str(error)) + raise + + self.__connect.onAuthenticated(user) + return user + + elif dia.is_manual_login(): + try: + return self.login_manual(server_address) + except RuntimeError: + return self.login(server) + else: + raise RuntimeError("No user selected") + + def login_manual(self, server, user=None): + # Return manual login user authenticated or raise error + dia = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH) + dia.set_server(server) + dia.set_user(user) + dia.doModal() + + if dia.is_logged_in(): + user = dia.get_user() + self.__connect.onAuthenticated(user) + return user + else: + raise RuntimeError("User is not authenticated") + + def update_token(self, server): + + credentials = self.__connect.credentialProvider.getCredentials() + self.__connect.credentialProvider.addOrUpdateServer(credentials['Servers'], server) + + for server in self.get_state()['Servers']: + for cred_server in credentials['Servers']: + if server['Id'] == cred_server['Id']: + # Update token saved in current state + server.update(cred_server) + # Update the token in data.txt + self.__connect.credentialProvider.getCredentials(credentials) + + def _get_connect_servers(self): + + connect_servers = [] + servers = self.__connect.getAvailableServers() + for server in servers: + if 'ExchangeToken' in server: + result = self.connect_server(server) + if result['State'] == STATE['SignedIn']: + connect_servers.append(server) + + log.info(connect_servers) + return connect_servers + + def connect_server(self, server): + return self.__connect.connectToServer(server, {'updateDateLastAccessed': False}) + + def pick_pms(self, show_dialog=False): + """ + Searches for PMS in local Lan and optionally (if self.plexToken set) + also on plex.tv + show_dialog=True: let the user pick one + show_dialog=False: automatically pick PMS based on + machineIdentifier + + Returns the picked PMS' detail as a dict: + { + 'name': friendlyName, the Plex server's name + 'address': ip:port + 'ip': ip, without http/https + 'port': port + 'scheme': 'http'/'https', nice for checking for secure connections + 'local': '1'/'0', Is the server a local server? + 'owned': '1'/'0', Is the server owned by the user? + 'machineIdentifier': id, Plex server machine identifier + 'accesstoken': token Access token to this server + 'baseURL': baseURL scheme://ip:port + 'ownername' Plex username of PMS owner + } + + or None if unsuccessful + """ + server = None + # If no server is set, let user choose one + if not self.server or not self.serverid: + show_dialog = True + if show_dialog is True: + server = self.connectmanager.select_servers() + log.info("Server: %s", server) + server = self.__user_pick_pms() + else: + server = self.__auto_pick_pms() + if server is not None: + self.write_pms_settings(server['baseURL'], server['accesstoken']) + return server + + @staticmethod + def write_pms_settings(url, token): + """ + Sets certain settings for server by asking for the PMS' settings + Call with url: scheme://ip:port + """ + xml = get_pms_settings(url, token) + try: + xml.attrib + except AttributeError: + log.error('Could not get PMS settings for %s' % url) + return + for entry in xml: + if entry.attrib.get('id', '') == 'allowMediaDeletion': + settings('plex_allows_mediaDeletion', + value=entry.attrib.get('value', 'true')) + window('plex_allows_mediaDeletion', + value=entry.attrib.get('value', 'true')) + + def __auto_pick_pms(self): + """ + Will try to pick PMS based on machineIdentifier saved in file settings + but only once + + Returns server or None if unsuccessful + """ + httpsUpdated = False + checkedPlexTV = False + server = None + while True: + if httpsUpdated is False: + serverlist = self.__get_server_list() + for item in serverlist: + if item.get('machineIdentifier') == self.serverid: + server = item + if server is None: + name = settings('plex_servername') + log.warn('The PMS you have used before with a unique ' + 'machineIdentifier of %s and name %s is ' + 'offline' % (self.serverid, name)) + return + chk = self._checkServerCon(server) + if chk == 504 and httpsUpdated is False: + # Not able to use HTTP, try HTTPs for now + server['scheme'] = 'https' + httpsUpdated = True + continue + if chk == 401: + log.warn('Not yet authorized for Plex server %s' + % server['name']) + if self.check_plex_tv_signin() is True: + if checkedPlexTV is False: + # Try again + checkedPlexTV = True + httpsUpdated = False + continue + else: + log.warn('Not authorized even though we are signed ' + ' in to plex.tv correctly') + dialog('ok', + lang(29999), '%s %s' + % (lang(39214), + tryEncode(server['name']))) + return + else: + return + # Problems connecting + elif chk >= 400 or chk is False: + log.warn('Problems connecting to server %s. chk is %s' + % (server['name'], chk)) + return + log.info('We found a server to automatically connect to: %s' + % server['name']) + return server + + def __user_pick_pms(self): + """ + Lets user pick his/her PMS from a list + + Returns server or None if unsuccessful + """ + httpsUpdated = False + while True: + if httpsUpdated is False: + serverlist = self.__get_server_list() + # Exit if no servers found + if len(serverlist) == 0: + log.warn('No plex media servers found!') + dialog('ok', lang(29999), lang(39011)) + return + # Get a nicer list + dialoglist = [] + for server in serverlist: + if server['local'] == '1': + # server is in the same network as client. + # Add"local" + msg = lang(39022) + else: + # Add 'remote' + msg = lang(39054) + if server.get('ownername'): + # Display username if its not our PMS + dialoglist.append('%s (%s, %s)' + % (server['name'], + server['ownername'], + msg)) + else: + dialoglist.append('%s (%s)' + % (server['name'], msg)) + # Let user pick server from a list + resp = dialog('select', lang(39012), dialoglist) + if resp == -1: + # User cancelled + return + + server = serverlist[resp] + chk = self._checkServerCon(server) + if chk == 504 and httpsUpdated is False: + # Not able to use HTTP, try HTTPs for now + serverlist[resp]['scheme'] = 'https' + httpsUpdated = True + continue + httpsUpdated = False + if chk == 401: + log.warn('Not yet authorized for Plex server %s' + % server['name']) + # Please sign in to plex.tv + dialog('ok', + lang(29999), + lang(39013) + server['name'], + lang(39014)) + if self.plex_tv_signin() is False: + # Exit while loop if user cancels + return + # Problems connecting + elif chk >= 400 or chk is False: + # Problems connecting to server. Pick another server? + # Exit while loop if user chooses No + if not dialog('yesno', lang(29999), lang(39015)): + return + # Otherwise: connection worked! + else: + return server + + @staticmethod + def write_pms_to_settings(server): + """ + Saves server to file settings. server is a dict of the form: + { + 'name': friendlyName, the Plex server's name + 'address': ip:port + 'ip': ip, without http/https + 'port': port + 'scheme': 'http'/'https', nice for checking for secure connections + 'local': '1'/'0', Is the server a local server? + 'owned': '1'/'0', Is the server owned by the user? + 'machineIdentifier': id, Plex server machine identifier + 'accesstoken': token Access token to this server + 'baseURL': baseURL scheme://ip:port + 'ownername' Plex username of PMS owner + } + """ + settings('plex_machineIdentifier', server['machineIdentifier']) + settings('plex_servername', server['name']) + settings('plex_serverowned', + 'true' if server['owned'] == '1' + else 'false') + # Careful to distinguish local from remote PMS + if server['local'] == '1': + scheme = server['scheme'] + settings('ipaddress', server['ip']) + settings('port', server['port']) + log.debug("Setting SSL verify to false, because server is " + "local") + settings('sslverify', 'false') + else: + baseURL = server['baseURL'].split(':') + scheme = baseURL[0] + settings('ipaddress', baseURL[1].replace('//', '')) + settings('port', baseURL[2]) + log.debug("Setting SSL verify to true, because server is not " + "local") + settings('sslverify', 'true') + + if scheme == 'https': + settings('https', 'true') + else: + settings('https', 'false') + # And finally do some logging + log.debug("Writing to Kodi user settings file") + log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s " + % (server['machineIdentifier'], server['ip'], + server['port'], server['scheme'])) + + def plex_tv_signin(self): + """ + Signs (freshly) in to plex.tv (will be saved to file settings) + + Returns True if successful, or False if not + """ + result = plex_tv_sign_in_with_pin() + if result: + self.plexLogin = result['username'] + self.plexToken = result['token'] + self.plexid = result['plexid'] + return True + return False + + def check_plex_tv_signin(self): + """ + Checks existing connection to plex.tv. If not, triggers sign in + + Returns True if signed in, False otherwise + """ + answer = True + chk = check_connection('plex.tv', token=self.plexToken) + if chk in (401, 403): + # HTTP Error: unauthorized. Token is no longer valid + log.info('plex.tv connection returned HTTP %s' % str(chk)) + # Delete token in the settings + settings('plexToken', value='') + settings('plexLogin', value='') + # Could not login, please try again + dialog('ok', lang(29999), lang(39009)) + answer = self.plex_tv_signin() + elif chk is False or chk >= 400: + # Problems connecting to plex.tv. Network or internet issue? + log.info('Problems connecting to plex.tv; connection returned ' + 'HTTP %s' % str(chk)) + dialog('ok', lang(29999), lang(39010)) + answer = False + else: + log.info('plex.tv connection with token successful') + settings('plex_status', value=lang(39227)) + # Refresh the info from Plex.tv + xml = self.doUtils('https://plex.tv/users/account', + authenticate=False, + headerOptions={'X-Plex-Token': self.plexToken}) + try: + self.plexLogin = xml.attrib['title'] + except (AttributeError, KeyError): + log.error('Failed to update Plex info from plex.tv') + else: + settings('plexLogin', value=self.plexLogin) + home = 'true' if xml.attrib.get('home') == '1' else 'false' + settings('plexhome', value=home) + settings('plexAvatar', value=xml.attrib.get('thumb')) + settings('plexHomeSize', value=xml.attrib.get('homeSize', '1')) + log.info('Updated Plex info from plex.tv') + return answer + + def check_pms(self): + """ + Check the PMS that was set in file settings. + Will return False if we need to reconnect, because: + PMS could not be reached (no matter the authorization) + machineIdentifier did not match + + Will also set the PMS machineIdentifier in the file settings if it was + not set before + """ + answer = True + chk = check_connection(self.server, verifySSL=False) + if chk is False: + log.warn('Could not reach PMS %s' % self.server) + answer = False + if answer is True and not self.serverid: + log.info('No PMS machineIdentifier found for %s. Trying to ' + 'get the PMS unique ID' % self.server) + self.serverid = GetMachineIdentifier(self.server) + if self.serverid is None: + log.warn('Could not retrieve machineIdentifier') + answer = False + else: + settings('plex_machineIdentifier', value=self.serverid) + elif answer is True: + tempServerid = GetMachineIdentifier(self.server) + if tempServerid != self.serverid: + log.warn('The current PMS %s was expected to have a ' + 'unique machineIdentifier of %s. But we got ' + '%s. Pick a new server to be sure' + % (self.server, self.serverid, tempServerid)) + answer = False + return answer + + def __get_server_list(self): + """ + Returns a list of servers from GDM and possibly plex.tv + """ + self.discoverPMS(xbmc.getIPAddress(), + plexToken=self.plexToken) + serverlist = self.plx.returnServerList(self.plx.g_PMS) + log.debug('PMS serverlist: %s' % serverlist) + return serverlist + + def _checkServerCon(self, server): + """ + Checks for server's connectivity. Returns check_connection result + """ + # Re-direct via plex if remote - will lead to the correct SSL + # certificate + if server['local'] == '1': + url = '%s://%s:%s' \ + % (server['scheme'], server['ip'], server['port']) + # Deactive SSL verification if the server is local! + verifySSL = False + else: + url = server['baseURL'] + verifySSL = True + chk = check_connection(url, + token=server['accesstoken'], + verifySSL=verifySSL) + return chk + + def discoverPMS(self, IP_self, plexToken=None): + """ + parameters: + IP_self Own IP + optional: + plexToken token for plex.tv + result: + self.g_PMS dict set + """ + self.g_PMS = {} + + # Look first for local PMS in the LAN + pmsList = self.PlexGDM() + log.debug('PMS found in the local LAN via GDM: %s' % pmsList) + + # Get PMS from plex.tv + if plexToken: + log.info('Checking with plex.tv for more PMS to connect to') + self.getPMSListFromMyPlex(plexToken) + else: + log.info('No plex token supplied, only checked LAN for PMS') + + for uuid in pmsList: + PMS = pmsList[uuid] + if PMS['uuid'] in self.g_PMS: + log.debug('We already know of PMS %s from plex.tv' + % PMS['serverName']) + # Update with GDM data - potentially more reliable than plex.tv + self.updatePMSProperty(PMS['uuid'], 'ip', PMS['ip']) + self.updatePMSProperty(PMS['uuid'], 'port', PMS['port']) + self.updatePMSProperty(PMS['uuid'], 'local', '1') + self.updatePMSProperty(PMS['uuid'], 'scheme', 'http') + self.updatePMSProperty(PMS['uuid'], + 'baseURL', + 'http://%s:%s' % (PMS['ip'], + PMS['port'])) + else: + self.declarePMS(PMS['uuid'], PMS['serverName'], 'http', + PMS['ip'], PMS['port']) + # Ping to check whether we need HTTPs or HTTP + https = PMSHttpsEnabled('%s:%s' % (PMS['ip'], PMS['port'])) + if https is None: + # Error contacting url. Skip for now + continue + elif https is True: + self.updatePMSProperty(PMS['uuid'], 'scheme', 'https') + self.updatePMSProperty( + PMS['uuid'], + 'baseURL', + 'https://%s:%s' % (PMS['ip'], PMS['port'])) + else: + # Already declared with http + pass + + # install plex.tv "virtual" PMS - for myPlex, PlexHome + # self.declarePMS('plex.tv', 'plex.tv', 'https', 'plex.tv', '443') + # self.updatePMSProperty('plex.tv', 'local', '-') + # self.updatePMSProperty('plex.tv', 'owned', '-') + # self.updatePMSProperty( + # 'plex.tv', 'accesstoken', plexToken) + # (remote and local) servers from plex.tv + + def declarePMS(self, uuid, name, scheme, ip, port): + """ + Plex Media Server handling + + parameters: + uuid - PMS ID + name, scheme, ip, port, type, owned, token + """ + address = ip + ':' + port + baseURL = scheme + '://' + ip + ':' + port + self.g_PMS[uuid] = { + 'name': name, + 'scheme': scheme, + 'ip': ip, + 'port': port, + 'address': address, + 'baseURL': baseURL, + 'local': '1', + 'owned': '1', + 'accesstoken': '', + 'enableGzip': False + } + + def updatePMSProperty(self, uuid, tag, value): + # set property element of PMS by UUID + try: + self.g_PMS[uuid][tag] = value + except: + log.error('%s has not yet been declared ' % uuid) + return False + + def getPMSProperty(self, uuid, tag): + # get name of PMS by UUID + try: + answ = self.g_PMS[uuid].get(tag, '') + except: + log.error('%s not found in PMS catalogue' % uuid) + answ = False + return answ + + def PlexGDM(self): + """ + PlexGDM + + parameters: + none + result: + PMS_list - dict() of PMSs found + """ + import struct + + IP_PlexGDM = '239.0.0.250' # multicast to PMS + Port_PlexGDM = 32414 + Msg_PlexGDM = 'M-SEARCH * HTTP/1.0' + + # setup socket for discovery -> multicast message + GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + GDM.settimeout(2.0) + + # Set the time-to-live for messages to 2 for local network + ttl = struct.pack('b', 2) + GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + + returnData = [] + try: + # Send data to the multicast group + GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM)) + + # Look for responses from all recipients + while True: + try: + data, server = GDM.recvfrom(1024) + returnData.append({'from': server, + 'data': data}) + except socket.timeout: + break + except Exception as e: + # Probably error: (101, 'Network is unreachable') + log.error(e) + import traceback + log.error("Traceback:\n%s" % traceback.format_exc()) + finally: + GDM.close() + + pmsList = {} + for response in returnData: + update = {'ip': response.get('from')[0]} + # Check if we had a positive HTTP response + if "200 OK" not in response.get('data'): + continue + for each in response.get('data').split('\n'): + # decode response data + update['discovery'] = "auto" + # update['owned']='1' + # update['master']= 1 + # update['role']='master' + + if "Content-Type:" in each: + update['content-type'] = each.split(':')[1].strip() + elif "Resource-Identifier:" in each: + update['uuid'] = each.split(':')[1].strip() + elif "Name:" in each: + update['serverName'] = tryDecode( + each.split(':')[1].strip()) + elif "Port:" in each: + update['port'] = each.split(':')[1].strip() + elif "Updated-At:" in each: + update['updated'] = each.split(':')[1].strip() + elif "Version:" in each: + update['version'] = each.split(':')[1].strip() + pmsList[update['uuid']] = update + return pmsList + + def getPMSListFromMyPlex(self, token): + """ + getPMSListFromMyPlex + + get Plex media Server List from plex.tv/pms/resources + """ + xml = self.doUtils('https://plex.tv/api/resources', + authenticate=False, + parameters={'includeHttps': 1}, + headerOptions={'X-Plex-Token': token}) + try: + xml.attrib + except AttributeError: + log.error('Could not get list of PMS from plex.tv') + return + + import Queue + queue = Queue.Queue() + threadQueue = [] + + maxAgeSeconds = 2*60*60*24 + for Dir in xml.findall('Device'): + if 'server' not in Dir.get('provides'): + # No PMS - skip + continue + 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() - int(Dir.get('lastSeenAt')) + if infoAge > maxAgeSeconds: + log.debug("Server %s not seen for 2 days - skipping." + % PMS['name']) + continue + + PMS['uuid'] = Dir.get('clientIdentifier') + PMS['token'] = Dir.get('accessToken', token) + PMS['owned'] = Dir.get('owned', '1') + PMS['local'] = Dir.get('publicAddressMatches') + PMS['ownername'] = Dir.get('sourceTitle', '') + PMS['path'] = '/' + PMS['options'] = None + + # Try a local connection first + # Backup to remote connection, if that failes + PMS['connections'] = [] + for Con in Dir.findall('Connection'): + if Con.get('local') == '1': + PMS['connections'].append(Con) + # Append non-local + for Con in Dir.findall('Connection'): + if Con.get('local') != '1': + PMS['connections'].append(Con) + + t = Thread(target=self.pokePMS, + args=(PMS, queue)) + threadQueue.append(t) + + maxThreads = 5 + threads = [] + # poke PMS, own thread for each PMS + while True: + # Remove finished threads + for t in threads: + if not t.isAlive(): + threads.remove(t) + if len(threads) < maxThreads: + try: + t = threadQueue.pop() + except IndexError: + # We have done our work + break + else: + t.start() + threads.append(t) + else: + sleep(50) + + # wait for requests being answered + for t in threads: + t.join() + + # declare new PMSs + while not queue.empty(): + PMS = queue.get() + self.declarePMS(PMS['uuid'], PMS['name'], + PMS['protocol'], PMS['ip'], PMS['port']) + self.updatePMSProperty( + PMS['uuid'], 'accesstoken', PMS['token']) + self.updatePMSProperty( + PMS['uuid'], 'owned', PMS['owned']) + self.updatePMSProperty( + PMS['uuid'], 'local', PMS['local']) + # set in declarePMS, overwrite for https encryption + self.updatePMSProperty( + PMS['uuid'], 'baseURL', PMS['baseURL']) + self.updatePMSProperty( + PMS['uuid'], 'ownername', PMS['ownername']) + log.debug('Found PMS %s: %s' + % (PMS['uuid'], self.g_PMS[PMS['uuid']])) + queue.task_done() + + def pokePMS(self, PMS, queue): + data = PMS['connections'][0].attrib + if data['local'] == '1': + protocol = data['protocol'] + address = data['address'] + port = data['port'] + url = '%s://%s:%s' % (protocol, address, port) + else: + url = data['uri'] + if url.count(':') == 1: + url = '%s:%s' % (url, data['port']) + protocol, address, port = url.split(':', 2) + address = address.replace('/', '') + + xml = self.doUtils('%s/identity' % url, + authenticate=False, + headerOptions={'X-Plex-Token': PMS['token']}, + verifySSL=False, + timeout=10) + try: + xml.attrib['machineIdentifier'] + except (AttributeError, KeyError): + # No connection, delete the one we just tested + del PMS['connections'][0] + if len(PMS['connections']) > 0: + # Still got connections left, try them + return self.pokePMS(PMS, queue) + return + else: + # Connection successful - correct PMS? + if xml.get('machineIdentifier') == PMS['uuid']: + # process later + PMS['baseURL'] = url + PMS['protocol'] = protocol + PMS['ip'] = address + PMS['port'] = port + queue.put(PMS) + return + log.info('Found a PMS at %s, but the expected machineIdentifier of ' + '%s did not match the one we found: %s' + % (url, PMS['uuid'], xml.get('machineIdentifier'))) + + def returnServerList(self, data): + """ + Returns a nicer list of all servers found in data, where data is in + g_PMS format, for the client device with unique ID ATV_udid + + Input: + data e.g. self.g_PMS + + Output: List of all servers, with an entry of the form: + { + 'name': friendlyName, the Plex server's name + 'address': ip:port + 'ip': ip, without http/https + 'port': port + 'scheme': 'http'/'https', nice for checking for secure connections + 'local': '1'/'0', Is the server a local server? + 'owned': '1'/'0', Is the server owned by the user? + 'machineIdentifier': id, Plex server machine identifier + 'accesstoken': token Access token to this server + 'baseURL': baseURL scheme://ip:port + 'ownername' Plex username of PMS owner + } + """ + serverlist = [] + for key, value in data.items(): + serverlist.append({ + 'name': value.get('name'), + 'address': value.get('address'), + 'ip': value.get('ip'), + 'port': value.get('port'), + 'scheme': value.get('scheme'), + 'local': value.get('local'), + 'owned': value.get('owned'), + 'machineIdentifier': key, + 'accesstoken': value.get('accesstoken'), + 'baseURL': value.get('baseURL'), + 'ownername': value.get('ownername') + }) + return serverlist diff --git a/resources/lib/dialogs/serverconnect.py b/resources/lib/dialogs/serverconnect.py index 541ca6f9..e5077bde 100644 --- a/resources/lib/dialogs/serverconnect.py +++ b/resources/lib/dialogs/serverconnect.py @@ -1,20 +1,18 @@ # -*- coding: utf-8 -*- -################################################################################################## +############################################################################### -import logging +from logging import getLogger import xbmc import xbmcgui -import connect.connectionmanager as connectionmanager from utils import language as lang -################################################################################################## +############################################################################### -log = logging.getLogger("EMBY."+__name__) +log = getLogger("PLEX."+__name__) -CONN_STATE = connectionmanager.ConnectionState ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -30,7 +28,7 @@ BUSY = 204 EMBY_CONNECT = 205 MANUAL_SERVER = 206 -################################################################################################## +############################################################################### class ServerConnect(xbmcgui.WindowXMLDialog): @@ -43,11 +41,6 @@ class ServerConnect(xbmcgui.WindowXMLDialog): _connect_login = False _manual_server = False - - def __init__(self, *args, **kwargs): - - xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) - def set_args(self, **kwargs): # connect_manager, username, user_image, servers, emby_connect for key, value in kwargs.iteritems(): @@ -65,7 +58,6 @@ class ServerConnect(xbmcgui.WindowXMLDialog): def is_manual_server(self): return self._manual_server - def onInit(self): self.message = self.getControl(MESSAGE) @@ -77,13 +69,13 @@ class ServerConnect(xbmcgui.WindowXMLDialog): server_type = "wifi" if server.get('ExchangeToken') else "network" self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) - self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8'))) + self.getControl(USER_NAME).setLabel("%s %s" % ('Switch plex.tv user', self.username.decode('utf-8'))) if self.user_image is not None: self.getControl(USER_IMAGE).setImage(self.user_image) if not self.emby_connect: # Change connect user - self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]") + self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+'plex.tv user change'+"[/B][/UPPERCASE]") if self.servers: self.setFocus(self.list_) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index a30ab4d9..d1ee2aff 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -142,13 +142,16 @@ class DownloadUtils(): def downloadUrl(self, url, action_type="GET", postBody=None, parameters=None, authenticate=True, headerOptions=None, - verifySSL=True, timeout=None, return_response=False): + verifySSL=True, timeout=None, return_response=False, + auth=None): """ Override SSL check with verifySSL=False If authenticate=True, existing request session will be used/started Otherwise, 'empty' request will be made + auth=None or auth=('user', 'password') + Returns: None If an error occured True If connection worked but no body was received @@ -190,6 +193,8 @@ class DownloadUtils(): kwargs['params'] = parameters if timeout is not None: kwargs['timeout'] = timeout + if auth is not None: + kwargs['auth'] = auth # ACTUAL DOWNLOAD HAPPENING HERE try: diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index eaf360fb..879a4d72 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -37,9 +37,9 @@ def chooseServer(): """ log.info("Choosing PMS server requested, starting") - import initialsetup - setup = initialsetup.InitialSetup() - server = setup.PickPMS(showDialog=True) + from connectmanager import ConnectManager + connectmanager = ConnectManager() + server = connectmanager.pick_pms(show_dialog=True) if server is None: log.error('We did not connect to a new PMS, aborting') plex_command('SUSPEND_USER_CLIENT', 'False') @@ -47,7 +47,7 @@ def chooseServer(): return log.info("User chose server %s" % server['name']) - setup.WritePMStoSettings(server) + connectmanager.write_pms_to_settings(server) if not __LogOut(): return @@ -85,8 +85,8 @@ def togglePlexTV(): plex_command('PLEX_USERNAME', '') else: log.info('Login to plex.tv') - import initialsetup - initialsetup.InitialSetup().PlexTVSignIn() + from connectmanager import ConnectManager + ConnectManager().plex_tv_signin() dialog('notification', lang(29999), lang(39221), diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index f29afa72..e3e16cdf 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -1,512 +1,136 @@ # -*- coding: utf-8 -*- - ############################################################################### +from logging import getLogger +from xbmc import executebuiltin -import logging -import xbmc -import xbmcgui +from utils import settings, language as lang, advancedsettings_xml, dialog +from connectmanager import ConnectManager -from utils import settings, window, language as lang, tryEncode, \ - advancedsettings_xml -import downloadutils -from userclient import UserClient - -from PlexAPI import PlexAPI -from PlexFunctions import GetMachineIdentifier, get_PMS_settings import state from migration import check_migration ############################################################################### - -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) ############################################################################### -class InitialSetup(): +def setup(self): + """ + Initial setup. Run once upon startup. - def __init__(self): - log.debug('Entering initialsetup class') - self.doUtils = downloadutils.DownloadUtils().downloadUrl - self.plx = PlexAPI() - self.dialog = xbmcgui.Dialog() + Check server, user, direct paths, music, direct stream if not direct + path. + """ + log.info("Initial setup called") + connectmanager = ConnectManager() - self.server = UserClient().getServer() - self.serverid = settings('plex_machineIdentifier') - # Get Plex credentials from settings file, if they exist - plexdict = self.plx.GetPlexLoginFromSettings() - self.myplexlogin = plexdict['myplexlogin'] == 'true' - self.plexLogin = plexdict['plexLogin'] - self.plexToken = plexdict['plexToken'] - self.plexid = plexdict['plexid'] - # Token for the PMS, not plex.tv - self.pms_token = settings('accessToken') - if self.plexToken: - log.debug('Found a plex.tv token in the settings') + # Get current Kodi video cache setting + cache, _ = advancedsettings_xml(['cache', 'memorysize']) + if cache is None: + # Kodi default cache + cache = '20971520' + else: + cache = str(cache.text) + log.info('Current Kodi video memory cache in bytes: %s' % cache) + settings('kodi_video_cache', value=cache) - def PlexTVSignIn(self): - """ - Signs (freshly) in to plex.tv (will be saved to file settings) + # Do we need to migrate stuff? + check_migration() - Returns True if successful, or False if not - """ - result = self.plx.PlexTvSignInWithPin() - if result: - self.plexLogin = result['username'] - self.plexToken = result['token'] - self.plexid = result['plexid'] - return True - return False + # Optionally sign into plex.tv. Will not be called on very first run + # as plexToken will be '' + settings('plex_status', value=lang(39226)) + if connectmanager.plexToken and connectmanager.myplexlogin: + connectmanager.check_plex_tv_signin() - def CheckPlexTVSignIn(self): - """ - Checks existing connection to plex.tv. If not, triggers sign in - - Returns True if signed in, False otherwise - """ - answer = True - chk = self.plx.CheckConnection('plex.tv', token=self.plexToken) - if chk in (401, 403): - # HTTP Error: unauthorized. Token is no longer valid - log.info('plex.tv connection returned HTTP %s' % str(chk)) - # Delete token in the settings - settings('plexToken', value='') - settings('plexLogin', value='') - # Could not login, please try again - self.dialog.ok(lang(29999), lang(39009)) - answer = self.PlexTVSignIn() - elif chk is False or chk >= 400: - # Problems connecting to plex.tv. Network or internet issue? - log.info('Problems connecting to plex.tv; connection returned ' - 'HTTP %s' % str(chk)) - self.dialog.ok(lang(29999), lang(39010)) - answer = False - else: - log.info('plex.tv connection with token successful') - settings('plex_status', value=lang(39227)) - # Refresh the info from Plex.tv - xml = self.doUtils('https://plex.tv/users/account', - authenticate=False, - headerOptions={'X-Plex-Token': self.plexToken}) - try: - self.plexLogin = xml.attrib['title'] - except (AttributeError, KeyError): - log.error('Failed to update Plex info from plex.tv') - else: - settings('plexLogin', value=self.plexLogin) - home = 'true' if xml.attrib.get('home') == '1' else 'false' - settings('plexhome', value=home) - settings('plexAvatar', value=xml.attrib.get('thumb')) - settings('plexHomeSize', value=xml.attrib.get('homeSize', '1')) - log.info('Updated Plex info from plex.tv') - return answer - - def CheckPMS(self): - """ - Check the PMS that was set in file settings. - Will return False if we need to reconnect, because: - PMS could not be reached (no matter the authorization) - machineIdentifier did not match - - Will also set the PMS machineIdentifier in the file settings if it was - not set before - """ - answer = True - chk = self.plx.CheckConnection(self.server, verifySSL=False) - if chk is False: - log.warn('Could not reach PMS %s' % self.server) - answer = False - if answer is True and not self.serverid: - log.info('No PMS machineIdentifier found for %s. Trying to ' - 'get the PMS unique ID' % self.server) - self.serverid = GetMachineIdentifier(self.server) - if self.serverid is None: - log.warn('Could not retrieve machineIdentifier') - answer = False - else: - settings('plex_machineIdentifier', value=self.serverid) - elif answer is True: - tempServerid = GetMachineIdentifier(self.server) - if tempServerid != self.serverid: - log.warn('The current PMS %s was expected to have a ' - 'unique machineIdentifier of %s. But we got ' - '%s. Pick a new server to be sure' - % (self.server, self.serverid, tempServerid)) - answer = False - return answer - - def _getServerList(self): - """ - Returns a list of servers from GDM and possibly plex.tv - """ - self.plx.discoverPMS(xbmc.getIPAddress(), - plexToken=self.plexToken) - serverlist = self.plx.returnServerList(self.plx.g_PMS) - log.debug('PMS serverlist: %s' % serverlist) - return serverlist - - def _checkServerCon(self, server): - """ - Checks for server's connectivity. Returns CheckConnection result - """ - # Re-direct via plex if remote - will lead to the correct SSL - # certificate - if server['local'] == '1': - url = '%s://%s:%s' \ - % (server['scheme'], server['ip'], server['port']) - # Deactive SSL verification if the server is local! - verifySSL = False - else: - url = server['baseURL'] - verifySSL = True - chk = self.plx.CheckConnection(url, - token=server['accesstoken'], - verifySSL=verifySSL) - return chk - - def PickPMS(self, showDialog=False): - """ - Searches for PMS in local Lan and optionally (if self.plexToken set) - also on plex.tv - showDialog=True: let the user pick one - showDialog=False: automatically pick PMS based on machineIdentifier - - Returns the picked PMS' detail as a dict: - { - 'name': friendlyName, the Plex server's name - 'address': ip:port - 'ip': ip, without http/https - 'port': port - 'scheme': 'http'/'https', nice for checking for secure connections - 'local': '1'/'0', Is the server a local server? - 'owned': '1'/'0', Is the server owned by the user? - 'machineIdentifier': id, Plex server machine identifier - 'accesstoken': token Access token to this server - 'baseURL': baseURL scheme://ip:port - 'ownername' Plex username of PMS owner - } - - or None if unsuccessful - """ - server = None - # If no server is set, let user choose one - if not self.server or not self.serverid: - showDialog = True - if showDialog is True: - server = self._UserPickPMS() - else: - server = self._AutoPickPMS() - if server is not None: - self._write_PMS_settings(server['baseURL'], server['accesstoken']) - return server - - def _write_PMS_settings(self, url, token): - """ - Sets certain settings for server by asking for the PMS' settings - Call with url: scheme://ip:port - """ - xml = get_PMS_settings(url, token) - try: - xml.attrib - except AttributeError: - log.error('Could not get PMS settings for %s' % url) - return - for entry in xml: - if entry.attrib.get('id', '') == 'allowMediaDeletion': - settings('plex_allows_mediaDeletion', - value=entry.attrib.get('value', 'true')) - window('plex_allows_mediaDeletion', - value=entry.attrib.get('value', 'true')) - - def _AutoPickPMS(self): - """ - Will try to pick PMS based on machineIdentifier saved in file settings - but only once - - Returns server or None if unsuccessful - """ - httpsUpdated = False - checkedPlexTV = False - server = None - while True: - if httpsUpdated is False: - serverlist = self._getServerList() - for item in serverlist: - if item.get('machineIdentifier') == self.serverid: - server = item - if server is None: - name = settings('plex_servername') - log.warn('The PMS you have used before with a unique ' - 'machineIdentifier of %s and name %s is ' - 'offline' % (self.serverid, name)) - return - chk = self._checkServerCon(server) - if chk == 504 and httpsUpdated is False: - # Not able to use HTTP, try HTTPs for now - server['scheme'] = 'https' - httpsUpdated = True - continue - if chk == 401: - log.warn('Not yet authorized for Plex server %s' - % server['name']) - if self.CheckPlexTVSignIn() is True: - if checkedPlexTV is False: - # Try again - checkedPlexTV = True - httpsUpdated = False - continue - else: - log.warn('Not authorized even though we are signed ' - ' in to plex.tv correctly') - self.dialog.ok(lang(29999), '%s %s' - % (lang(39214), - tryEncode(server['name']))) - return - else: - return - # Problems connecting - elif chk >= 400 or chk is False: - log.warn('Problems connecting to server %s. chk is %s' - % (server['name'], chk)) - return - log.info('We found a server to automatically connect to: %s' - % server['name']) - return server - - def _UserPickPMS(self): - """ - Lets user pick his/her PMS from a list - - Returns server or None if unsuccessful - """ - httpsUpdated = False - while True: - if httpsUpdated is False: - serverlist = self._getServerList() - # Exit if no servers found - if len(serverlist) == 0: - log.warn('No plex media servers found!') - self.dialog.ok(lang(29999), lang(39011)) - return - # Get a nicer list - dialoglist = [] - for server in serverlist: - if server['local'] == '1': - # server is in the same network as client. - # Add"local" - msg = lang(39022) - else: - # Add 'remote' - msg = lang(39054) - if server.get('ownername'): - # Display username if its not our PMS - dialoglist.append('%s (%s, %s)' - % (server['name'], - server['ownername'], - msg)) - else: - dialoglist.append('%s (%s)' - % (server['name'], msg)) - # Let user pick server from a list - resp = self.dialog.select(lang(39012), dialoglist) - if resp == -1: - # User cancelled - return - - server = serverlist[resp] - chk = self._checkServerCon(server) - if chk == 504 and httpsUpdated is False: - # Not able to use HTTP, try HTTPs for now - serverlist[resp]['scheme'] = 'https' - httpsUpdated = True - continue - httpsUpdated = False - if chk == 401: - log.warn('Not yet authorized for Plex server %s' - % server['name']) - # Please sign in to plex.tv - self.dialog.ok(lang(29999), - lang(39013) + server['name'], - lang(39014)) - if self.PlexTVSignIn() is False: - # Exit while loop if user cancels - return - # Problems connecting - elif chk >= 400 or chk is False: - # Problems connecting to server. Pick another server? - answ = self.dialog.yesno(lang(29999), - lang(39015)) - # Exit while loop if user chooses No - if not answ: - return - # Otherwise: connection worked! - else: - return server - - def WritePMStoSettings(self, server): - """ - Saves server to file settings. server is a dict of the form: - { - 'name': friendlyName, the Plex server's name - 'address': ip:port - 'ip': ip, without http/https - 'port': port - 'scheme': 'http'/'https', nice for checking for secure connections - 'local': '1'/'0', Is the server a local server? - 'owned': '1'/'0', Is the server owned by the user? - 'machineIdentifier': id, Plex server machine identifier - 'accesstoken': token Access token to this server - 'baseURL': baseURL scheme://ip:port - 'ownername' Plex username of PMS owner - } - """ - settings('plex_machineIdentifier', server['machineIdentifier']) - settings('plex_servername', server['name']) - settings('plex_serverowned', - 'true' if server['owned'] == '1' - else 'false') - # Careful to distinguish local from remote PMS - if server['local'] == '1': - scheme = server['scheme'] - settings('ipaddress', server['ip']) - settings('port', server['port']) - log.debug("Setting SSL verify to false, because server is " - "local") - settings('sslverify', 'false') - else: - baseURL = server['baseURL'].split(':') - scheme = baseURL[0] - settings('ipaddress', baseURL[1].replace('//', '')) - settings('port', baseURL[2]) - log.debug("Setting SSL verify to true, because server is not " - "local") - settings('sslverify', 'true') - - if scheme == 'https': - settings('https', 'true') - else: - settings('https', 'false') - # And finally do some logging - log.debug("Writing to Kodi user settings file") - log.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s " - % (server['machineIdentifier'], server['ip'], - server['port'], server['scheme'])) - - def setup(self): - """ - Initial setup. Run once upon startup. - - Check server, user, direct paths, music, direct stream if not direct - path. - """ - log.info("Initial setup called.") - dialog = self.dialog - - # Get current Kodi video cache setting - cache, _ = advancedsettings_xml(['cache', 'memorysize']) - if cache is None: - # Kodi default cache - cache = '20971520' - else: - cache = str(cache.text) - log.info('Current Kodi video memory cache in bytes: %s' % cache) - settings('kodi_video_cache', value=cache) - - # Do we need to migrate stuff? - check_migration() - - # Optionally sign into plex.tv. Will not be called on very first run - # as plexToken will be '' - settings('plex_status', value=lang(39226)) - if self.plexToken and self.myplexlogin: - self.CheckPlexTVSignIn() - - # If a Plex server IP has already been set - # return only if the right machine identifier is found - if self.server: - log.info("PMS is already set: %s. Checking now..." % self.server) - if self.CheckPMS(): - log.info("Using PMS %s with machineIdentifier %s" - % (self.server, self.serverid)) - self._write_PMS_settings(self.server, self.pms_token) - return - - # If not already retrieved myplex info, optionally let user sign in - # to plex.tv. This DOES get called on very first install run - if not self.plexToken and self.myplexlogin: - self.PlexTVSignIn() - - server = self.PickPMS() - if server is not None: - # Write our chosen server to Kodi settings file - self.WritePMStoSettings(server) - - # User already answered the installation questions - if settings('InstallQuestionsAnswered') == 'true': + # If a Plex server IP has already been set + # return only if the right machine identifier is found + if connectmanager.server: + log.info("PMS is already set: %s. Checking now..." % self.server) + if connectmanager.check_pms(): + log.info("Using PMS %s with machineIdentifier %s" + % (self.server, self.serverid)) + connectmanager.write_pms_settings(self.server, self.pms_token) return - # Additional settings where the user needs to choose - # Direct paths (\\NAS\mymovie.mkv) or addon (http)? - goToSettings = False - if dialog.yesno(lang(29999), - lang(39027), - lang(39028), - nolabel="Addon (Default)", - yeslabel="Native (Direct Paths)"): - log.debug("User opted to use direct paths.") - settings('useDirectPaths', value="1") - state.DIRECT_PATHS = True - # Are you on a system where you would like to replace paths - # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) - if dialog.yesno(heading=lang(29999), line1=lang(39033)): - log.debug("User chose to replace paths with smb") - else: - settings('replaceSMB', value="false") + # If not already retrieved myplex info, optionally let user sign in + # to plex.tv. This DOES get called on very first install run + if not connectmanager.plexToken and connectmanager.myplexlogin: + connectmanager.plex_tv_signin() - # complete replace all original Plex library paths with custom SMB - if dialog.yesno(heading=lang(29999), line1=lang(39043)): - log.debug("User chose custom smb paths") - settings('remapSMB', value="true") - # Please enter your custom smb paths in the settings under - # "Sync Options" and then restart Kodi - dialog.ok(heading=lang(29999), line1=lang(39044)) - goToSettings = True + server = connectmanager.connectmanager.pick_pms() + if server is not None: + # Write our chosen server to Kodi settings file + connectmanager.write_pms_to_settings(server) - # Go to network credentials? - if dialog.yesno(heading=lang(29999), - line1=lang(39029), - line2=lang(39030)): - log.debug("Presenting network credentials dialog.") - from utils import passwordsXML - passwordsXML() - # Disable Plex music? - if dialog.yesno(heading=lang(29999), line1=lang(39016)): - log.debug("User opted to disable Plex music library.") - settings('enableMusic', value="false") + # User already answered the installation questions + if settings('InstallQuestionsAnswered') == 'true': + return - # Download additional art from FanArtTV - if dialog.yesno(heading=lang(29999), line1=lang(39061)): - log.debug("User opted to use FanArtTV") - settings('FanartTV', value="true") - # Do you want to replace your custom user ratings with an indicator of - # how many versions of a media item you posses? - if dialog.yesno(heading=lang(29999), line1=lang(39718)): - log.debug("User opted to replace user ratings with version number") - settings('indicate_media_versions', value="true") + # Additional settings where the user needs to choose + # Direct paths (\\NAS\mymovie.mkv) or addon (http)? + goToSettings = False + if dialog('yesno', + lang(29999), + lang(39027), + lang(39028), + nolabel="Addon (Default)", + yeslabel="Native (Direct Paths)"): + log.debug("User opted to use direct paths.") + settings('useDirectPaths', value="1") + state.DIRECT_PATHS = True + # Are you on a system where you would like to replace paths + # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) + if dialog('yesno', heading=lang(29999), line1=lang(39033)): + log.debug("User chose to replace paths with smb") + else: + settings('replaceSMB', value="false") - # If you use several Plex libraries of one kind, e.g. "Kids Movies" and - # "Parents Movies", be sure to check https://goo.gl/JFtQV9 - dialog.ok(heading=lang(29999), line1=lang(39076)) + # complete replace all original Plex library paths with custom SMB + if dialog('yesno', heading=lang(29999), line1=lang(39043)): + log.debug("User chose custom smb paths") + settings('remapSMB', value="true") + # Please enter your custom smb paths in the settings under + # "Sync Options" and then restart Kodi + dialog('ok', heading=lang(29999), line1=lang(39044)) + goToSettings = True - # Need to tell about our image source for collections: themoviedb.org - dialog.ok(heading=lang(29999), line1=lang(39717)) - # Make sure that we only ask these questions upon first installation - settings('InstallQuestionsAnswered', value='true') + # Go to network credentials? + if dialog('yesno', + heading=lang(29999), + line1=lang(39029), + line2=lang(39030)): + log.debug("Presenting network credentials dialog.") + from utils import passwordsXML + passwordsXML() + # Disable Plex music? + if dialog('yesno', heading=lang(29999), line1=lang(39016)): + log.debug("User opted to disable Plex music library.") + settings('enableMusic', value="false") - if goToSettings is False: - # Open Settings page now? You will need to restart! - goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017)) - if goToSettings: - state.PMS_STATUS = 'Stop' - xbmc.executebuiltin( - 'Addon.OpenSettings(plugin.video.plexkodiconnect)') + # Download additional art from FanArtTV + if dialog('yesno', heading=lang(29999), line1=lang(39061)): + log.debug("User opted to use FanArtTV") + settings('FanartTV', value="true") + # Do you want to replace your custom user ratings with an indicator of + # how many versions of a media item you posses? + if dialog('yesno', heading=lang(29999), line1=lang(39718)): + log.debug("User opted to replace user ratings with version number") + settings('indicate_media_versions', value="true") + + # If you use several Plex libraries of one kind, e.g. "Kids Movies" and + # "Parents Movies", be sure to check https://goo.gl/JFtQV9 + dialog('ok', heading=lang(29999), line1=lang(39076)) + + # Need to tell about our image source for collections: themoviedb.org + dialog('ok', heading=lang(29999), line1=lang(39717)) + # Make sure that we only ask these questions upon first installation + settings('InstallQuestionsAnswered', value='true') + + if goToSettings is False: + # Open Settings page now? You will need to restart! + goToSettings = dialog('yesno', heading=lang(29999), line1=lang(39017)) + if goToSettings: + state.PMS_STATUS = 'Stop' + executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index f9671263..11dada7b 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -14,6 +14,9 @@ from utils import window, settings, language as lang, thread_methods import downloadutils import PlexAPI +from connectmanager import check_connection +from connect.plex_tv import get_user_artwork_url + from PlexFunctions import GetMachineIdentifier import state @@ -106,7 +109,7 @@ class UserClient(threading.Thread): log.debug('Setting user preferences') # Only try to get user avatar if there is a token if self.currToken: - url = PlexAPI.PlexAPI().GetUserArtworkURL(self.currUser) + url = get_user_artwork_url(self.currUser) if url: window('PlexUserImage', value=url) # Set resume point max @@ -130,9 +133,9 @@ class UserClient(threading.Thread): if self.currServer is None: return False log.debug('Testing validity of current token') - res = PlexAPI.PlexAPI().CheckConnection(self.currServer, - token=self.currToken, - verifySSL=self.ssl) + res = check_connection(self.currServer, + token=self.currToken, + verifySSL=self.ssl) if res is False: # PMS probably offline return False diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 27354384..21172c0b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -34,6 +34,26 @@ log = logging.getLogger("PLEX."+__name__) WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') +# For use with xbmcgui.dialog +ICONS = { + '{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png', + '{info}': xbmcgui.NOTIFICATION_INFO, + '{warning}': xbmcgui.NOTIFICATION_WARNING, + '{error}': xbmcgui.NOTIFICATION_ERROR +} +TYPES = { + '{alphanum}': xbmcgui.INPUT_ALPHANUM, + '{numeric}': xbmcgui.INPUT_NUMERIC, + '{date}': xbmcgui.INPUT_DATE, + '{time}': xbmcgui.INPUT_TIME, + '{ipaddress}': xbmcgui.INPUT_IPADDRESS, + '{password}': xbmcgui.INPUT_PASSWORD +} +OPTIONS = { + '{hide_input}': xbmcgui.ALPHANUM_HIDE_INPUT, + '{password_verify}': xbmcgui.PASSWORD_VERIFY +} + ############################################################################### # Main methods @@ -154,27 +174,19 @@ def dialog(typus, *args, **kwargs): type='{ipaddress}' xbmcgui.INPUT_IPADDRESS (format: #.#.#.#) type='{password}' xbmcgui.INPUT_PASSWORD (return md5 hash of input, input is masked) + + Input Options: + option='{hide_input}': xbmcgui.ALPHANUM_HIDE_INPUT + option='{password_verify}': xbmcgui.PASSWORD_VERIFY """ d = xbmcgui.Dialog() if "icon" in kwargs: - types = { - '{plex}': 'special://home/addons/plugin.video.plexkodiconnect/icon.png', - '{info}': xbmcgui.NOTIFICATION_INFO, - '{warning}': xbmcgui.NOTIFICATION_WARNING, - '{error}': xbmcgui.NOTIFICATION_ERROR - } - for key, value in types.iteritems(): + for key, value in ICONS.iteritems(): kwargs['icon'] = kwargs['icon'].replace(key, value) if 'type' in kwargs: - types = { - '{alphanum}': xbmcgui.INPUT_ALPHANUM, - '{numeric}': xbmcgui.INPUT_NUMERIC, - '{date}': xbmcgui.INPUT_DATE, - '{time}': xbmcgui.INPUT_TIME, - '{ipaddress}': xbmcgui.INPUT_IPADDRESS, - '{password}': xbmcgui.INPUT_PASSWORD - } - kwargs['type'] = types[kwargs['type']] + kwargs['type'] = TYPES[kwargs['type']] + if 'option' in kwargs: + kwargs['option'] = OPTIONS[kwargs['option']] if "heading" in kwargs: kwargs['heading'] = kwargs['heading'].replace("{plex}", language(29999)) diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 95042299..85766cee 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -26,6 +26,7 @@ _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' ADDON_ID = 'plugin.video.plexkodiconnect' ADDON_VERSION = _ADDON.getAddonInfo('version') +ADDON_PATH = tryDecode(_ADDON.getAddonInfo('path')) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) diff --git a/service.py b/service.py index 601a2a59..1785c853 100644 --- a/service.py +++ b/service.py @@ -30,8 +30,7 @@ sys_path.append(_base_resource) ############################################################################### -from utils import settings, window, language as lang, dialog, tryEncode, \ - tryDecode +from utils import settings, window, language as lang, dialog, tryDecode from userclient import UserClient import initialsetup from kodimonitor import KodiMonitor @@ -40,8 +39,8 @@ import videonodes from websocket_client import PMS_Websocket, Alexa_Websocket import downloadutils from playqueue import Playqueue +from connectmanager import ConnectManager, check_connection -import PlexAPI from PlexCompanion import PlexCompanion from command_pipeline import Monitor_Window from playback_starter import Playback_Starter @@ -165,8 +164,6 @@ class Service(): if settings('enableTextureCache') == "true": self.image_cache_thread = Image_Cache_Thread() - plx = PlexAPI.PlexAPI() - welcome_msg = True counter = 0 while not __stop_PKC(): @@ -260,7 +257,7 @@ class Service(): if server is False: # No server info set in add-on settings pass - elif plx.CheckConnection(server, verifySSL=True) is False: + elif check_connection(server, verifySSL=True) is False: # Server is offline or cannot be reached # Alert the user and suppress future warning if self.server_online: @@ -279,10 +276,10 @@ class Service(): # Periodically check if the IP changed, e.g. per minute if counter > 20: counter = 0 - setup = initialsetup.InitialSetup() - tmp = setup.PickPMS() + connectmanager = ConnectManager() + tmp = connectmanager.pick_pms() if tmp is not None: - setup.WritePMStoSettings(tmp) + connectmanager.write_pms_to_settings(tmp) else: # Server is online counter = 0