diff --git a/resources/lib/connect/connectionmanager.py b/resources/lib/connect/connectionmanager.py new file mode 100644 index 00000000..c027c33f --- /dev/null +++ b/resources/lib/connect/connectionmanager.py @@ -0,0 +1,816 @@ +# -*- coding: utf-8 -*- +############################################################################### +from logging import getLogger +from hashlib import md5 +import json +import requests +import socket +import time +from datetime import datetime + +import credentials as cred + +############################################################################### + +# Disable annoying requests warnings +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +log = getLogger("PLEX."+__name__) + +############################################################################### + +CONNECTIONSTATE = { + 'Unavailable': 0, + 'ServerSelection': 1, + 'ServerSignIn': 2, + 'SignedIn': 3, + 'ConnectSignIn': 4, + 'ServerUpdateNeeded': 5 +} + +CONNECTIONMODE = { + 'Local': 0, + 'Remote': 1, + 'Manual': 2 +} + +############################################################################### + + +def getServerAddress(server, mode): + + modes = { + CONNECTIONMODE['Local']: server.get('LocalAddress'), + CONNECTIONMODE['Remote']: server.get('RemoteAddress'), + CONNECTIONMODE['Manual']: server.get('ManualAddress') + } + return (modes.get(mode) or + server.get('ManualAddress', + server.get('LocalAddress', + server.get('RemoteAddress')))) + + +class ConnectionManager(object): + + default_timeout = 20 + apiClients = [] + minServerVersion = "3.0.5930" + connectUser = None + + def __init__(self, appName, appVersion, deviceName, deviceId, + capabilities=None, devicePixelRatio=None): + log.info("Begin ConnectionManager constructor") + + self.credentialProvider = cred.Credentials() + self.appName = appName + self.appVersion = appVersion + self.deviceName = deviceName + self.deviceId = deviceId + self.capabilities = capabilities + self.devicePixelRatio = devicePixelRatio + + def setFilePath(self, path): + # Set where to save persistant data + self.credentialProvider.setPath(path) + + def _getAppVersion(self): + return self.appVersion + + def _getCapabilities(self): + return self.capabilities + + def _getDeviceId(self): + return self.deviceId + + def _connectUserId(self): + return self.credentialProvider.getCredentials().get('ConnectUserId') + + def _connectToken(self): + return self.credentialProvider.getCredentials().get('ConnectAccessToken') + + def getServerInfo(self, id_): + + servers = self.credentialProvider.getCredentials()['Servers'] + + for s in servers: + if s['Id'] == id_: + return s + + def _getLastUsedServer(self): + + servers = self.credentialProvider.getCredentials()['Servers'] + + if not len(servers): + return + + try: + servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) + except TypeError: + servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) + + return servers[0] + + def _mergeServers(self, list1, list2): + + for i in range(0, len(list2), 1): + try: + self.credentialProvider.addOrUpdateServer(list1, list2[i]) + except KeyError: + continue + + return list1 + + def _connectUser(self): + + return self.connectUser + + def _resolveFailure(self): + + return { + 'State': CONNECTIONSTATE['Unavailable'], + 'ConnectUser': self._connectUser() + } + + def _getMinServerVersion(self, val=None): + + if val is not None: + self.minServerVersion = val + + return self.minServerVersion + + def _updateServerInfo(self, server, systemInfo): + + if server is None or systemInfo is None: + return + + server['Name'] = systemInfo['ServerName'] + server['Id'] = systemInfo['Id'] + + if systemInfo.get('LocalAddress'): + server['LocalAddress'] = systemInfo['LocalAddress'] + if systemInfo.get('WanAddress'): + server['RemoteAddress'] = systemInfo['WanAddress'] + if systemInfo.get('MacAddress'): + server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}] + + def _getHeaders(self, request): + + headers = request.setdefault('headers', {}) + + if request.get('dataType') == "json": + headers['Accept'] = "application/json" + request.pop('dataType') + + headers['X-Application'] = self._addAppInfoToConnectRequest() + headers['Content-type'] = request.get('contentType', + 'application/x-www-form-urlencoded; charset=UTF-8') + + def requestUrl(self, request): + + if not request: + raise AttributeError("Request cannot be null") + + self._getHeaders(request) + request['timeout'] = request.get('timeout') or self.default_timeout + request['verify'] = request.get('ssl') or False + + action = request['type'] + request.pop('type', None) + request.pop('ssl', None) + + log.debug("ConnectionManager requesting %s" % request) + + try: + r = self._requests(action, **request) + log.info("ConnectionManager response status: %s" % r.status_code) + r.raise_for_status() + + except Exception as e: # Elaborate on exceptions? + log.error(e) + raise + + else: + try: + return r.json() + except ValueError: + r.content # Read response to release connection + return + + def _requests(self, action, **kwargs): + + if action == "GET": + r = requests.get(**kwargs) + elif action == "POST": + r = requests.post(**kwargs) + + return r + + def getEmbyServerUrl(self, baseUrl, handler): + return "%s/emby/%s" % (baseUrl, handler) + + def getConnectUrl(self, handler): + return "https://connect.emby.media/service/%s" % handler + + def _findServers(self, foundServers): + + servers = [] + + for foundServer in foundServers: + + server = self._convertEndpointAddressToManualAddress(foundServer) + + info = { + 'Id': foundServer['Id'], + 'LocalAddress': server or foundServer['Address'], + 'Name': foundServer['Name'] + } + info['LastCONNECTIONMODE'] = CONNECTIONMODE['Manual'] if info.get('ManualAddress') else CONNECTIONMODE['Local'] + + servers.append(info) + else: + return servers + + def _convertEndpointAddressToManualAddress(self, info): + + if info.get('Address') and info.get('EndpointAddress'): + address = info['EndpointAddress'].split(':')[0] + + # Determine the port, if any + parts = info['Address'].split(':') + if len(parts) > 1: + portString = parts[len(parts)-1] + + try: + address += ":%s" % int(portString) + return self._normalizeAddress(address) + except ValueError: + pass + + return None + + def _serverDiscovery(self): + + MULTI_GROUP = ("", 7359) + MESSAGE = "who is EmbyServer?" + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(1.0) # This controls the socket.timeout exception + + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) + + log.debug("MultiGroup : %s" % str(MULTI_GROUP)) + log.debug("Sending UDP Data: %s" % MESSAGE) + + servers = [] + + try: + sock.sendto(MESSAGE, MULTI_GROUP) + except Exception as error: + log.error(error) + return servers + + while True: + try: + data, addr = sock.recvfrom(1024) # buffer size + servers.append(json.loads(data)) + + except socket.timeout: + log.info("Found Servers: %s" % servers) + return servers + + except Exception as e: + log.error("Error trying to find servers: %s" % e) + return servers + + def _normalizeAddress(self, address): + # Attempt to correct bad input + address = address.strip() + address = address.lower() + + if 'http' not in address: + address = "http://%s" % address + + return address + + def connectToAddress(self, address, options={}): + + if not address: + return False + + address = self._normalizeAddress(address) + + def _onFail(): + log.error("connectToAddress %s failed" % address) + return self._resolveFailure() + + try: + publicInfo = self._tryConnect(address, options=options) + except Exception: + return _onFail() + else: + log.info("connectToAddress %s succeeded" % address) + server = { + 'ManualAddress': address, + 'LastCONNECTIONMODE': CONNECTIONMODE['Manual'] + } + self._updateServerInfo(server, publicInfo) + server = self.connectToServer(server, options) + if server is False: + return _onFail() + else: + return server + + def onAuthenticated(self, result, options={}): + + credentials = self.credentialProvider.getCredentials() + for s in credentials['Servers']: + if s['Id'] == result['ServerId']: + server = s + break + else: # Server not found? + return + + if options.get('updateDateLastAccessed') is not False: + server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + + server['UserId'] = result['User']['Id'] + server['AccessToken'] = result['AccessToken'] + + self.credentialProvider.addOrUpdateServer(credentials['Servers'], server) + self._saveUserInfoIntoCredentials(server, result['User']) + self.credentialProvider.getCredentials(credentials) + + def _tryConnect(self, url, timeout=None, options={}): + + url = self.getEmbyServerUrl(url, "system/info/public") + log.info("tryConnect url: %s" % url) + + return self.requestUrl({ + + 'type': "GET", + 'url': url, + 'dataType': "json", + 'timeout': timeout, + 'ssl': options.get('ssl') + }) + + def _addAppInfoToConnectRequest(self): + return "%s/%s" % (self.appName, self.appVersion) + + def _getConnectServers(self, credentials): + + log.info("Begin getConnectServers") + + servers = [] + + if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'): + return servers + + url = self.getConnectUrl("servers?userId=%s" % credentials['ConnectUserId']) + request = { + + 'type': "GET", + 'url': url, + 'dataType': "json", + 'headers': { + 'X-Connect-UserToken': credentials['ConnectAccessToken'] + } + } + for server in self.requestUrl(request): + + servers.append({ + + 'ExchangeToken': server['AccessKey'], + 'ConnectServerId': server['Id'], + 'Id': server['SystemId'], + 'Name': server['Name'], + 'RemoteAddress': server['Url'], + 'LocalAddress': server['LocalAddress'], + 'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser", + }) + + return servers + + def getAvailableServers(self): + + log.info("Begin getAvailableServers") + + # Clone the array + credentials = self.credentialProvider.getCredentials() + + connectServers = self._getConnectServers(credentials) + foundServers = self._findServers(self._serverDiscovery()) + + servers = list(credentials['Servers']) + self._mergeServers(servers, foundServers) + self._mergeServers(servers, connectServers) + + servers = self._filterServers(servers, connectServers) + + try: + servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True) + except TypeError: + servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True) + + credentials['Servers'] = servers + self.credentialProvider.getCredentials(credentials) + + return servers + + def _filterServers(self, servers, connectServers): + + filtered = [] + + for server in servers: + # It's not a connect server, so assume it's still valid + if server.get('ExchangeToken') is None: + filtered.append(server) + continue + + for connectServer in connectServers: + if server['Id'] == connectServer['Id']: + filtered.append(server) + break + else: + return filtered + + def _getConnectPasswordHash(self, password): + + password = self._cleanConnectPassword(password) + + return md5(password).hexdigest() + + def _saveUserInfoIntoCredentials(self, server, user): + + info = { + 'Id': user['Id'], + 'IsSignedInOffline': True + } + + self.credentialProvider.addOrUpdateUser(server, info) + + def _compareVersions(self, a, b): + """ + -1 a is smaller + 1 a is larger + 0 equal + """ + a = a.split('.') + b = b.split('.') + + for i in range(0, max(len(a), len(b)), 1): + try: + aVal = a[i] + except IndexError: + aVal = 0 + + try: + bVal = b[i] + except IndexError: + bVal = 0 + + if aVal < bVal: + return -1 + + if aVal > bVal: + return 1 + + return 0 + + def connectToServer(self, server, options={}): + + log.info("begin connectToServer") + + tests = [] + + if server.get('LastCONNECTIONMODE') is not None: + #tests.append(server['LastCONNECTIONMODE']) + pass + if CONNECTIONMODE['Manual'] not in tests: + tests.append(CONNECTIONMODE['Manual']) + if CONNECTIONMODE['Local'] not in tests: + tests.append(CONNECTIONMODE['Local']) + if CONNECTIONMODE['Remote'] not in tests: + tests.append(CONNECTIONMODE['Remote']) + + # TODO: begin to wake server + + log.info("beginning connection tests") + return self._testNextCONNECTIONMODE(tests, 0, server, options) + + def _stringEqualsIgnoreCase(self, str1, str2): + + return (str1 or "").lower() == (str2 or "").lower() + + def _testNextCONNECTIONMODE(self, tests, index, server, options): + + if index >= len(tests): + log.info("Tested all connection modes. Failing server connection.") + return self._resolveFailure() + + mode = tests[index] + address = getServerAddress(server, mode) + enableRetry = False + skipTest = False + timeout = self.default_timeout + + if mode == CONNECTIONMODE['Local']: + enableRetry = True + timeout = 8 + + if self._stringEqualsIgnoreCase(address, server.get('ManualAddress')): + log.info("skipping LocalAddress test because it is the same as ManualAddress") + skipTest = True + + elif mode == CONNECTIONMODE['Manual']: + + if self._stringEqualsIgnoreCase(address, server.get('LocalAddress')): + enableRetry = True + timeout = 8 + + if skipTest or not address: + log.info("skipping test at index: %s" % index) + return self._testNextCONNECTIONMODE(tests, index+1, server, options) + + log.info("testing connection mode %s with server %s" % (mode, server.get('Name'))) + try: + result = self._tryConnect(address, timeout, options) + + except Exception: + log.error("test failed for connection mode %s with server %s" % (mode, server.get('Name'))) + + if enableRetry: + # TODO: wake on lan and retry + return self._testNextCONNECTIONMODE(tests, index+1, server, options) + else: + return self._testNextCONNECTIONMODE(tests, index+1, server, options) + else: + + if self._compareVersions(self._getMinServerVersion(), result['Version']) == 1: + log.warn("minServerVersion requirement not met. Server version: %s" % result['Version']) + return { + 'State': CONNECTIONSTATE['ServerUpdateNeeded'], + 'Servers': [server] + } + else: + log.info("calling onSuccessfulConnection with connection mode %s with server %s" + % (mode, server.get('Name'))) + return self._onSuccessfulConnection(server, result, mode, options) + + def _onSuccessfulConnection(self, server, systemInfo, CONNECTIONMODE, options): + + credentials = self.credentialProvider.getCredentials() + + if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False: + + if self._ensureConnectUser(credentials) is not False: + + if server.get('ExchangeToken'): + + self._addAuthenticationInfoFromConnect(server, CONNECTIONMODE, credentials, options) + + return self._afterConnectValidated(server, credentials, systemInfo, CONNECTIONMODE, True, options) + + def _afterConnectValidated(self, server, credentials, systemInfo, CONNECTIONMODE, verifyLocalAuthentication, options): + + if options.get('enableAutoLogin') is False: + server['UserId'] = None + server['AccessToken'] = None + + elif (verifyLocalAuthentication and server.get('AccessToken') and + options.get('enableAutoLogin') is not False): + + if self._validateAuthentication(server, CONNECTIONMODE, options) is not False: + return self._afterConnectValidated(server, credentials, systemInfo, CONNECTIONMODE, False, options) + + return + + self._updateServerInfo(server, systemInfo) + server['LastCONNECTIONMODE'] = CONNECTIONMODE + + if options.get('updateDateLastAccessed') is not False: + server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') + + self.credentialProvider.addOrUpdateServer(credentials['Servers'], server) + self.credentialProvider.getCredentials(credentials) + + result = { + 'Servers': [], + 'ConnectUser': self._connectUser() + } + result['State'] = CONNECTIONSTATE['SignedIn'] if (server.get('AccessToken') and options.get('enableAutoLogin') is not False) else CONNECTIONSTATE['ServerSignIn'] + result['Servers'].append(server) + + # Connected + return result + + def _validateAuthentication(self, server, CONNECTIONMODE, options={}): + + url = getServerAddress(server, CONNECTIONMODE) + request = { + + 'type': "GET", + 'url': self.getEmbyServerUrl(url, "System/Info"), + 'ssl': options.get('ssl'), + 'dataType': "json", + 'headers': { + 'X-MediaBrowser-Token': server['AccessToken'] + } + } + try: + systemInfo = self.requestUrl(request) + self._updateServerInfo(server, systemInfo) + + if server.get('UserId'): + user = self.requestUrl({ + + 'type': "GET", + 'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']), + 'ssl': options.get('ssl'), + 'dataType': "json", + 'headers': { + 'X-MediaBrowser-Token': server['AccessToken'] + } + }) + + except Exception: + server['UserId'] = None + server['AccessToken'] = None + return False + + def loginToConnect(self, username, password): + + if not username: + raise AttributeError("username cannot be empty") + + if not password: + raise AttributeError("password cannot be empty") + + md5 = self._getConnectPasswordHash(password) + request = { + 'type': "POST", + 'url': self.getConnectUrl("user/authenticate"), + 'data': { + 'nameOrEmail': username, + 'password': md5 + }, + 'dataType': "json" + } + try: + result = self.requestUrl(request) + except Exception as e: # Failed to login + log.error(e) + return False + else: + credentials = self.credentialProvider.getCredentials() + credentials['ConnectAccessToken'] = result['AccessToken'] + credentials['ConnectUserId'] = result['User']['Id'] + credentials['ConnectUser'] = result['User']['DisplayName'] + self.credentialProvider.getCredentials(credentials) + # Signed in + self._onConnectUserSignIn(result['User']) + + return result + + def _onConnectUserSignIn(self, user): + + self.connectUser = user + log.info("connectusersignedin %s" % user) + + def _getConnectUser(self, userId, accessToken): + + if not userId: + raise AttributeError("null userId") + + if not accessToken: + raise AttributeError("null accessToken") + + url = self.getConnectUrl('user?id=%s' % userId) + + return self.requestUrl({ + + 'type': "GET", + 'url': url, + 'dataType': "json", + 'headers': { + 'X-Connect-UserToken': accessToken + } + }) + + def _addAuthenticationInfoFromConnect(self, server, CONNECTIONMODE, credentials, options={}): + + if not server.get('ExchangeToken'): + raise KeyError("server['ExchangeToken'] cannot be null") + + if not credentials.get('ConnectUserId'): + raise KeyError("credentials['ConnectUserId'] cannot be null") + + url = getServerAddress(server, CONNECTIONMODE) + url = self.getEmbyServerUrl(url, "Connect/Exchange?format=json") + auth = ('MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"' + % (self.appName, self.deviceName, self.deviceId, self.appVersion)) + try: + auth = self.requestUrl({ + + 'url': url, + 'type': "GET", + 'dataType': "json", + 'ssl': options.get('ssl'), + 'params': { + 'ConnectUserId': credentials['ConnectUserId'] + }, + 'headers': { + 'X-MediaBrowser-Token': server['ExchangeToken'], + 'X-Emby-Authorization': auth + } + }) + except Exception: + server['UserId'] = None + server['AccessToken'] = None + return False + else: + server['UserId'] = auth['LocalUserId'] + server['AccessToken'] = auth['AccessToken'] + return auth + + def _ensureConnectUser(self, credentials): + + if self.connectUser and self.connectUser['Id'] == credentials['ConnectUserId']: + return + + elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'): + + self.connectUser = None + + try: + result = self._getConnectUser(credentials['ConnectUserId'], credentials['ConnectAccessToken']) + self._onConnectUserSignIn(result) + except Exception: + return False + + def connect(self, options={}): + + log.info("Begin connect") + + servers = self.getAvailableServers() + return self._connectToServers(servers, options) + + def _connectToServers(self, servers, options): + + log.info("Begin connectToServers, with %s servers" % len(servers)) + + if len(servers) == 1: + result = self.connectToServer(servers[0], options) + if result and result.get('State') == CONNECTIONSTATE['Unavailable']: + result['State'] = CONNECTIONSTATE['ConnectSignIn'] if result['ConnectUser'] == None else CONNECTIONSTATE['ServerSelection'] + + log.info("resolving connectToServers with result['State']: %s" % result) + return result + + firstServer = self._getLastUsedServer() + # See if we have any saved credentials and can auto sign in + if firstServer: + + result = self.connectToServer(firstServer, options) + if result and result.get('State') == CONNECTIONSTATE['SignedIn']: + return result + + # Return loaded credentials if exists + credentials = self.credentialProvider.getCredentials() + self._ensureConnectUser(credentials) + + return { + 'Servers': servers, + 'State': CONNECTIONSTATE['ConnectSignIn'] if (not len(servers) and not self._connectUser()) else CONNECTIONSTATE['ServerSelection'], + 'ConnectUser': self._connectUser() + } + + def _cleanConnectPassword(self, password): + + password = password or "" + + password = password.replace("&", '&') + password = password.replace("/", '\') + password = password.replace("!", '!') + password = password.replace("$", '$') + password = password.replace("\"", '"') + password = password.replace("<", '<') + password = password.replace(">", '>') + password = password.replace("'", ''') + + return password + + def clearData(self): + + log.info("connection manager clearing data") + + self.connectUser = None + credentials = self.credentialProvider.getCredentials() + credentials['ConnectAccessToken'] = None + credentials['ConnectUserId'] = None + credentials['Servers'] = [] + self.credentialProvider.getCredentials(credentials) diff --git a/resources/lib/connect/credentials.py b/resources/lib/connect/credentials.py new file mode 100644 index 00000000..4f336afe --- /dev/null +++ b/resources/lib/connect/credentials.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +############################################################################### +import json +from logging import getLogger +import os +import time +from datetime import datetime +############################################################################### + +log = getLogger("PLEX."+__name__) + +############################################################################### + + +class Credentials(object): + # Borg + _shared_state = {} + credentials = None + path = "" + + def __init__(self): + # Borg + self.__dict__ = self._shared_state + + def setPath(self, path): + # Path to save persistant data.txt + self.path = path + + def _ensure(self): + if self.credentials is None: + try: + with open(os.path.join(self.path, 'data.txt')) as infile: + self.credentials = json.load(infile) + + if not isinstance(self.credentials, dict): + raise ValueError("invalid credentials format") + + except Exception as e: + # File is either empty or missing + log.warn(e) + self.credentials = {} + + log.info("credentials initialized with: %s" % self.credentials) + self.credentials['Servers'] = self.credentials.setdefault('Servers', []) + + def _get(self): + + self._ensure() + return self.credentials + + def _set(self, data): + + if data: + self.credentials = data + # Set credentials to file + with open(os.path.join(self.path, 'data.txt'), 'w') as outfile: + for server in data['Servers']: + server['Name'] = server['Name'].encode('utf-8') + json.dump(data, outfile, ensure_ascii=False) + else: + self._clear() + + log.info("credentialsupdated") + + def _clear(self): + + self.credentials = None + # Remove credentials from file + with open(os.path.join(self.path, 'data.txt'), 'w'): + pass + + def getCredentials(self, data=None): + + if data is not None: + self._set(data) + + return self._get() + + def addOrUpdateServer(self, list_, server): + + if server.get('Id') is None: + raise KeyError("Server['Id'] cannot be null or empty") + + # Add default DateLastAccessed if doesn't exist. + server.setdefault('DateLastAccessed', "2001-01-01T00:00:00Z") + + for existing in list_: + if existing['Id'] == server['Id']: + # Merge the data + if server.get('DateLastAccessed'): + if self._dateObject(server['DateLastAccessed']) > self._dateObject(existing['DateLastAccessed']): + existing['DateLastAccessed'] = server['DateLastAccessed'] + + if server.get('UserLinkType'): + existing['UserLinkType'] = server['UserLinkType'] + + if server.get('AccessToken'): + existing['AccessToken'] = server['AccessToken'] + existing['UserId'] = server['UserId'] + + if server.get('ExchangeToken'): + existing['ExchangeToken'] = server['ExchangeToken'] + + if server.get('RemoteAddress'): + existing['RemoteAddress'] = server['RemoteAddress'] + + if server.get('ManualAddress'): + existing['ManualAddress'] = server['ManualAddress'] + + if server.get('LocalAddress'): + existing['LocalAddress'] = server['LocalAddress'] + + if server.get('Name'): + existing['Name'] = server['Name'] + + if server.get('WakeOnLanInfos'): + existing['WakeOnLanInfos'] = server['WakeOnLanInfos'] + + if server.get('LastConnectionMode') is not None: + existing['LastConnectionMode'] = server['LastConnectionMode'] + + if server.get('ConnectServerId'): + existing['ConnectServerId'] = server['ConnectServerId'] + + return existing + else: + list_.append(server) + return server + + def addOrUpdateUser(self, server, user): + + for existing in server.setdefault('Users', []): + if existing['Id'] == user['Id']: + # Merge the data + existing['IsSignedInOffline'] = True + break + else: + server['Users'].append(user) + + def _dateObject(self, date): + # Convert string to date + try: + date_obj = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") + except (ImportError, TypeError): + # TypeError: attribute of type 'NoneType' is not callable + # Known Kodi/python error + date_obj = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) + return date_obj diff --git a/resources/lib/connectmanager.py b/resources/lib/connectmanager.py index b152a384..5d8018e5 100644 --- a/resources/lib/connectmanager.py +++ b/resources/lib/connectmanager.py @@ -5,6 +5,7 @@ from logging import getLogger # from connect.connectionmanager import ConnectionManager from downloadutils import DownloadUtils from dialogs.serverconnect import ServerConnect +from dialogs.servermanual import ServerManual 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 @@ -131,7 +132,7 @@ class ConnectManager(object): def manual_server(self): # Return server or raise error - dia = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH) + dia = ServerManual("script-plex-connect-server-manual.xml", *XML_PATH) dia._set_connect_manager(self.__connect) dia.doModal() diff --git a/resources/lib/dialogs/servermanual.py b/resources/lib/dialogs/servermanual.py index d54199eb..47339b99 100644 --- a/resources/lib/dialogs/servermanual.py +++ b/resources/lib/dialogs/servermanual.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- - -################################################################################################## - -import logging +############################################################################### +from logging import getLogger import os import xbmcgui @@ -11,12 +9,12 @@ import xbmcaddon import connect.connectionmanager as connectionmanager from utils import language as lang -################################################################################################## +############################################################################### -log = logging.getLogger("EMBY."+__name__) -addon = xbmcaddon.Addon('plugin.video.emby') +log = getLogger("PLEX."+__name__) +addon = xbmcaddon.Addon('plugin.video.plexkodiconnect') -CONN_STATE = connectionmanager.ConnectionState +CONN_STATE = connectionmanager.CONNECTIONSTATE ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 ACTION_BACK = 92 @@ -29,7 +27,7 @@ ERROR = { 'Empty': 2 } -################################################################################################## +############################################################################### class ServerManual(xbmcgui.WindowXMLDialog): @@ -37,7 +35,6 @@ class ServerManual(xbmcgui.WindowXMLDialog): _server = None error = None - def __init__(self, *args, **kwargs): xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)