Initial dialog for manually entering server
This commit is contained in:
parent
78f6ad7da8
commit
10942558cc
4 changed files with 973 additions and 11 deletions
816
resources/lib/connect/connectionmanager.py
Normal file
816
resources/lib/connect/connectionmanager.py
Normal file
|
@ -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 = ("<broadcast>", 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)
|
148
resources/lib/connect/credentials.py
Normal file
148
resources/lib/connect/credentials.py
Normal file
|
@ -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
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue