diff --git a/default.py b/default.py
index 9d9c098b..e11c867c 100644
--- a/default.py
+++ b/default.py
@@ -72,7 +72,8 @@ class Main():
'recentepisodes': entrypoint.getRecentEpisodes,
'refreshplaylist': entrypoint.refreshPlaylist,
'deviceid': entrypoint.resetDeviceId,
- 'delete': entrypoint.deleteItem
+ 'delete': entrypoint.deleteItem,
+ 'connect': entrypoint.emby_connect
}
if "/extrafanart" in sys.argv[0]:
diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml
index 1f626dd9..affdc2da 100644
--- a/resources/language/English/strings.xml
+++ b/resources/language/English/strings.xml
@@ -3,13 +3,13 @@
Emby for Kodi
- Primary Server Address
- Play from HTTP instead of SMB
- Log level
- Device Name
+ Primary Server Address
+ Play from HTTP instead of SMB
+ Log level
+ Device Name
Advanced
- Username
- Port Number
+ Username
+ Port Number
Number of recent Music Albums to show:
Number of recent Movies to show:
@@ -232,12 +232,23 @@
Sign in with Emby Connect
- Username or email:
- Password:
+ Password
Please see our terms of use. The use of any Emby software constitutes acceptance of these terms.
Scan me
Sign in
- Remind me later
+ Cancel
+ Select main server
+ Username or password cannot be empty
+ Unable to connect to the selected server
+ Connect to
+ Manually add server
+ Please sign in
+ Username cannot be empty
+ Connect to server
+ Host
+ Connect
+ Server or port cannot be empty
+ Change Emby Connect user
Welcome
diff --git a/resources/lib/connect.py b/resources/lib/connect.py
deleted file mode 100644
index aa3082a7..00000000
--- a/resources/lib/connect.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# -*- coding: utf-8 -*-
-
-##################################################################################################
-
-import json
-import requests
-import logging
-
-import clientinfo
-from utils import window
-
-##################################################################################################
-
-# Disable requests logging
-from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning
-requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
-requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
-
-log = logging.getLogger("EMBY."+__name__)
-
-##################################################################################################
-
-
-class ConnectUtils():
-
- # Borg - multiple instances, shared state
- _shared_state = {}
- clientInfo = clientinfo.ClientInfo()
-
- # Requests session
- c = None
- timeout = 30
-
-
- def __init__(self):
-
- self.__dict__ = self._shared_state
-
-
- def setUserId(self, userId):
- # Reserved for userclient only
- self.userId = userId
- log.debug("Set connect userId: %s" % userId)
-
- def setServer(self, server):
- # Reserved for userclient only
- self.server = server
- log.debug("Set connect server: %s" % server)
-
- def setToken(self, token):
- # Reserved for userclient only
- self.token = token
- log.debug("Set connect token: %s" % token)
-
-
- def startSession(self):
-
- self.deviceId = self.clientInfo.getDeviceId()
-
- # User is identified from this point
- # Attach authenticated header to the session
- verify = False
- header = self.getHeader()
-
- # If user enabled host certificate verification
- try:
- verify = self.sslverify
- if self.sslclient is not None:
- verify = self.sslclient
- except:
- log.info("Could not load SSL settings.")
-
- # Start session
- self.c = requests.Session()
- self.c.headers = header
- self.c.verify = verify
- # Retry connections to the server
- self.c.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
- self.c.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
-
- log.info("Requests session started on: %s" % self.server)
-
- def stopSession(self):
- try:
- self.c.close()
- except Exception:
- log.warn("Requests session could not be terminated")
-
- def getHeader(self, authenticate=True):
-
- version = self.clientInfo.getVersion()
-
- if not authenticate:
- # If user is not authenticated
- header = {
-
- 'X-Application': "Kodi/%s" % version,
- 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
- 'Accept': "application/json"
- }
- log.info("Header: %s" % header)
-
- else:
- token = self.token
- # Attached to the requests session
- header = {
-
- 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
- 'Accept': "application/json",
- 'X-Application': "Kodi/%s" % version,
- 'X-Connect-UserToken': token
- }
- log.info("Header: %s" % header)
-
- return header
-
- def doUrl(self, url, data=None, postBody=None, rtype="GET",
- parameters=None, authenticate=True, timeout=None):
-
- log.debug("=== ENTER connectUrl ===")
-
- default_link = ""
-
- if timeout is None:
- timeout = self.timeout
-
- # Get requests session
- try:
- # If connect user is authenticated
- if authenticate:
- try:
- c = self.c
- # Replace for the real values
- url = url.replace("{server}", self.server)
- url = url.replace("{UserId}", self.userId)
-
- # Prepare request
- if rtype == "GET":
- r = c.get(url, json=postBody, params=parameters, timeout=timeout)
- elif rtype == "POST":
- r = c.post(url, data=data, timeout=timeout)
- elif rtype == "DELETE":
- r = c.delete(url, json=postBody, timeout=timeout)
-
- except AttributeError:
- # request session does not exists
- self.server = "https://connect.emby.media/service"
- self.userId = window('embyco_currUser')
- self.token = window('embyco_accessToken%s' % self.userId)
-
- header = self.getHeader()
- verifyssl = False
-
- # If user enables ssl verification
- try:
- verifyssl = self.sslverify
- if self.sslclient is not None:
- verifyssl = self.sslclient
- except AttributeError:
- pass
-
- # Prepare request
- if rtype == "GET":
- r = requests.get(url,
- json=postBody,
- params=parameters,
- headers=header,
- timeout=timeout,
- verify=verifyssl)
-
- elif rtype == "POST":
- r = requests.post(url,
- data=data,
- headers=header,
- timeout=timeout,
- verify=verifyssl)
- # If user is not authenticated
- else:
- header = self.getHeader(authenticate=False)
- verifyssl = False
-
- # If user enables ssl verification
- try:
- verifyssl = self.sslverify
- if self.sslclient is not None:
- verifyssl = self.sslclient
- except AttributeError:
- pass
-
- # Prepare request
- if rtype == "GET":
- r = requests.get(url,
- json=postBody,
- params=parameters,
- headers=header,
- timeout=timeout,
- verify=verifyssl)
-
- elif rtype == "POST":
- r = requests.post(url,
- data=data,
- headers=header,
- timeout=timeout,
- verify=verifyssl)
-
- ##### THE RESPONSE #####
- log.info(r.url)
- log.info(r)
-
- if r.status_code == 204:
- # No body in the response
- log.info("====== 204 Success ======")
-
- elif r.status_code == requests.codes.ok:
-
- try:
- # UNICODE - JSON object
- r = r.json()
- log.info("====== 200 Success ======")
- log.info("Response: %s" % r)
- return r
-
- except:
- if r.headers.get('content-type') != "text/html":
- log.info("Unable to convert the response for: %s" % url)
- else:
- r.raise_for_status()
-
- ##### EXCEPTIONS #####
-
- except requests.exceptions.ConnectionError as e:
- # Make the addon aware of status
- pass
-
- except requests.exceptions.ConnectTimeout as e:
- log.warn("Server timeout at: %s" % url)
-
- except requests.exceptions.HTTPError as e:
-
- if r.status_code == 401:
- # Unauthorized
- pass
-
- elif r.status_code in (301, 302):
- # Redirects
- pass
- elif r.status_code == 400:
- # Bad requests
- pass
-
- except requests.exceptions.SSLError as e:
- log.warn("Invalid SSL certificate for: %s" % url)
-
- except requests.exceptions.RequestException as e:
- log.warn("Unknown error connecting to: %s" % url)
-
- return default_link
\ No newline at end of file
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/connectionmanager.py b/resources/lib/connect/connectionmanager.py
new file mode 100644
index 00000000..11dc2514
--- /dev/null
+++ b/resources/lib/connect/connectionmanager.py
@@ -0,0 +1,802 @@
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import hashlib
+import json
+import logging
+import requests
+import socket
+import time
+from datetime import datetime
+
+import credentials as cred
+
+#################################################################################################
+
+# Disable requests logging
+from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning, SNIMissingWarning
+requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
+requests.packages.urllib3.disable_warnings(SNIMissingWarning)
+
+log = logging.getLogger("EMBY."+__name__.split('.')[-1])
+
+#################################################################################################
+
+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):
+
+ 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'] = False
+
+ action = request['type']
+ request.pop('type')
+
+ 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)
+ sock.sendto(MESSAGE, MULTI_GROUP)
+
+ 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)
+ 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 _tryConnect(self, url, timeout=None):
+
+ url = self.getEmbyServerUrl(url, "system/info/public")
+ log.info("tryConnect url: %s" % url)
+
+ return self.requestUrl({
+
+ 'type': "GET",
+ 'url': url,
+ 'dataType': "json",
+ 'timeout': timeout
+ })
+
+ 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 hashlib.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['Name']))
+ try:
+ result = self._tryConnect(address, timeout)
+
+ except Exception:
+ log.error("test failed for connection mode %s with server %s" % (mode, server['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['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)
+
+ 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) 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):
+
+ url = getServerAddress(server, connectionMode)
+ request = {
+
+ 'type': "GET",
+ 'url': self.getEmbyServerUrl(url, "System/Info"),
+ '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']),
+ 'dataType': "json",
+ 'headers': {
+ 'X-MediaBrowser-Token': server['AccessToken']
+ }
+ })
+ # TODO: add apiclient
+
+ 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']
+ self.credentialProvider.getCredentials(credentials)
+ # Signed in
+ self._onConnectUserSignIn(result['User'])
+
+ return result
+
+ 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 _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):
+
+ 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")
+ try:
+ auth = self.requestUrl({
+
+ 'url': url,
+ 'type': "GET",
+ 'dataType': "json",
+ 'params': {
+ 'ConnectUserId': credentials['ConnectUserId']
+ },
+ 'headers': {
+ 'X-MediaBrowser-Token': server['ExchangeToken']
+ }
+ })
+ 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.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)
\ No newline at end of file
diff --git a/resources/lib/connect/credentials.py b/resources/lib/connect/credentials.py
new file mode 100644
index 00000000..9cc779b6
--- /dev/null
+++ b/resources/lib/connect/credentials.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+import logging
+import os
+import time
+from datetime import datetime
+
+#################################################################################################
+
+log = logging.getLogger("EMBY."+__name__.split('.')[-1])
+
+#################################################################################################
+
+
+class Credentials(object):
+
+ credentials = None
+ path = ""
+
+
+ def __init__(self):
+ pass
+
+ 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)
+
+ 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:
+ json.dump(data, outfile, indent=4, 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
\ No newline at end of file
diff --git a/resources/lib/connectmanager.py b/resources/lib/connectmanager.py
new file mode 100644
index 00000000..22afc2eb
--- /dev/null
+++ b/resources/lib/connectmanager.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import logging
+
+import xbmc
+import xbmcaddon
+
+import clientinfo
+import read_embyserver as embyserver
+import connect.connectionmanager as connectionmanager
+from dialog import ServerConnect, UsersConnect, LoginConnect, LoginManual, ServerManual
+
+##################################################################################################
+
+log = logging.getLogger("EMBY."+__name__)
+addon = xbmcaddon.Addon(id='plugin.video.emby')
+
+XML_PATH = (addon.getAddonInfo('path'), "default", "1080i")
+
+##################################################################################################
+
+class ConnectManager(object):
+
+ _shared_state = {} # Borg
+ state = {}
+
+
+ def __init__(self):
+
+ self.__dict__ = self._shared_state
+
+ if not self.state:
+ client_info = clientinfo.ClientInfo()
+ self.emby = embyserver.Read_EmbyServer()
+
+ version = client_info.getVersion()
+ device_name = client_info.getDeviceName()
+ device_id = client_info.getDeviceId()
+
+ self._connect = connectionmanager.ConnectionManager(appName="Kodi",
+ appVersion=version,
+ deviceName=device_name,
+ deviceId=device_id)
+ self._connect.setFilePath(xbmc.translatePath(
+ addon.getAddonInfo('profile')).decode('utf-8'))
+ self.state = self._connect.connect()
+ log.info("Started with: %s", self.state)
+
+
+ def update_state(self):
+
+ self.state = self._connect.connect({'updateDateLastAccessed': False})
+ return self.state
+
+ def get_state(self):
+ return self.state
+
+ def get_server(self, server):
+ return self._connect.connectToAddress(server)
+
+ @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 error
+ state = self._connect.connect({'enableAutoLogin': False})
+ user = state.get('ConnectUser') or {}
+
+ dialog = ServerConnect("script-emby-connect-server.xml", *XML_PATH)
+ kwargs = {
+ 'connect_manager': self._connect,
+ 'username': user.get('DisplayName', ""),
+ 'user_image': user.get('ImageUrl'),
+ 'servers': state['Servers'],
+ 'emby_connect': False if user else True
+ }
+ dialog.set_args(**kwargs)
+ dialog.doModal()
+
+ if dialog.is_server_selected():
+ log.debug("Server selected")
+ return dialog.get_server()
+
+ elif dialog.is_connect_login():
+ log.debug("Login with Emby Connect")
+ try: # Login to emby connect
+ self.login_connect()
+ except RuntimeError:
+ pass
+ return self.select_servers()
+
+ elif dialog.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
+ dialog = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH)
+ dialog.set_connect_manager(self._connect)
+ dialog.doModal()
+
+ if dialog.is_connected():
+ return dialog.get_server()
+ else:
+ raise RuntimeError("Server is not connected")
+
+ def login_connect(self):
+ # Return connect user or raise error
+ dialog = LoginConnect("script-emby-connect-login.xml", *XML_PATH)
+ dialog.set_connect_manager(self._connect)
+ dialog.doModal()
+
+ self.update_state()
+
+ if dialog.is_logged_in():
+ return dialog.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 = self.emby.getUsers(server_address)
+
+ dialog = UsersConnect("script-emby-connect-users.xml", *XML_PATH)
+ dialog.set_server(server_address)
+ dialog.set_users(users)
+ dialog.doModal()
+
+ if dialog.is_user_selected():
+ user = dialog.get_user()
+ if user['HasPassword']:
+ log.debug("User has password, present manual login")
+ try:
+ return self.login_manual(server_address, user)
+ except RuntimeError:
+ return self.login(server)
+ else:
+ user = self.emby.loginUser(server_address, user['Name'])
+ self._connect.onAuthenticated(user)
+ return user
+
+ elif dialog.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
+ dialog = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH)
+ dialog.set_server(server)
+ dialog.set_user(user)
+ dialog.doModal()
+
+ if dialog.is_logged_in():
+ user = dialog.get_user()
+ self._connect.onAuthenticated(user)
+ return user
+ else:
+ raise RuntimeError("User is not authenticated")
diff --git a/resources/lib/dialog/__init__.py b/resources/lib/dialog/__init__.py
index b93054b3..b6c69bf6 100644
--- a/resources/lib/dialog/__init__.py
+++ b/resources/lib/dialog/__init__.py
@@ -1 +1,6 @@
# Dummy file to make this directory a package.
+from serverconnect import ServerConnect
+from usersconnect import UsersConnect
+from loginconnect import LoginConnect
+from loginmanual import LoginManual
+from servermanual import ServerManual
diff --git a/resources/lib/dialog/loginconnect.py b/resources/lib/dialog/loginconnect.py
index 62651e4f..db7c39cc 100644
--- a/resources/lib/dialog/loginconnect.py
+++ b/resources/lib/dialog/loginconnect.py
@@ -2,52 +2,63 @@
##################################################################################################
+import logging
import os
import xbmcgui
import xbmcaddon
+from utils import language as lang
+
##################################################################################################
+log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
+ACTION_PARENT_DIR = 9
+ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
SIGN_IN = 200
-REMIND_LATER = 201
+CANCEL = 201
+ERROR_TOGGLE = 202
+ERROR_MSG = 203
+ERROR = {
+ 'Invalid': 1,
+ 'Empty': 2
+}
+
+##################################################################################################
class LoginConnect(xbmcgui.WindowXMLDialog):
+ _user = None
+ error = None
+
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
- def __add_editcontrol(self, x, y, height, width, password=0):
-
- media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
- control = xbmcgui.ControlEdit(0,0,0,0,
- label="User",
- font="font10",
- textColor="ff464646",
- focusTexture=os.path.join(media, "button-focus.png"),
- noFocusTexture=os.path.join(media, "button-focus.png"),
- isPassword=password)
+ def set_connect_manager(self, connect_manager):
+ self.connect_manager = connect_manager
- control.setPosition(x,y)
- control.setHeight(height)
- control.setWidth(width)
+ def is_logged_in(self):
+ return True if self._user else False
+
+ def get_user(self):
+ return self._user
- self.addControl(control)
- return control
def onInit(self):
-
- self.user_field = self.__add_editcontrol(685,385,40,500)
+
+ self.user_field = self._add_editcontrol(725, 385, 40, 500)
self.setFocus(self.user_field)
- self.password_field = self.__add_editcontrol(685,470,40,500, password=1)
+ self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1)
self.signin_button = self.getControl(SIGN_IN)
- self.remind_button = self.getControl(REMIND_LATER)
+ self.remind_button = self.getControl(CANCEL)
+ self.error_toggle = self.getControl(ERROR_TOGGLE)
+ self.error_msg = self.getControl(ERROR_MSG)
self.user_field.controlUp(self.remind_button)
self.user_field.controlDown(self.password_field)
@@ -60,17 +71,66 @@ class LoginConnect(xbmcgui.WindowXMLDialog):
if control == SIGN_IN:
# Sign in to emby connect
- self.user = self.user_field.getText()
- __password = self.password_field.getText()
+ self._disable_error()
- ### REVIEW ONCE CONNECT MODULE IS MADE
- self.close()
+ user = self.user_field.getText()
+ password = self.password_field.getText()
- elif control == REMIND_LATER:
+ if not user or not password:
+ # Display error
+ self._error(ERROR['Empty'], lang(30608))
+ log.error("Username or password cannot be null")
+
+ elif self._login(user, password):
+ self.close()
+
+ elif control == CANCEL:
# Remind me later
self.close()
def onAction(self, action):
- if action == ACTION_BACK:
- self.close()
\ No newline at end of file
+ if (self.error == ERROR['Empty']
+ and self.user_field.getText() and self.password_field.getText()):
+ self._disable_error()
+
+ if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
+ self.close()
+
+ def _add_editcontrol(self, x, y, height, width, password=0):
+
+ media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
+ control = xbmcgui.ControlEdit(0, 0, 0, 0,
+ label="User",
+ font="font10",
+ textColor="ff525252",
+ focusTexture=os.path.join(media, "button-focus.png"),
+ noFocusTexture=os.path.join(media, "button-focus.png"),
+ isPassword=password)
+ control.setPosition(x, y)
+ control.setHeight(height)
+ control.setWidth(width)
+
+ self.addControl(control)
+ return control
+
+ def _login(self, username, password):
+
+ result = self.connect_manager.loginToConnect(username, password)
+ if result is False:
+ self._error(ERROR['Invalid'], lang(33009))
+ return False
+ else:
+ self._user = result
+ return True
+
+ def _error(self, state, message):
+
+ self.error = state
+ self.error_msg.setLabel(message)
+ self.error_toggle.setVisibleCondition('True')
+
+ def _disable_error(self):
+
+ self.error = None
+ self.error_toggle.setVisibleCondition('False')
diff --git a/resources/lib/dialog/loginmanual.py b/resources/lib/dialog/loginmanual.py
new file mode 100644
index 00000000..aa34986b
--- /dev/null
+++ b/resources/lib/dialog/loginmanual.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import logging
+import os
+
+import xbmcgui
+import xbmcaddon
+
+import read_embyserver as embyserver
+from utils import language as lang
+
+##################################################################################################
+
+log = logging.getLogger("EMBY."+__name__)
+addon = xbmcaddon.Addon('plugin.video.emby')
+
+ACTION_PARENT_DIR = 9
+ACTION_PREVIOUS_MENU = 10
+ACTION_BACK = 92
+SIGN_IN = 200
+CANCEL = 201
+ERROR_TOGGLE = 202
+ERROR_MSG = 203
+ERROR = {
+ 'Invalid': 1,
+ 'Empty': 2
+}
+
+##################################################################################################
+
+
+class LoginManual(xbmcgui.WindowXMLDialog):
+
+ _user = None
+ error = None
+
+
+ def __init__(self, *args, **kwargs):
+
+ self.emby = embyserver.Read_EmbyServer()
+ xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
+
+ def is_logged_in(self):
+ return True if self._user else False
+
+ def set_server(self, server):
+ self.server = server
+
+ def set_user(self, user):
+ self.user = user or {}
+
+ def get_user(self):
+ return self._user
+
+ def onInit(self):
+
+ self.signin_button = self.getControl(SIGN_IN)
+ self.cancel_button = self.getControl(CANCEL)
+ self.error_toggle = self.getControl(ERROR_TOGGLE)
+ self.error_msg = self.getControl(ERROR_MSG)
+ self.user_field = self._add_editcontrol(725, 400, 40, 500)
+ self.password_field = self._add_editcontrol(725, 475, 40, 500, password=1)
+
+ if "Name" in self.user:
+ self.user_field.setText(self.user['Name'])
+ self.setFocus(self.password_field)
+ else:
+ self.setFocus(self.user_field)
+
+ self.user_field.controlUp(self.cancel_button)
+ self.user_field.controlDown(self.password_field)
+ self.password_field.controlUp(self.user_field)
+ self.password_field.controlDown(self.signin_button)
+ self.signin_button.controlUp(self.password_field)
+ self.cancel_button.controlDown(self.user_field)
+
+ def onClick(self, control):
+
+ if control == SIGN_IN:
+ # Sign in to emby connect
+ self._disable_error()
+
+ user = self.user_field.getText()
+ password = self.password_field.getText()
+
+ if not user:
+ # Display error
+ self._error(ERROR['Empty'], lang(30613))
+ log.error("Username cannot be null")
+
+ elif self._login(user, password):
+ self.close()
+
+ elif control == CANCEL:
+ # Remind me later
+ self.close()
+
+ def onAction(self, action):
+
+ if self.error == ERROR['Empty'] and self.user_field.getText():
+ self._disable_error()
+
+ if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
+ self.close()
+
+ def _add_editcontrol(self, x, y, height, width, password=0):
+
+ media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
+ control = xbmcgui.ControlEdit(0, 0, 0, 0,
+ label="User",
+ font="font10",
+ textColor="ff525252",
+ focusTexture=os.path.join(media, "button-focus.png"),
+ noFocusTexture=os.path.join(media, "button-focus.png"),
+ isPassword=password)
+ control.setPosition(x, y)
+ control.setHeight(height)
+ control.setWidth(width)
+
+ self.addControl(control)
+ return control
+
+ def _login(self, username, password):
+
+ result = self.emby.loginUser(self.server, username, password)
+ if not result:
+ self._error(ERROR['Invalid'], lang(33009))
+ return False
+ else:
+ self._user = result
+ return True
+
+ def _error(self, state, message):
+
+ self.error = state
+ self.error_msg.setLabel(message)
+ self.error_toggle.setVisibleCondition('True')
+
+ def _disable_error(self):
+
+ self.error = None
+ self.error_toggle.setVisibleCondition('False')
diff --git a/resources/lib/dialog/serverconnect.py b/resources/lib/dialog/serverconnect.py
new file mode 100644
index 00000000..8a39b09b
--- /dev/null
+++ b/resources/lib/dialog/serverconnect.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import logging
+
+import xbmc
+import xbmcgui
+
+import connect.connectionmanager as connectionmanager
+from utils import language as lang
+
+##################################################################################################
+
+log = logging.getLogger("EMBY."+__name__)
+
+CONN_STATE = connectionmanager.ConnectionState
+ACTION_PARENT_DIR = 9
+ACTION_PREVIOUS_MENU = 10
+ACTION_BACK = 92
+ACTION_SELECT_ITEM = 7
+ACTION_MOUSE_LEFT_CLICK = 100
+USER_IMAGE = 150
+USER_NAME = 151
+LIST = 155
+CANCEL = 201
+MESSAGE_BOX = 202
+MESSAGE = 203
+BUSY = 204
+EMBY_CONNECT = 205
+MANUAL_SERVER = 206
+
+##################################################################################################
+
+
+class ServerConnect(xbmcgui.WindowXMLDialog):
+
+ username = ""
+ user_image = None
+ servers = []
+
+ _selected_server = None
+ _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():
+ setattr(self, key, value)
+
+ def is_server_selected(self):
+ return True if self._selected_server else False
+
+ def get_server(self):
+ return self._selected_server
+
+ def is_connect_login(self):
+ return self._connect_login
+
+ def is_manual_server(self):
+ return self._manual_server
+
+
+ def onInit(self):
+
+ self.message = self.getControl(MESSAGE)
+ self.message_box = self.getControl(MESSAGE_BOX)
+ self.busy = self.getControl(BUSY)
+ self.list_ = self.getControl(LIST)
+
+ for server in self.servers:
+ 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')))
+
+ 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.setFocus(self.list_)
+
+ @classmethod
+ def _add_listitem(cls, label, server_id, server_type):
+
+ item = xbmcgui.ListItem(label)
+ item.setProperty('id', server_id)
+ item.setProperty('server_type', server_type)
+
+ return item
+
+ def onAction(self, action):
+
+ if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
+ self.close()
+
+ if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
+
+ if self.getFocusId() == LIST:
+ server = self.list_.getSelectedItem()
+ selected_id = server.getProperty('id')
+ log.info('Server Id selected: %s', selected_id)
+
+ if self._connect_server(selected_id):
+ self.message_box.setVisibleCondition('False')
+ self.close()
+
+ def onClick(self, control):
+
+ if control == EMBY_CONNECT:
+ self.connect_manager.clearData()
+ self._connect_login = True
+ self.close()
+
+ elif control == MANUAL_SERVER:
+ self._manual_server = True
+ self.close()
+
+ elif control == CANCEL:
+ self.close()
+
+ def _connect_server(self, server_id):
+
+ server = self.connect_manager.getServerInfo(server_id)
+ self.message.setLabel("%s %s..." % (lang(30610), server['Name']))
+ self.message_box.setVisibleCondition('True')
+ self.busy.setVisibleCondition('True')
+ result = self.connect_manager.connectToServer(server)
+
+ if result['State'] == CONN_STATE['Unavailable']:
+ self.busy.setVisibleCondition('False')
+ self.message.setLabel(lang(30609))
+ return False
+ else:
+ xbmc.sleep(1000)
+ self._selected_server = result['Servers'][0]
+ return True
diff --git a/resources/lib/dialog/servermanual.py b/resources/lib/dialog/servermanual.py
new file mode 100644
index 00000000..d54199eb
--- /dev/null
+++ b/resources/lib/dialog/servermanual.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import logging
+import os
+
+import xbmcgui
+import xbmcaddon
+
+import connect.connectionmanager as connectionmanager
+from utils import language as lang
+
+##################################################################################################
+
+log = logging.getLogger("EMBY."+__name__)
+addon = xbmcaddon.Addon('plugin.video.emby')
+
+CONN_STATE = connectionmanager.ConnectionState
+ACTION_PARENT_DIR = 9
+ACTION_PREVIOUS_MENU = 10
+ACTION_BACK = 92
+CONNECT = 200
+CANCEL = 201
+ERROR_TOGGLE = 202
+ERROR_MSG = 203
+ERROR = {
+ 'Invalid': 1,
+ 'Empty': 2
+}
+
+##################################################################################################
+
+
+class ServerManual(xbmcgui.WindowXMLDialog):
+
+ _server = None
+ error = None
+
+
+ def __init__(self, *args, **kwargs):
+
+ xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
+
+ def set_connect_manager(self, connect_manager):
+ self.connect_manager = connect_manager
+
+ def is_connected(self):
+ return True if self._server else False
+
+ def get_server(self):
+ return self._server
+
+ def onInit(self):
+
+ self.connect_button = self.getControl(CONNECT)
+ self.cancel_button = self.getControl(CANCEL)
+ self.error_toggle = self.getControl(ERROR_TOGGLE)
+ self.error_msg = self.getControl(ERROR_MSG)
+ self.host_field = self._add_editcontrol(725, 400, 40, 500)
+ self.port_field = self._add_editcontrol(725, 525, 40, 500)
+
+ self.port_field.setText('8096')
+ self.setFocus(self.host_field)
+
+ self.host_field.controlUp(self.cancel_button)
+ self.host_field.controlDown(self.port_field)
+ self.port_field.controlUp(self.host_field)
+ self.port_field.controlDown(self.connect_button)
+ self.connect_button.controlUp(self.port_field)
+ self.cancel_button.controlDown(self.host_field)
+
+ def onClick(self, control):
+
+ if control == CONNECT:
+ # Sign in to emby connect
+ self._disable_error()
+
+ server = self.host_field.getText()
+ port = self.port_field.getText()
+
+ if not server or not port:
+ # Display error
+ self._error(ERROR['Empty'], lang(30617))
+ log.error("Server or port cannot be null")
+
+ elif self._connect_to_server(server, port):
+ self.close()
+
+ elif control == CANCEL:
+ # Remind me later
+ self.close()
+
+ def onAction(self, action):
+
+ if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText():
+ self._disable_error()
+
+ if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
+ self.close()
+
+ def _add_editcontrol(self, x, y, height, width):
+
+ media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
+ control = xbmcgui.ControlEdit(0, 0, 0, 0,
+ label="User",
+ font="font10",
+ textColor="ffc2c2c2",
+ focusTexture=os.path.join(media, "button-focus.png"),
+ noFocusTexture=os.path.join(media, "button-focus.png"))
+ control.setPosition(x, y)
+ control.setHeight(height)
+ control.setWidth(width)
+
+ self.addControl(control)
+ return control
+
+ def _connect_to_server(self, server, port):
+
+ server_address = "%s:%s" % (server, port)
+ self._message("%s %s..." % (lang(30610), server_address))
+ result = self.connect_manager.connectToAddress(server_address)
+
+ if result['State'] == CONN_STATE['Unavailable']:
+ self._message(lang(30609))
+ return False
+ else:
+ self._server = result['Servers'][0]
+ return True
+
+ def _message(self, message):
+
+ self.error_msg.setLabel(message)
+ self.error_toggle.setVisibleCondition('True')
+
+ def _error(self, state, message):
+
+ self.error = state
+ self.error_msg.setLabel(message)
+ self.error_toggle.setVisibleCondition('True')
+
+ def _disable_error(self):
+
+ self.error = None
+ self.error_toggle.setVisibleCondition('False')
diff --git a/resources/lib/dialog/usersconnect.py b/resources/lib/dialog/usersconnect.py
new file mode 100644
index 00000000..314d2e23
--- /dev/null
+++ b/resources/lib/dialog/usersconnect.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import logging
+
+import xbmcgui
+
+##################################################################################################
+
+log = logging.getLogger("EMBY."+__name__)
+
+ACTION_PARENT_DIR = 9
+ACTION_PREVIOUS_MENU = 10
+ACTION_BACK = 92
+ACTION_SELECT_ITEM = 7
+ACTION_MOUSE_LEFT_CLICK = 100
+LIST = 155
+MANUAL = 200
+CANCEL = 201
+
+##################################################################################################
+
+
+class UsersConnect(xbmcgui.WindowXMLDialog):
+
+ _user = None
+ _manual_login = False
+
+
+ def __init__(self, *args, **kwargs):
+
+ xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
+
+ def set_server(self, server):
+ self.server = server
+
+ def set_users(self, users):
+ self.users = users
+
+ def is_user_selected(self):
+ return True if self._user else False
+
+ def get_user(self):
+ return self._user
+
+ def is_manual_login(self):
+ return self._manual_login
+
+
+ def onInit(self):
+
+ self.list_ = self.getControl(LIST)
+ for user in self.users:
+ user_image = ("userflyoutdefault2.png" if not user.get('PrimaryImageTag')
+ else self._get_user_artwork(user['Id'], 'Primary'))
+ self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image))
+
+ self.setFocus(self.list_)
+
+ @classmethod
+ def _add_listitem(cls, label, user_id, user_image):
+
+ item = xbmcgui.ListItem(label)
+ item.setProperty('id', user_id)
+ item.setArt({'Icon': user_image})
+
+ return item
+
+ def onAction(self, action):
+
+ if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
+ self.close()
+
+ if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
+
+ if self.getFocusId() == LIST:
+ user = self.list_.getSelectedItem()
+ selected_id = user.getProperty('id')
+ log.info('User Id selected: %s', selected_id)
+
+ for user in self.users:
+ if user['Id'] == selected_id:
+ self._user = user
+ break
+
+ self.close()
+
+ def onClick(self, control):
+
+ if control == MANUAL:
+ self._manual_login = True
+ self.close()
+
+ elif control == CANCEL:
+ self.close()
+
+ def _get_user_artwork(self, user_id, art_type):
+ # Load user information set by UserClient
+ return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, user_id, art_type)
diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py
index a74b634f..bdb9ad48 100644
--- a/resources/lib/downloadutils.py
+++ b/resources/lib/downloadutils.py
@@ -10,7 +10,7 @@ import xbmc
import xbmcgui
import clientinfo
-from utils import window, settings
+from utils import window, settings, language as lang
##################################################################################################
@@ -40,11 +40,6 @@ class DownloadUtils():
self.__dict__ = self._shared_state
- def setUsername(self, username):
- # Reserved for userclient only
- self.username = username
- log.debug("Set username: %s" % username)
-
def setUserId(self, userId):
# Reserved for userclient only
self.userId = userId
@@ -60,12 +55,10 @@ class DownloadUtils():
self.token = token
log.debug("Set token: %s" % token)
- def setSSL(self, ssl, sslclient):
+ def setSSL(self, ssl):
# Reserved for userclient only
self.sslverify = ssl
- self.sslclient = sslclient
- log.debug("Verify SSL host certificate: %s" % ssl)
- log.debug("SSL client side certificate: %s" % sslclient)
+ log.debug("Verify SSL verify/certificate: %s" % ssl)
def postCapabilities(self, deviceId):
@@ -149,8 +142,6 @@ class DownloadUtils():
# If user enabled host certificate verification
try:
verify = self.sslverify
- if self.sslclient is not None:
- verify = self.sslclient
except:
log.info("Could not load SSL settings.")
@@ -214,47 +205,31 @@ class DownloadUtils():
default_link = ""
try:
- if authenticate:
+ if self.s is not None:
+ session = self.s
+ else:
+ # request session does not exists
+ # Get user information
+ self.userId = window('emby_currUser')
+ self.server = window('emby_server%s' % self.userId)
+ self.token = window('emby_accessToken%s' % self.userId)
+ verifyssl = False
- if self.s is not None:
- session = self.s
- else:
- # request session does not exists
- # Get user information
- self.userId = window('emby_currUser')
- self.server = window('emby_server%s' % self.userId)
- self.token = window('emby_accessToken%s' % self.userId)
- verifyssl = False
-
- # IF user enables ssl verification
- if settings('sslverify') == "true":
- verifyssl = True
- if settings('sslcert') != "None":
- verifyssl = settings('sslcert')
-
- kwargs.update({
- 'verify': verifyssl,
- 'headers': self.getHeader()
- })
-
- # Replace for the real values
- url = url.replace("{server}", self.server)
- url = url.replace("{UserId}", self.userId)
-
- else: # User is not authenticated
- # If user enables ssl verification
- try:
- verifyssl = self.sslverify
- if self.sslclient is not None:
- verifyssl = self.sslclient
- except AttributeError:
- verifyssl = False
+ # IF user enables ssl verification
+ if settings('sslverify') == "true":
+ verifyssl = True
+ if settings('sslcert') != "None":
+ verifyssl = settings('sslcert')
kwargs.update({
'verify': verifyssl,
- 'headers': self.getHeader(authenticate=False)
+ 'headers': self.getHeader(authenticate)
})
+ # Replace for the real values
+ url = url.replace("{server}", self.server)
+ url = url.replace("{UserId}", self.userId)
+
##### PREPARE REQUEST #####
kwargs.update({
'url': url,
@@ -310,13 +285,15 @@ class DownloadUtils():
# Emby server errors
if r.headers['X-Application-Error-Code'] == "ParentalControl":
# Parental control - access restricted
+ if status != "restricted":
+ xbmcgui.Dialog().notification(
+ heading=lang(29999),
+ message="Access restricted.",
+ icon=xbmcgui.NOTIFICATION_ERROR,
+ time=5000)
+
window('emby_serverStatus', value="restricted")
- xbmcgui.Dialog().notification(
- heading="Emby server",
- message="Access restricted.",
- icon=xbmcgui.NOTIFICATION_ERROR,
- time=5000)
- return False
+ raise Warning('restricted')
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
# User tried to do something his emby account doesn't allow
@@ -330,7 +307,7 @@ class DownloadUtils():
heading="Error connecting",
message="Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
- return 401
+ raise Warning('401')
elif r.status_code in (301, 302):
# Redirects
diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py
index e532e5e2..6074280c 100644
--- a/resources/lib/entrypoint.py
+++ b/resources/lib/entrypoint.py
@@ -17,6 +17,7 @@ import xbmcplugin
import artwork
import utils
import clientinfo
+import connectmanager
import downloadutils
import librarysync
import read_embyserver as embyserver
@@ -102,7 +103,6 @@ def doMainListing():
addDirectoryItem(lang(33052),
"plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root")
- # some extra entries for settings and stuff. TODO --> localize the labels
addDirectoryItem(lang(30517), "plugin://plugin.video.emby/?mode=passwords")
addDirectoryItem(lang(33053), "plugin://plugin.video.emby/?mode=settings")
addDirectoryItem(lang(33054), "plugin://plugin.video.emby/?mode=adduser")
@@ -115,6 +115,26 @@ def doMainListing():
xbmcplugin.endOfDirectory(int(sys.argv[1]))
+def emby_connect():
+ # Login user to emby connect
+ connect = connectmanager.ConnectManager()
+ try:
+ connectUser = connect.login_connect()
+ except RuntimeError:
+ return
+ else:
+ user = connectUser['User']
+ token = connectUser['AccessToken']
+ username = user['Name']
+ icon = user.get('ImageUrl') or "special://home/addons/plugin.video.emby/icon.png"
+ xbmcgui.Dialog().notification(heading=lang(29999),
+ message="%s %s" % (lang(33000), username.decode('utf-8')),
+ icon=icon,
+ time=1500,
+ sound=False)
+
+ settings('connectUsername', value=username)
+
##### Generate a new deviceId
def resetDeviceId():
diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py
index 032affb1..f59dc47b 100644
--- a/resources/lib/initialsetup.py
+++ b/resources/lib/initialsetup.py
@@ -2,174 +2,125 @@
#################################################################################################
-import json
import logging
-import socket
import xbmc
import xbmcgui
-import xbmcaddon
import clientinfo
-import downloadutils
+import connectmanager
+import connect.connectionmanager as connectionmanager
import userclient
from utils import settings, language as lang, passwordsXML
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
+STATE = connectionmanager.ConnectionState
#################################################################################################
-class InitialSetup():
+class InitialSetup(object):
def __init__(self):
- self.addonId = clientinfo.ClientInfo().getAddonId()
- self.doUtils = downloadutils.DownloadUtils().downloadUrl
- self.userClient = userclient.UserClient()
+ self.addon_id = clientinfo.ClientInfo().getAddonId()
+ self.user_client = userclient.UserClient()
+ self.connectmanager = connectmanager.ConnectManager()
def setup(self):
# Check server, user, direct paths, music, direct stream if not direct path.
- addonId = self.addonId
+ addon_id = self.addon_id
dialog = xbmcgui.Dialog()
##### SERVER INFO #####
-
+
log.debug("Initial setup called.")
- server = self.userClient.getServer()
- if server:
- log.debug("Server is already set.")
+ ###$ Begin transition phase $###
+ if settings('server') == "":
+ current_server = self.user_client.get_server()
+ self.connectmanager.get_server(current_server)
+ self.user_client.get_userid()
+ self.user_client.get_token()
+ ###$ End transition phase $###
+
+ current_state = self.connectmanager.get_state()
+ if current_state['State'] == STATE['SignedIn']:
+ server = current_state['Servers'][0]
+ server_address = self.connectmanager.get_address(server)
+ self._set_server(server_address, server['Name'])
+ self._set_user(server['UserId'], server['AccessToken'])
return
-
- log.debug("Looking for server...")
- server = self.getServerDetails()
- log.debug("Found: %s" % server)
+
try:
- prefix, ip, port = server.replace("/", "").split(":")
- except Exception: # Failed to retrieve server information
- log.error("getServerDetails failed.")
- xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
- return
- else:
- server_confirm = dialog.yesno(
- heading=lang(29999),
- line1=lang(33034),
- line2="%s %s" % (lang(30169), server))
- if server_confirm:
- # Correct server found
- log.info("Server is selected. Saving the information.")
- settings('ipaddress', value=ip)
- settings('port', value=port)
+ server = self.connectmanager.select_servers()
+ log.info("Server: %s", server)
- if prefix == "https":
- settings('https', value="true")
+ except RuntimeError as error:
+ log.exception(error)
+ xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id)
+ return
+
+ else:
+ server_address = self.connectmanager.get_address(server)
+ self._set_server(server_address, server['Name'])
+
+ if not server.get('AccessToken') and not server.get('UserId'):
+ try:
+ user = self.connectmanager.login(server)
+ log.info("User authenticated: %s", user)
+ except RuntimeError as error:
+ log.exception(error)
+ xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id)
+ return
+ settings('username', value=user['User']['Name'])
+ self._set_user(user['User']['Id'], user['AccessToken'])
else:
- # User selected no or cancelled the dialog
- log.info("No server selected.")
- xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
- return
-
- ##### USER INFO #####
-
- log.info("Getting user list.")
-
- result = self.doUtils("%s/emby/Users/Public?format=json" % server, authenticate=False)
- if result == "":
- log.info("Unable to connect to %s" % server)
- return
-
- log.debug("Response: %s" % result)
- # Process the list of users
- usernames = []
- users_hasPassword = []
-
- for user in result:
- # Username
- name = user['Name']
- usernames.append(name)
- # Password
- if user['HasPassword']:
- name = "%s (secure)" % name
- users_hasPassword.append(name)
-
- log.info("Presenting user list: %s" % users_hasPassword)
- user_select = dialog.select(lang(30200), users_hasPassword)
- if user_select > -1:
- selected_user = usernames[user_select]
- log.info("Selected user: %s" % selected_user)
- settings('username', value=selected_user)
- else:
- log.info("No user selected.")
- xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
- return
+ user = self.connectmanager.get_state()
+ settings('connectUsername', value=user['ConnectUser']['Name'])
+ self._set_user(server['UserId'], server['AccessToken'])
##### ADDITIONAL PROMPTS #####
- directPaths = dialog.yesno(
- heading=lang(30511),
- line1=lang(33035),
- nolabel=lang(33036),
- yeslabel=lang(33037))
- if directPaths:
+ direct_paths = dialog.yesno(heading=lang(30511),
+ line1=lang(33035),
+ nolabel=lang(33036),
+ yeslabel=lang(33037))
+ if direct_paths:
log.info("User opted to use direct paths.")
settings('useDirectPaths', value="1")
# ask for credentials
- credentials = dialog.yesno(
- heading=lang(30517),
- line1= lang(33038))
+ credentials = dialog.yesno(heading=lang(30517), line1=lang(33038))
if credentials:
log.info("Presenting network credentials dialog.")
passwordsXML()
-
- musicDisabled = dialog.yesno(
- heading=lang(29999),
- line1=lang(33039))
- if musicDisabled:
+
+ music_disabled = dialog.yesno(heading=lang(29999), line1=lang(33039))
+ if music_disabled:
log.info("User opted to disable Emby music library.")
settings('enableMusic', value="false")
else:
# Only prompt if the user didn't select direct paths for videos
- if not directPaths:
- musicAccess = dialog.yesno(
- heading=lang(29999),
- line1=lang(33040))
- if musicAccess:
+ if not direct_paths:
+ music_access = dialog.yesno(heading=lang(29999), line1=lang(33040))
+ if music_access:
log.info("User opted to direct stream music.")
settings('streamMusic', value="true")
-
- def getServerDetails(self):
- log.info("Getting Server Details from Network")
-
- MULTI_GROUP = ("", 7359)
- MESSAGE = "who is EmbyServer?"
-
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.settimeout(6.0)
+ @classmethod
+ def _set_server(cls, server, name):
- 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)
- sock.sendto(MESSAGE, MULTI_GROUP)
-
- try:
- data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
- log.info("Received Response: %s" % data)
- except Exception:
- log.error("No UDP Response")
- return None
- else:
- # Get the address
- data = json.loads(data)
- return data['Address']
\ No newline at end of file
+ settings('serverName', value=name)
+ settings('server', value=server)
+ log.info("Saved server information: %s", server)
+
+ @classmethod
+ def _set_user(cls, user_id, token):
+
+ settings('userId', value=user_id)
+ settings('token', value=token)
diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py
index 73dc4c59..18d7b9f3 100644
--- a/resources/lib/itemtypes.py
+++ b/resources/lib/itemtypes.py
@@ -1207,6 +1207,10 @@ class TVShows(Items):
artwork = self.artwork
API = api.API(item)
+ if item.get('LocationType') == "Virtual": # TODO: Filter via api instead
+ log.info("Skipping virtual episode: %s", item['Name'])
+ return
+
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py
index b90a671b..ad5c6b82 100644
--- a/resources/lib/librarysync.py
+++ b/resources/lib/librarysync.py
@@ -905,6 +905,9 @@ class LibrarySync(threading.Thread):
try:
self.run_internal()
+ except Warning as e:
+ if "restricted" in e:
+ pass
except Exception as e:
window('emby_dbScan', clear=True)
log.exception(e)
diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py
index 091e1730..448a9b59 100644
--- a/resources/lib/read_embyserver.py
+++ b/resources/lib/read_embyserver.py
@@ -3,6 +3,7 @@
#################################################################################################
import logging
+import hashlib
import xbmc
@@ -571,4 +572,20 @@ class Read_EmbyServer():
def deleteItem(self, itemid):
url = "{server}/emby/Items/%s?format=json" % itemid
- self.doUtils(url, action_type="DELETE")
\ No newline at end of file
+ self.doUtils(url, action_type="DELETE")
+
+ def getUsers(self, server):
+
+ url = "%s/emby/Users/Public?format=json" % server
+ users = self.doUtils(url, authenticate=False)
+
+ return users or []
+
+ def loginUser(self, server, username, password=None):
+
+ password = password or ""
+ url = "%s/emby/Users/AuthenticateByName?format=json" % server
+ data = {'username': username, 'password': hashlib.sha1(password).hexdigest()}
+ user = self.doUtils(url, postBody=data, action_type="POST", authenticate=False)
+
+ return user
\ No newline at end of file
diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py
index 37282efb..b5f8534b 100644
--- a/resources/lib/userclient.py
+++ b/resources/lib/userclient.py
@@ -2,18 +2,16 @@
##################################################################################################
-import hashlib
import logging
import threading
import xbmc
import xbmcgui
-import xbmcaddon
-import xbmcvfs
import artwork
-import clientinfo
+import connectmanager
import downloadutils
+import read_embyserver as embyserver
from utils import window, settings, language as lang
##################################################################################################
@@ -22,426 +20,276 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
-
class UserClient(threading.Thread):
- # Borg - multiple instances, shared state
- _shared_state = {}
+ _shared_state = {} # Borg
- stop_thread = False
- auth = True
- retry = 0
+ _stop_thread = False
+ _user = None
+ _server = None
- currUser = None
- currUserId = None
- currServer = None
- currToken = None
- HasAccess = True
- AdditionalUser = []
-
- userSettings = None
+ _auth = True
+ _has_access = True
def __init__(self):
self.__dict__ = self._shared_state
- self.addon = xbmcaddon.Addon()
- self.doUtils = downloadutils.DownloadUtils()
+ self.doutils = downloadutils.DownloadUtils()
+ self.download = self.doutils.downloadUrl
+ self.emby = embyserver.Read_EmbyServer()
threading.Thread.__init__(self)
+ @classmethod
+ def get_username(cls):
+ return settings('username') or settings('connectUsername') or None
- def getAdditionalUsers(self):
+ def get_user(self, data=None):
- additionalUsers = settings('additionalUsers')
+ if data is not None:
+ self._user = data
+ self._set_user_server()
- if additionalUsers:
- self.AdditionalUser = additionalUsers.split(',')
+ return self._user
- def getUsername(self):
+ def get_server_details(self):
+ return self._server
- username = settings('username')
+ @classmethod
+ def get_server(cls):
- if not username:
- log.debug("No username saved.")
- return ""
-
- return username
-
- def getLogLevel(self):
-
- try:
- logLevel = int(settings('logLevel'))
- except ValueError:
- logLevel = 0
-
- return logLevel
-
- def getUserId(self):
-
- username = self.getUsername()
- w_userId = window('emby_currUser')
- s_userId = settings('userId%s' % username)
-
- # Verify the window property
- if w_userId:
- if not s_userId:
- # Save access token if it's missing from settings
- settings('userId%s' % username, value=w_userId)
- log.debug("Returning userId from WINDOW for username: %s UserId: %s"
- % (username, w_userId))
- return w_userId
- # Verify the settings
- elif s_userId:
- log.debug("Returning userId from SETTINGS for username: %s userId: %s"
- % (username, s_userId))
- return s_userId
- # No userId found
- else:
- log.info("No userId saved for username: %s." % username)
-
- def getServer(self, prefix=True):
-
- alternate = settings('altip') == "true"
- if alternate:
- # Alternate host
- HTTPS = settings('secondhttps') == "true"
- host = settings('secondipaddress')
- port = settings('secondport')
- else:
- # Original host
- HTTPS = settings('https') == "true"
+ ###$ Begin transition phase $###
+ if settings('server') == "":
+ http = "https" if settings('https') == "true" else "http"
host = settings('ipaddress')
port = settings('port')
- server = host + ":" + port
+ if host and port:
+ settings('server', value="%s://%s:%s" % (http, host, port))
+ ###$ End transition phase $###
- if not host:
- log.debug("No server information saved.")
- return False
+ return settings('server') or None
- # If https is true
- if prefix and HTTPS:
- server = "https://%s" % server
- return server
- # If https is false
- elif prefix and not HTTPS:
- server = "http://%s" % server
- return server
- # If only the host:port is required
- elif not prefix:
- return server
+ def verify_server(self):
- def getToken(self):
-
- username = self.getUsername()
- userId = self.getUserId()
- w_token = window('emby_accessToken%s' % userId)
- s_token = settings('accessToken')
-
- # Verify the window property
- if w_token:
- if not s_token:
- # Save access token if it's missing from settings
- settings('accessToken', value=w_token)
- log.debug("Returning accessToken from WINDOW for username: %s accessToken: %s"
- % (username, w_token))
- return w_token
- # Verify the settings
- elif s_token:
- log.debug("Returning accessToken from SETTINGS for username: %s accessToken: %s"
- % (username, s_token))
- window('emby_accessToken%s' % username, value=s_token)
- return s_token
- else:
- log.info("No token found.")
- return ""
-
- def getSSLverify(self):
- # Verify host certificate
- s_sslverify = settings('sslverify')
- if settings('altip') == "true":
- s_sslverify = settings('secondsslverify')
-
- if s_sslverify == "true":
+ url = "%s/emby/Users/Public?format=json" % self.get_server()
+ result = self.download(url, authenticate=False)
+ if result != "": # Specific verification, due to possibility of returning empty dict
return True
- else:
- return False
-
- def getSSL(self):
- # Client side certificate
- s_cert = settings('sslcert')
- if settings('altip') == "true":
- s_cert = settings('secondsslcert')
-
- if s_cert == "None":
- return None
- else:
- return s_cert
-
- def setUserPref(self):
-
- doUtils = self.doUtils.downloadUrl
-
- result = doUtils("{server}/emby/Users/{UserId}?format=json")
- self.userSettings = result
- # Set user image for skin display
- if result.get('PrimaryImageTag'):
- window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result['Id'], 'Primary'))
-
- # Set resume point max
- result = doUtils("{server}/emby/System/Configuration?format=json")
- settings('markPlayed', value=str(result['MaxResumePct']))
-
- def getPublicUsers(self):
- # Get public Users
- url = "%s/emby/Users/Public?format=json" % self.getServer()
- result = self.doUtils.downloadUrl(url, authenticate=False)
- if result != "":
- return result
else:
# Server connection failed
return False
+ @classmethod
+ def get_ssl(cls):
+ """
+ Returns boolean value or path to certificate
+ True: Verify ssl
+ False: Don't verify connection
+ """
+ certificate = settings('sslcert')
+ if certificate != "None":
+ return certificate
- def hasAccess(self):
- # hasAccess is verified in service.py
- result = self.doUtils.downloadUrl("{server}/emby/Users?format=json")
+ return True if settings('sslverify') == "true" else False
- if result == False:
- # Access is restricted, set in downloadutils.py via exception
- log.info("Access is restricted.")
- self.HasAccess = False
+ def get_access(self):
- elif window('emby_online') != "true":
- # Server connection failed
- pass
+ if not self._has_access:
+ self._set_access()
- elif window('emby_serverStatus') == "restricted":
- log.info("Access is granted.")
- self.HasAccess = True
- window('emby_serverStatus', clear=True)
- xbmcgui.Dialog().notification(lang(29999), lang(33007))
+ return self._has_access
- def loadCurrUser(self, authenticated=False):
+ def _set_access(self):
- doUtils = self.doUtils
- username = self.getUsername()
- userId = self.getUserId()
+ try:
+ self.download("{server}/emby/Users?format=json")
+ except Warning as error:
+ if self._has_access and "restricted" in error:
+ self._has_access = False
+ log.info("Access is restricted")
+ else:
+ if not self._has_access:
+ self._has_access = True
+ window('emby_serverStatus', clear=True)
+ log.info("Access is granted")
+ xbmcgui.Dialog().notification(lang(29999), lang(33007))
- # Only to be used if token exists
- self.currUserId = userId
- self.currServer = self.getServer()
- self.currToken = self.getToken()
- self.ssl = self.getSSLverify()
- self.sslcert = self.getSSL()
+ @classmethod
+ def get_userid(cls):
- # Test the validity of current token
- if authenticated == False:
- url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
- window('emby_currUser', value=userId)
- window('emby_accessToken%s' % userId, value=self.currToken)
- result = doUtils.downloadUrl(url)
+ ###$ Begin transition phase $###
+ if settings('userId') == "":
+ settings('userId', value=settings('userId%s' % settings('username')))
+ ###$ End transition phase $###
- if result == 401:
- # Token is no longer valid
- self.resetClient()
- return False
+ return settings('userId') or None
- # Set to windows property
- window('emby_currUser', value=userId)
- window('emby_accessToken%s' % userId, value=self.currToken)
- window('emby_server%s' % userId, value=self.currServer)
- window('emby_server_%s' % userId, value=self.getServer(prefix=False))
+ @classmethod
+ def get_token(cls):
- # Set DownloadUtils values
- doUtils.setUsername(username)
- doUtils.setUserId(self.currUserId)
- doUtils.setServer(self.currServer)
- doUtils.setToken(self.currToken)
- doUtils.setSSL(self.ssl, self.sslcert)
- # parental control - let's verify if access is restricted
- self.hasAccess()
- # Start DownloadUtils session
- doUtils.startSession()
- self.getAdditionalUsers()
- # Set user preferences in settings
- self.currUser = username
- self.setUserPref()
+ ###$ Begin transition phase $###
+ if settings('token') == "":
+ settings('token', value=settings('accessToken'))
+ ###$ End transition phase $###
+ return settings('token') or None
- def authenticate(self):
+ def _set_user_server(self):
- dialog = xbmcgui.Dialog()
+ self._server = self.download("{server}/emby/System/Configuration?format=json")
+ settings('markPlayed', value=str(self._server['MaxResumePct']))
- # Get /profile/addon_data
- addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
- hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
+ self._user = self.download("{server}/emby/Users/{UserId}?format=json")
+ if "PrimaryImageTag" in self._user:
+ window('EmbyUserImage',
+ value=artwork.Artwork().getUserArtwork(self._user['Id'], 'Primary'))
- username = self.getUsername()
- server = self.getServer()
+ def _authenticate(self):
- # If there's no settings.xml
- if not hasSettings:
- log.info("No settings.xml found.")
- self.auth = False
- return
- # If no user information
- elif not server or not username:
- log.info("Missing server information.")
- self.auth = False
- return
- # If there's a token, load the user
- elif self.getToken():
- result = self.loadCurrUser()
+ if not self.get_server() or not self.get_username():
+ log.info('missing server or user information')
+ self._auth = False
- if result == False:
- pass
+ elif self.get_token():
+ try:
+ self._load_user()
+ except Warning:
+ log.info("token is invalid")
else:
- log.info("Current user: %s" % self.currUser)
- log.info("Current userId: %s" % self.currUserId)
- log.debug("Current accessToken: %s" % self.currToken)
+ log.info("current user: %s", self.get_username())
+ log.info("current userid: %s", self.get_userid())
+ log.debug("current token: %s", self.get_token())
return
##### AUTHENTICATE USER #####
+ server = self.get_server()
+ username = self.get_username().decode('utf-8')
+ users = self.emby.getUsers(server)
+ user_found = None
- users = self.getPublicUsers()
- password = ""
-
- # Find user in list
for user in users:
- name = user['Name']
-
- if username.decode('utf-8') in name:
- # If user has password
- if user['HasPassword'] == True:
- password = dialog.input(
- heading="%s %s" % (lang(33008), username.decode('utf-8')),
- option=xbmcgui.ALPHANUM_HIDE_INPUT)
- # If password dialog is cancelled
- if not password:
- log.warn("No password entered.")
- window('emby_serverStatus', value="Stop")
- self.auth = False
- return
+ if username == user['Name']:
+ user_found = user
break
- else:
- # Manual login, user is hidden
- password = dialog.input(
- heading="%s %s" % (lang(33008), username.decode('utf-8')),
- option=xbmcgui.ALPHANUM_HIDE_INPUT)
- sha1 = hashlib.sha1(password)
- sha1 = sha1.hexdigest()
-
- # Authenticate username and password
- data = {'username': username, 'password': sha1}
- log.debug(data)
-
- url = "%s/emby/Users/AuthenticateByName?format=json" % server
- result = self.doUtils.downloadUrl(url, postBody=data, action_type="POST", authenticate=False)
-
try:
- log.info("Auth response: %s" % result)
- accessToken = result['AccessToken']
-
- except (KeyError, TypeError):
- log.info("Failed to retrieve the api key.")
- accessToken = None
-
- if accessToken is not None:
- self.currUser = username
- dialog.notification(lang(29999),
- "%s %s!" % (lang(33000), self.currUser.decode('utf-8')))
- settings('accessToken', value=accessToken)
- settings('userId%s' % username, value=result['User']['Id'])
- log.info("User Authenticated: %s" % accessToken)
- self.loadCurrUser(authenticated=True)
- window('emby_serverStatus', clear=True)
- self.retry = 0
+ user = self.connectmanager.login_manual(server, user_found)
+ except RuntimeError:
+ window('emby_serverStatus', value="stop")
+ self._auth = False
+ return
else:
- log.error("User authentication failed.")
- settings('accessToken', value="")
- settings('userId%s' % username, value="")
- dialog.ok(lang(33001), lang(33009))
+ log.info("user: %s", user)
+ settings('username', value=user['User']['Name'])
+ settings('token', value=user['AccessToken'])
+ settings('userId', value=user['User']['Id'])
+ xbmcgui.Dialog().notification(lang(29999),
+ "%s %s!" % (lang(33000), username))
+ self._load_user(authenticated=True)
+ window('emby_serverStatus', clear=True)
- # Give two attempts at entering password
- if self.retry == 2:
- log.info("Too many retries. "
- "You can retry by resetting attempts in the addon settings.")
- window('emby_serverStatus', value="Stop")
- dialog.ok(lang(33001), lang(33010))
+ def _load_user(self, authenticated=False):
- self.retry += 1
- self.auth = False
+ doutils = self.doutils
- def resetClient(self):
+ userid = self.get_userid()
+ server = self.get_server()
+ token = self.get_token()
- log.info("Reset UserClient authentication.")
- if self.currToken is not None:
- # In case of 401, removed saved token
- settings('accessToken', value="")
- window('emby_accessToken%s' % self.getUserId(), clear=True)
- self.currToken = None
- log.info("User token has been removed.")
+ # Set properties
+ window('emby_currUser', value=userid)
+ window('emby_server%s' % userid, value=server)
+ window('emby_accessToken%s' % userid, value=token)
- self.auth = True
- self.currUser = None
+ # Test the validity of the current token
+ if not authenticated:
+ try:
+ self.download("{server}/emby/Users/{UserId}?format=json")
+ except Warning as error:
+ if "401" in error:
+ # Token is not longer valid
+ raise
+
+ # Set downloadutils.py values
+ doutils.setUserId(userid)
+ doutils.setServer(server)
+ doutils.setToken(token)
+ doutils.setSSL(self.get_ssl())
+
+ # Start downloadutils.py session
+ doutils.startSession()
+
+ # Set _user and _server
+ # verify user access
+ try:
+ self._set_access()
+ self._set_user_server()
+ except Warning: # We don't need to raise any exceptions
+ pass
+
+ def _reset_client(self):
+
+ log.info("reset UserClient authentication")
+
+ settings('accessToken', value="")
+ window('emby_accessToken', clear=True)
+
+ log.info("user token revoked.")
+
+ self._user = None
+ self.auth = None
def run(self):
monitor = xbmc.Monitor()
+ self.connectmanager = connectmanager.ConnectManager()
+
log.warn("----===## Starting UserClient ##===----")
- while not monitor.abortRequested():
+ while not self._stop_thread:
status = window('emby_serverStatus')
if status:
# Verify the connection status to server
if status == "restricted":
# Parental control is restricting access
- self.HasAccess = False
+ self._has_access = False
elif status == "401":
# Unauthorized access, revoke token
- window('emby_serverStatus', value="Auth")
- self.resetClient()
+ window('emby_serverStatus', value="auth")
+ self._reset_client()
- if self.auth and (self.currUser is None):
+ if self._auth and self._user is None:
# Try to authenticate user
status = window('emby_serverStatus')
- if not status or status == "Auth":
+ if not status or status == "auth":
# Set auth flag because we no longer need
# to authenticate the user
- self.auth = False
- self.authenticate()
+ self._auth = False
+ self._authenticate()
-
- if not self.auth and (self.currUser is None):
+ if not self._auth and self._user is None:
# If authenticate failed.
- server = self.getServer()
- username = self.getUsername()
+ server = self.get_server()
+ username = self.get_username()
status = window('emby_serverStatus')
# The status Stop is for when user cancelled password dialog.
- if server and username and status != "Stop":
+ if server and username and status != "stop":
# Only if there's information found to login
- log.debug("Server found: %s" % server)
- log.debug("Username found: %s" % username)
- self.auth = True
-
-
- if self.stop_thread == True:
- # If stopping the client didn't work
- break
+ log.info("Server found: %s", server)
+ log.info("Username found: %s", username)
+ self._auth = True
if monitor.waitForAbort(1):
# Abort was requested while waiting. We should exit
break
- self.doUtils.stopSession()
+ self.doutils.stopSession()
log.warn("##===---- UserClient Stopped ----===##")
- def stopClient(self):
- # When emby for kodi terminates
- self.stop_thread = True
\ No newline at end of file
+ def stop_client(self):
+ self._stop_thread = True
diff --git a/resources/lib/utils.py b/resources/lib/utils.py
index 9d38e710..b1dfac81 100644
--- a/resources/lib/utils.py
+++ b/resources/lib/utils.py
@@ -408,12 +408,14 @@ def reset():
# Remove emby info
resp = dialog.yesno(language(29999), language(33087))
if resp:
+ import connectmanager
# Delete the settings
addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
dataPath = "%ssettings.xml" % addondir
xbmcvfs.delete(dataPath)
log.info("Deleting: settings.xml")
+ connectmanager.ConnectManager().clear_data()
dialog.ok(heading=language(29999), line1=language(33088))
xbmc.executebuiltin('RestartApp')
diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py
index bc158338..c79e81d0 100644
--- a/resources/lib/websocket_client.py
+++ b/resources/lib/websocket_client.py
@@ -251,7 +251,7 @@ class WebSocket_Client(threading.Thread):
elif messageType == "UserConfigurationUpdated":
# Update user data set in userclient
- userclient.UserClient().userSettings = data
+ userclient.UserClient().get_user(data)
self.librarySync.refresh_views = True
def on_close(self, ws):
diff --git a/resources/settings.xml b/resources/settings.xml
index ac6ddd0f..b29f8e8d 100644
--- a/resources/settings.xml
+++ b/resources/settings.xml
@@ -1,27 +1,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
-
-
+
diff --git a/resources/skins/default/1080i/script-emby-connect-login-manual.xml b/resources/skins/default/1080i/script-emby-connect-login-manual.xml
new file mode 100644
index 00000000..57e4a88a
--- /dev/null
+++ b/resources/skins/default/1080i/script-emby-connect-login-manual.xml
@@ -0,0 +1,145 @@
+
+
+ 200
+ 0
+ dialogeffect
+
+
+ Background fade
+ 100%
+ 100%
+ emby-bg-fade.png
+
+
+
+ 600
+ 35%
+ 20%
+
+ Background box
+ white.png
+ 600
+ 480
+
+
+
+ 485
+ False
+
+ Error box
+ white.png
+ 100%
+ 50
+
+
+
+ Error message
+ white
+ font10
+ center
+ center
+ 50
+
+
+
+
+ Emby logo
+ logo-white.png
+ keep
+ 120
+ 49
+ 30
+ 25
+
+
+
+ 500
+ 50
+
+ Please sign in
+
+ white
+ font12
+ top
+ center
+ 100%
+ 100
+
+
+
+ 150
+
+ Username
+
+ ffa6a6a6
+ font10
+ top
+
+
+
+ separator
+ 102%
+ 0.5
+ 66
+ -10
+ emby-separator.png
+
+
+
+
+ Password
+ 225
+
+ Password label
+
+ ffa6a6a6
+ font10
+ top
+
+
+
+ separator
+ 102%
+ 0.5
+ 66
+ -10
+ emby-separator.png
+
+
+
+
+ Buttons
+ 335
+
+ Sign in
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 100%
+ 50
+ 201
+
+
+
+ Cancel
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 100%
+ 50
+ 55
+ 200
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/skins/default/1080i/script-emby-connect-login.xml b/resources/skins/default/1080i/script-emby-connect-login.xml
index b5b01632..cb69387c 100644
--- a/resources/skins/default/1080i/script-emby-connect-login.xml
+++ b/resources/skins/default/1080i/script-emby-connect-login.xml
@@ -1,6 +1,6 @@
- 200
+ 200
0
dialogeffect
@@ -13,21 +13,41 @@
600
- 33%
+ 35%
15%
Background box
- box.png
+ white.png
600
700
+
+
+ 705
+ False
+
+ Error box
+ white.png
+ 100%
+ 50
+
+
+
+ Error message
+ white
+ font10
+ center
+ center
+ 50
+
+
Emby logo
logo-white.png
160
49
- 20
+ 30
25
@@ -47,7 +67,7 @@
190
Username email
-
+
ffa6a6a6
font10
top
@@ -59,7 +79,7 @@
0.5
66
-10
- separator.png
+ emby-separator.png
@@ -80,7 +100,7 @@
0.5
66
-10
- separator.png
+ emby-separator.png
@@ -89,10 +109,12 @@
385
Sign in
- box.png
- box.png
+ box.png
+ box.png
font10
+ ffa6a6a6
+ white
center
100%
50
@@ -100,11 +122,13 @@
- Later
+ Cancel
box.png
- box.png
+ box.png
font10
+ ffa6a6a6
+ white
center
100%
50
@@ -124,30 +148,32 @@
true
top
340
+ 100%
-
- qrcode
- qrcode_disclaimer.png
- 140
- 140
- 10
- 360
-
+
+
+ Scan me
+
+ font12
+ ff0b8628
+ top
+ 200
+ 120
+ 230
+
-
- Scan me
-
- font12
- green
- right
- top
- 120
- 160
+
+ qrcode
+ qrcode_disclaimer.png
+ 140
+ 140
+ 10
+ 360
+
-
\ No newline at end of file
diff --git a/resources/skins/default/1080i/script-emby-connect-server-manual.xml b/resources/skins/default/1080i/script-emby-connect-server-manual.xml
new file mode 100644
index 00000000..27e50037
--- /dev/null
+++ b/resources/skins/default/1080i/script-emby-connect-server-manual.xml
@@ -0,0 +1,154 @@
+
+
+ 200
+ 0
+ dialogeffect
+
+
+ Background fade
+ 100%
+ 100%
+ emby-bg-fade.png
+
+
+
+ 600
+ 35%
+ 20%
+
+ Background box
+ white.png
+ 600
+ 525
+
+
+
+ 530
+ False
+
+ Error box
+ white.png
+ 100%
+ 50
+
+
+
+ Error message
+ white
+ font10
+ center
+ center
+ 50
+
+
+
+
+ Emby logo
+ logo-white.png
+ keep
+ 120
+ 49
+ 30
+ 25
+
+
+
+ 500
+ 50
+
+ Connect to server
+
+ white
+ font12
+ top
+ center
+ 100%
+ 100
+
+
+
+ 150
+
+ Host
+
+ ffa6a6a6
+ font10
+ top
+
+
+
+ separator
+ 102%
+ 0.5
+ 66
+ -10
+ emby-separator.png
+
+
+
+ Host example
+
+ ff464646
+ font10
+ top
+ 70
+
+
+
+
+ Port
+ 275
+
+ Port label
+
+ ffa6a6a6
+ font10
+ top
+
+
+
+ separator
+ 102%
+ 0.5
+ 66
+ -10
+ emby-separator.png
+
+
+
+
+ Buttons
+ 380
+
+ Connect
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 100%
+ 50
+ 201
+
+
+
+ Cancel
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 100%
+ 50
+ 55
+ 200
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/skins/default/1080i/script-emby-connect-server.xml b/resources/skins/default/1080i/script-emby-connect-server.xml
new file mode 100644
index 00000000..01fc9141
--- /dev/null
+++ b/resources/skins/default/1080i/script-emby-connect-server.xml
@@ -0,0 +1,280 @@
+
+
+ 155
+ 0
+ dialogeffect
+
+
+ Background fade
+ 100%
+ 100%
+ emby-bg-fade.png
+
+
+
+ 450
+ 38%
+ 15%
+
+ Background box
+ white.png
+ 450
+ 710
+
+
+
+ Emby logo
+ logo-white.png
+ keep
+ 120
+ 49
+ 30
+ 25
+
+
+
+ User info
+ 70
+ 350
+ 50
+
+ User image
+ userflyoutdefault.png
+ keep
+ center
+ 100%
+ 70
+ 40
+
+
+
+ Busy animation
+ center
+ 23
+ 100%
+ 105
+ False
+ fading_circle.png
+ keep
+ conditional
+
+
+
+ Welcome user
+ white
+ font12
+ center
+ top
+ 120
+ 100%
+ 50
+
+
+
+ separator
+ 102%
+ 0.5
+ 165
+ -10
+ emby-separator.png
+
+
+
+ Select server
+ ffa6a6a6
+
+ font10
+ center
+ top
+ 170
+ 100%
+ 50
+
+
+
+
+ 290
+ 100%
+ 184
+
+ Connect servers
+ 0
+ 100%
+ 100%
+ 10
+ 55
+ 155
+ 205
+ 206
+ 205
+ 206
+ 155
+ 60
+ 250
+
+
+ 45
+ 45
+
+ Network
+ keep
+ network.png
+ StringCompare(ListItem.Property(server_type),network)
+
+
+ Wifi
+ keep
+ wifi.png
+ StringCompare(ListItem.Property(server_type),wifi)
+
+
+
+
+ 300
+ 40
+ 55
+ font10
+ center
+ ff838383
+ ListItem.Label
+
+
+
+
+ 45
+ 45
+
+ Network
+ keep
+ network.png
+ StringCompare(ListItem.Property(server_type),network)
+
+
+ Wifi
+ keep
+ wifi.png
+ StringCompare(ListItem.Property(server_type),wifi)
+
+
+
+
+ 300
+ 40
+ 55
+ font10
+ center
+ white
+ ListItem.Label
+ Control.HasFocus(155)
+
+
+ 300
+ 40
+ 55
+ font10
+ center
+ ff838383
+ ListItem.Label
+ !Control.HasFocus(155)
+
+
+
+
+
+ 395
+ 10
+ 5
+ 100%
+ 155
+ 60
+ 60
+ box.png
+ box.png
+ box.png
+ false
+
+
+
+ 100%
+ 220
+
+ 45
+ 150
+
+ True
+ Sign in Connect
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 350
+ 50
+ 50
+ 155
+ 206
+
+
+
+ Manually add server
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 55
+ 350
+ 50
+ 50
+ 205
+ 155
+ 201
+
+
+
+ Cancel
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 110
+ 350
+ 50
+ 50
+ 206
+
+
+
+
+ 100%
+ False
+
+ Message box
+ white.png
+ 100%
+ 50
+ 20
+
+
+
+ Message
+ white
+ font10
+ center
+ center
+ 50
+ 20
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/skins/default/1080i/script-emby-connect-users.xml b/resources/skins/default/1080i/script-emby-connect-users.xml
new file mode 100644
index 00000000..0acd2b32
--- /dev/null
+++ b/resources/skins/default/1080i/script-emby-connect-users.xml
@@ -0,0 +1,198 @@
+
+
+ 155
+ 0
+ dialogeffect
+
+
+ Background fade
+ 100%
+ 100%
+ emby-bg-fade.png
+
+
+
+ 715
+ 32%
+ 20%
+
+ Background box
+ white.png
+ 100%
+ 525
+
+
+
+ Emby logo
+ logo-white.png
+ keep
+ 120
+ 49
+ 30
+ 25
+
+
+
+ Please sign in
+
+ white
+ font12
+ top
+ center
+ 80
+ 100%
+
+
+
+ 100
+ 620
+ 245
+ 50
+
+ Select User
+ 0
+ 100%
+ 40
+ 155
+ 155
+ 200
+ 60
+ horizontal
+ 250
+
+
+ 150
+
+ User image
+ ff888888
+ ListItem.Icon
+ keep
+ 100%
+ 150
+
+
+
+ Background label
+ white.png
+ 100%
+ 50
+ 150
+
+
+
+ 100%
+ center
+ 50
+ 150
+ font10
+ white
+ ListItem.Label
+
+
+
+
+
+ 150
+
+ User image
+ ListItem.Icon
+ keep
+ 100%
+ 150
+ Control.HasFocus(155)
+
+
+ User image
+ ff888888
+ ListItem.Icon
+ keep
+ 100%
+ 150
+ !Control.HasFocus(155)
+
+
+
+ Background label
+ white.png
+ 100%
+ 50
+ 150
+ Control.HasFocus(155)
+
+
+ Background label
+ white.png
+ 100%
+ 50
+ 150
+ !Control.HasFocus(155)
+
+
+
+ 100%
+ center
+ 50
+ 150
+ font10
+ white
+ ListItem.Label
+
+
+
+
+
+
+ 100%
+ 615
+ 5
+ 155
+ 60
+ 60
+ box.png
+ box.png
+ box.png
+ false
+ horizontal
+
+
+
+ 615
+ 325
+ 100%
+
+
+ Manual Login button
+ box.png
+ box.png
+
+ center
+ 100%
+ 50
+ 35
+ font10
+ ffa6a6a6
+ white
+ 201
+ 155
+
+
+
+ Cancel
+ box.png
+ box.png
+
+ font10
+ ffa6a6a6
+ white
+ center
+ 100%
+ 50
+ 90
+ 200
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/skins/default/media/separator.png b/resources/skins/default/media/emby-separator.png
similarity index 100%
rename from resources/skins/default/media/separator.png
rename to resources/skins/default/media/emby-separator.png
diff --git a/resources/skins/default/media/fading_circle.png b/resources/skins/default/media/fading_circle.png
new file mode 100644
index 00000000..56aea399
Binary files /dev/null and b/resources/skins/default/media/fading_circle.png differ
diff --git a/resources/skins/default/media/network.png b/resources/skins/default/media/network.png
new file mode 100644
index 00000000..7cd11ddf
Binary files /dev/null and b/resources/skins/default/media/network.png differ
diff --git a/resources/skins/default/media/user_image.png b/resources/skins/default/media/user_image.png
new file mode 100644
index 00000000..194a78b1
Binary files /dev/null and b/resources/skins/default/media/user_image.png differ
diff --git a/resources/skins/default/media/userflyoutdefault.png b/resources/skins/default/media/userflyoutdefault.png
new file mode 100644
index 00000000..046676e5
Binary files /dev/null and b/resources/skins/default/media/userflyoutdefault.png differ
diff --git a/resources/skins/default/media/userflyoutdefault2.png b/resources/skins/default/media/userflyoutdefault2.png
new file mode 100644
index 00000000..24d771eb
Binary files /dev/null and b/resources/skins/default/media/userflyoutdefault2.png differ
diff --git a/resources/skins/default/media/wifi.png b/resources/skins/default/media/wifi.png
new file mode 100644
index 00000000..2a646c55
Binary files /dev/null and b/resources/skins/default/media/wifi.png differ
diff --git a/service.py b/service.py
index dbdfa414..0cd245c5 100644
--- a/service.py
+++ b/service.py
@@ -43,7 +43,7 @@ log = logging.getLogger("EMBY.service")
#################################################################################################
-class Service():
+class Service(object):
welcome_msg = True
server_online = True
@@ -59,7 +59,7 @@ class Service():
self.clientInfo = clientinfo.ClientInfo()
self.addonName = self.clientInfo.getAddonName()
- logLevel = userclient.UserClient().getLogLevel()
+ logLevel = settings('logLevel')
self.monitor = xbmc.Monitor()
window('emby_logLevel', value=str(logLevel))
@@ -128,7 +128,7 @@ class Service():
# Emby server is online
# Verify if user is set and has access to the server
- if (user.currUser is not None) and user.HasAccess:
+ if user.get_user() is not None and user.get_access():
# If an item is playing
if xbmc.Player().isPlaying():
@@ -166,7 +166,7 @@ class Service():
# Reset authentication warnings
self.welcome_msg = False
# Get additional users
- additionalUsers = user.AdditionalUser
+ additionalUsers = settings('additionalUsers')
if additionalUsers:
add = ", %s" % ", ".join(additionalUsers)
else:
@@ -174,7 +174,7 @@ class Service():
xbmcgui.Dialog().notification(
heading=lang(29999),
message=("%s %s%s!"
- % (lang(33000), user.currUser.decode('utf-8'),
+ % (lang(33000), user.get_username().decode('utf-8'),
add.decode('utf-8'))),
icon="special://home/addons/plugin.video.emby/icon.png",
time=2000,
@@ -194,7 +194,7 @@ class Service():
library.start()
else:
- if (user.currUser is None) and self.warn_auth:
+ if (user.get_user() is None) and self.warn_auth:
# Alert user is not authenticated and suppress future warning
self.warn_auth = False
log.info("Not authenticated yet.")
@@ -202,10 +202,8 @@ class Service():
# User access is restricted.
# Keep verifying until access is granted
# unless server goes offline or Kodi is shut down.
- while user.HasAccess == False:
+ while not user.get_access():
# Verify access with an API call
- user.hasAccess()
-
if window('emby_online') != "true":
# Server went offline
break
@@ -218,11 +216,11 @@ class Service():
# or Kodi is shut down.
while not monitor.abortRequested():
- if user.getServer() == False:
+ if user.get_server() is None:
# No server info set in add-on settings
pass
- elif user.getPublicUsers() == False:
+ elif not user.verify_server():
# Server is offline.
# Alert the user and suppress future warning
if self.server_online:
@@ -287,7 +285,7 @@ class Service():
##### Emby thread is terminating. #####
if self.userclient_running:
- user.stopClient()
+ user.stop_client()
if self.library_running:
library.stopThread()