Emby Connect (#58)

* Update with latest connect module

* Update string

* Change error behavior

* Add connectmanager

Handle dialogs for emby connect in one place

* Add user select dialog

* Add manual server dialog

* Add onAuthenticated

* Filter virtual episodes

* Update userclient with new methods
This commit is contained in:
angelblue05 2016-09-04 05:18:31 -05:00 committed by GitHub
parent 79c841bf32
commit 6a2ea9a4dd
36 changed files with 2959 additions and 862 deletions

View file

@ -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]:

View file

@ -3,13 +3,13 @@
<!-- Add-on settings -->
<string id="29999">Emby for Kodi</string>
<string id="30000">Primary Server Address</string><!-- Verified -->
<string id="30002">Play from HTTP instead of SMB</string><!-- Verified -->
<string id="30004">Log level</string><!-- Verified -->
<string id="30016">Device Name</string><!-- Verified -->
<string id="30000">Primary Server Address</string>
<string id="30002">Play from HTTP instead of SMB</string>
<string id="30004">Log level</string>
<string id="30016">Device Name</string>
<string id="30022">Advanced</string>
<string id="30024">Username</string><!-- Verified -->
<string id="30030">Port Number</string><!-- Verified -->
<string id="30024">Username</string>
<string id="30030">Port Number</string>
<string id="30035">Number of recent Music Albums to show:</string>
<string id="30036">Number of recent Movies to show:</string>
@ -232,12 +232,23 @@
<!-- dialogs -->
<string id="30600">Sign in with Emby Connect</string>
<string id="30601">Username or email:</string>
<string id="30602">Password:</string>
<string id="30602">Password</string>
<string id="30603">Please see our terms of use. The use of any Emby software constitutes acceptance of these terms.</string>
<string id="30604">Scan me</string>
<string id="30605">Sign in</string>
<string id="30606">Remind me later</string>
<string id="30606">Cancel</string>
<string id="30607">Select main server</string>
<string id="30608">Username or password cannot be empty</string>
<string id="30609">Unable to connect to the selected server</string>
<string id="30610">Connect to</string><!-- Connect to {server} -->
<string id="30611">Manually add server</string>
<string id="30612">Please sign in</string>
<string id="30613">Username cannot be empty</string>
<string id="30614">Connect to server</string>
<string id="30615">Host</string>
<string id="30616">Connect</string>
<string id="30617">Server or port cannot be empty</string>
<string id="30618">Change Emby Connect user</string>
<!-- service add-on -->
<string id="33000">Welcome</string>

View file

@ -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

View file

@ -0,0 +1 @@
# Dummy file to make this directory a package.

View file

@ -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 = ("<broadcast>", 7359)
MESSAGE = "who is EmbyServer?"
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1.0) # This controls the socket.timeout exception
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
log.debug("MultiGroup : %s" % str(MULTI_GROUP))
log.debug("Sending UDP Data: %s" % MESSAGE)
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("&", '&amp;')
password = password.replace("/", '&#092;')
password = password.replace("!", '&#33;')
password = password.replace("$", '&#036;')
password = password.replace("\"", '&quot;')
password = password.replace("<", '&lt;')
password = password.replace(">", '&gt;')
password = password.replace("'", '&#39;')
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)

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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):
def set_connect_manager(self, connect_manager):
self.connect_manager = connect_manager
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 is_logged_in(self):
return True if self._user else False
control.setPosition(x,y)
control.setHeight(height)
control.setWidth(width)
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
user = self.user_field.getText()
password = self.password_field.getText()
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 == REMIND_LATER:
elif control == CANCEL:
# Remind me later
self.close()
def onAction(self, action):
if action == ACTION_BACK:
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')

View file

@ -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')

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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,8 +205,6 @@ class DownloadUtils():
default_link = ""
try:
if authenticate:
if self.s is not None:
session = self.s
else:
@ -234,27 +223,13 @@ class DownloadUtils():
kwargs.update({
'verify': verifyssl,
'headers': self.getHeader()
'headers': self.getHeader(authenticate)
})
# 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
kwargs.update({
'verify': verifyssl,
'headers': self.getHeader(authenticate=False)
})
##### 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
window('emby_serverStatus', value="restricted")
if status != "restricted":
xbmcgui.Dialog().notification(
heading="Emby server",
heading=lang(29999),
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
return False
window('emby_serverStatus', value="restricted")
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

View file

@ -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():

View file

@ -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)
server = self.connectmanager.select_servers()
log.info("Server: %s", server)
except RuntimeError as error:
log.exception(error)
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id)
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_address = self.connectmanager.get_address(server)
self._set_server(server_address, server['Name'])
if prefix == "https":
settings('https', value="true")
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),
direct_paths = dialog.yesno(heading=lang(30511),
line1=lang(33035),
nolabel=lang(33036),
yeslabel=lang(33037))
if directPaths:
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):
@classmethod
def _set_server(cls, server, name):
log.info("Getting Server Details from Network")
settings('serverName', value=name)
settings('server', value=server)
log.info("Saved server information: %s", server)
MULTI_GROUP = ("<broadcast>", 7359)
MESSAGE = "who is EmbyServer?"
@classmethod
def _set_user(cls, user_id, token):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(6.0)
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']
settings('userId', value=user_id)
settings('token', value=token)

View file

@ -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

View file

@ -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)

View file

@ -3,6 +3,7 @@
#################################################################################################
import logging
import hashlib
import xbmc
@ -572,3 +573,19 @@ class Read_EmbyServer():
url = "{server}/emby/Items/%s?format=json" % itemid
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

View file

@ -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
return self._has_access
def _set_access(self):
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))
def loadCurrUser(self, authenticated=False):
@classmethod
def get_userid(cls):
doUtils = self.doUtils
username = self.getUsername()
userId = self.getUserId()
###$ Begin transition phase $###
if settings('userId') == "":
settings('userId', value=settings('userId%s' % settings('username')))
###$ End transition phase $###
# 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()
return settings('userId') or None
# 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)
@classmethod
def get_token(cls):
if result == 401:
# Token is no longer valid
self.resetClient()
return False
###$ Begin transition phase $###
if settings('token') == "":
settings('token', value=settings('accessToken'))
###$ End transition phase $###
# 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))
return settings('token') or None
# 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()
def _set_user_server(self):
self._server = self.download("{server}/emby/System/Configuration?format=json")
settings('markPlayed', value=str(self._server['MaxResumePct']))
def authenticate(self):
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'))
dialog = xbmcgui.Dialog()
def _authenticate(self):
# Get /profile/addon_data
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
if not self.get_server() or not self.get_username():
log.info('missing server or user information')
self._auth = False
username = self.getUsername()
server = self.getServer()
# 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 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.")
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)
def _load_user(self, authenticated=False):
doutils = self.doutils
userid = self.get_userid()
server = self.get_server()
token = self.get_token()
# Set properties
window('emby_currUser', value=userid)
window('emby_server%s' % userid, value=server)
window('emby_accessToken%s' % userid, value=token)
# 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="")
settings('userId%s' % username, value="")
dialog.ok(lang(33001), lang(33009))
window('emby_accessToken', 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))
log.info("user token revoked.")
self.retry += 1
self.auth = False
def resetClient(self):
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.")
self.auth = True
self.currUser = None
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
def stop_client(self):
self._stop_thread = True

View file

@ -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')

View file

@ -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):

View file

@ -1,27 +1,24 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
<category label="30014"><!-- Emby -->
<!-- Primary address -->
<setting id="ipaddress" label="30000" type="text" default="" />
<setting id="port" label="30030" type="number" default="8096" />
<setting id="https" label="30243" type="bool" default="false" />
<setting id="sslverify" subsetting="true" label="30500" type="bool" default="false" visible="eq(-1,true)" />
<setting id="sslcert" subsetting="true" label="30501" type="file" default="None" visible="eq(-2,true)" />
<!-- Secondary address -->
<setting id="altip" label="30502" type="bool" default="false" />
<setting id="secondipaddress" subsetting="true" label="30503" type="text" default="" visible="eq(-1,true)" />
<setting id="secondport" subsetting="true" label="30030" type="number" default="8096" visible="eq(-2,true)" />
<setting id="secondhttps" subsetting="true" label="30243" type="bool" default="false" visible="eq(-3,true)" />
<setting id="secondsslverify" subsetting="true" label="30500" type="bool" default="false" visible="eq(-1,true)" />
<setting id="secondsslcert" subsetting="true" label="30501" type="file" default="None" visible="eq(-2,true)" />
<setting id="idMethod" label="Login method" type="enum" values="Manual|Emby Connect" default="0" />
<!-- Manual address -->
<setting id="username" label="30024" type="text" default="" visible="eq(-1,0)" />
<setting id="serverName" label="30000" type="text" default="" />
<setting id="server" type="text" default="" visible="false" />
<setting id="sslverify" label="30500" type="bool" default="false" visible="eq(-1,true)" subsetting="true" />
<setting id="sslcert" label="30501" type="file" default="None" visible="eq(-2,true)" subsetting="true" />
<!-- Emby Connect -->
<setting id="connectUsername" label="30543" type="text" default="" visible="!eq(0,) + eq(-7,1)" />
<setting label="30600" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=connect)" visible="eq(-7,1) + eq(-1,)" option="close" />
<setting label="30618" type="action" action="RunPlugin(plugin://plugin.video.emby?mode=connect)" visible="eq(-8,1) + !eq(-2,)" option="close" />
<!-- User settings -->
<setting id="username" label="30024" type="text" default="" />
<setting id="accessToken" type="text" default="" visible="false" />
<setting id="userId" type="text" default="" visible="false" />
<!-- Device settings -->
<setting type="sep" />
<setting id="deviceNameOpt" label="30504" type="bool" default="false" />
<setting id="deviceName" label="30016" type="text" visible="eq(-1,true)" default="Kodi" />
<setting label="30505" type="action" visible="eq(1,) + !eq(-15,)" action="RunPlugin(plugin://plugin.video.emby?mode=resetauth)" option="close" />
<setting id="accessToken" type="text" visible="false" default="" />
<setting id="pathsub" type="bool" visible="false" default="false" />
<setting id="deviceName" label="30016" type="text" default="Kodi" visible="eq(-1,true)" />
</category>
<category label="30506"><!-- Sync Options -->

View file

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<width>600</width>
<left>35%</left>
<top>20%</top>
<control type="image">
<description>Background box</description>
<texture colordiffuse="ff111111">white.png</texture>
<width>600</width>
<height>480</height>
</control>
<control type="group" id="202">
<top>485</top>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
</control>
<control type="label" id="203">
<description>Error message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Please sign in</description>
<label>$ADDON[plugin.video.emby 30612]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<width>100%</width>
<top>100</top>
</control>
<control type="group">
<top>150</top>
<control type="label">
<description>Username</description>
<label>$ADDON[plugin.video.emby 30024]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Password</description>
<top>225</top>
<control type="label">
<description>Password label</description>
<label>$ADDON[plugin.video.emby 30602]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>335</top>
<control type="button" id="200">
<description>Sign in</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>55</top>
<onup>200</onup>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="false">200</defaultcontrol>
<defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
@ -13,21 +13,41 @@
<control type="group">
<width>600</width>
<left>33%</left>
<left>35%</left>
<top>15%</top>
<control type="image">
<description>Background box</description>
<texture border="6" colordiffuse="ff111111">box.png</texture>
<texture colordiffuse="ff111111">white.png</texture>
<width>600</width>
<height>700</height>
</control>
<control type="group" id="202">
<top>705</top>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
</control>
<control type="label" id="203">
<description>Error message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<width>160</width>
<height>49</height>
<top>20</top>
<top>30</top>
<left>25</left>
</control>
@ -47,7 +67,7 @@
<top>190</top>
<control type="label">
<description>Username email</description>
<label>$ADDON[plugin.video.emby 30601]</label>
<label>$ADDON[plugin.video.emby 30543]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
@ -59,7 +79,7 @@
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff222222" border="90,3,90,3">separator.png</texture>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
@ -80,7 +100,7 @@
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff222222" border="90,3,90,3">separator.png</texture>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
@ -89,10 +109,12 @@
<top>385</top>
<control type="button" id="200">
<description>Sign in</description>
<texturenofocus border="5" colordiffuse="green">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="green">box.png</texturefocus>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30605][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
@ -100,11 +122,13 @@
</control>
<control type="button" id="201">
<description>Later</description>
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff464646">box.png</texturefocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
@ -124,6 +148,19 @@
<wrapmultiline>true</wrapmultiline>
<aligny>top</aligny>
<width>340</width>
<height>100%</height>
</control>
<control type="group">
<control type="label">
<description>Scan me</description>
<label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label>
<font>font12</font>
<textcolor>ff0b8628</textcolor>
<aligny>top</aligny>
<width>200</width>
<top>120</top>
<left>230</left>
</control>
<control type="image">
@ -134,20 +171,9 @@
<top>10</top>
<left>360</left>
</control>
<control type="label">
<description>Scan me</description>
<label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label>
<font>font12</font>
<textcolor>green</textcolor>
<align>right</align>
<aligny>top</aligny>
<top>120</top>
<right>160</right>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">200</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<width>600</width>
<left>35%</left>
<top>20%</top>
<control type="image">
<description>Background box</description>
<texture colordiffuse="ff111111">white.png</texture>
<width>600</width>
<height>525</height>
</control>
<control type="group" id="202">
<top>530</top>
<visible>False</visible>
<control type="image">
<description>Error box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
</control>
<control type="label" id="203">
<description>Error message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
</control>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Connect to server</description>
<label>$ADDON[plugin.video.emby 30614]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<width>100%</width>
<top>100</top>
</control>
<control type="group">
<top>150</top>
<control type="label">
<description>Host</description>
<label>$ADDON[plugin.video.emby 30615]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
<control type="label">
<description>Host example</description>
<label>192.168.1.100 or https://myserver.com</label>
<textcolor>ff464646</textcolor>
<font>font10</font>
<aligny>top</aligny>
<top>70</top>
</control>
</control>
<control type="group">
<description>Port</description>
<top>275</top>
<control type="label">
<description>Port label</description>
<label>$ADDON[plugin.video.emby 30030]</label>
<textcolor>ffa6a6a6</textcolor>
<font>font10</font>
<aligny>top</aligny>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>66</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
</control>
<control type="group">
<description>Buttons</description>
<top>380</top>
<control type="button" id="200">
<description>Connect</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30616][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>55</top>
<onup>200</onup>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

@ -0,0 +1,280 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">155</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<width>450</width>
<left>38%</left>
<top>15%</top>
<control type="image">
<description>Background box</description>
<texture colordiffuse="ff111111">white.png</texture>
<width>450</width>
<height>710</height>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="group">
<description>User info</description>
<top>70</top>
<width>350</width>
<left>50</left>
<control type="image" id="150">
<description>User image</description>
<texture diffuse="user_image.png">userflyoutdefault.png</texture>
<aspectratio>keep</aspectratio>
<align>center</align>
<width>100%</width>
<height>70</height>
<top>40</top>
</control>
<control type="image" id="204">
<description>Busy animation</description>
<align>center</align>
<top>23</top>
<width>100%</width>
<height>105</height>
<visible>False</visible>
<texture colordiffuse="ff13a134">fading_circle.png</texture>
<aspectratio>keep</aspectratio>
<animation effect="rotate" start="360" end="0" center="auto" time="2000" loop="true" condition="true">conditional</animation>
</control>
<control type="label" id="151">
<description>Welcome user</description>
<textcolor>white</textcolor>
<font>font12</font>
<align>center</align>
<aligny>top</aligny>
<top>120</top>
<width>100%</width>
<height>50</height>
</control>
<control type="image">
<description>separator</description>
<width>102%</width>
<height>0.5</height>
<top>165</top>
<left>-10</left>
<texture colordiffuse="ff525252" border="90,3,90,3">emby-separator.png</texture>
</control>
<control type="label">
<description>Select server</description>
<textcolor>ffa6a6a6</textcolor>
<label>$ADDON[plugin.video.emby 30607]</label>
<font>font10</font>
<align>center</align>
<aligny>top</aligny>
<top>170</top>
<width>100%</width>
<height>50</height>
</control>
</control>
<control type="group">
<top>290</top>
<width>100%</width>
<height>184</height>
<control type="list" id="155">
<description>Connect servers</description>
<focusposition>0</focusposition>
<width>100%</width>
<height>100%</height>
<top>10</top>
<left>55</left>
<onup>155</onup>
<ondown condition="Control.IsVisible(205)">205</ondown>
<ondown condition="!Control.IsVisible(205)">206</ondown>
<onleft condition="Control.IsVisible(205)">205</onleft>
<onleft condition="!Control.IsVisible(205)">206</onleft>
<onright>155</onright>
<pagecontrol>60</pagecontrol>
<scrolltime tween="sine" easing="out">250</scrolltime>
<itemlayout height="46">
<control type="group">
<width>45</width>
<height>45</height>
<control type="image">
<description>Network</description>
<aspectratio>keep</aspectratio>
<texture>network.png</texture>
<visible>StringCompare(ListItem.Property(server_type),network)</visible>
</control>
<control type="image">
<description>Wifi</description>
<aspectratio>keep</aspectratio>
<texture>wifi.png</texture>
<visible>StringCompare(ListItem.Property(server_type),wifi)</visible>
</control>
</control>
<control type="label">
<width>300</width>
<height>40</height>
<left>55</left>
<font>font10</font>
<aligny>center</aligny>
<textcolor>ff838383</textcolor>
<info>ListItem.Label</info>
</control>
</itemlayout>
<focusedlayout height="46">
<control type="group">
<width>45</width>
<height>45</height>
<control type="image">
<description>Network</description>
<aspectratio>keep</aspectratio>
<texture>network.png</texture>
<visible>StringCompare(ListItem.Property(server_type),network)</visible>
</control>
<control type="image">
<description>Wifi</description>
<aspectratio>keep</aspectratio>
<texture>wifi.png</texture>
<visible>StringCompare(ListItem.Property(server_type),wifi)</visible>
</control>
</control>
<control type="label">
<width>300</width>
<height>40</height>
<left>55</left>
<font>font10</font>
<aligny>center</aligny>
<textcolor>white</textcolor>
<info>ListItem.Label</info>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="label">
<width>300</width>
<height>40</height>
<left>55</left>
<font>font10</font>
<aligny>center</aligny>
<textcolor>ff838383</textcolor>
<info>ListItem.Label</info>
<visible>!Control.HasFocus(155)</visible>
</control>
</focusedlayout>
</control>
<control type="scrollbar" id="60">
<left>395</left>
<top>10</top>
<width>5</width>
<height>100%</height>
<onleft>155</onleft>
<onup>60</onup>
<ondown>60</ondown>
<texturesliderbackground colordiffuse="ff000000" border="4">box.png</texturesliderbackground>
<texturesliderbar colordiffuse="ff222222" border="4">box.png</texturesliderbar>
<texturesliderbarfocus colordiffuse="ff222222" border="4">box.png</texturesliderbarfocus>
<showonepage>false</showonepage>
</control>
<control type="group">
<top>100%</top>
<height>220</height>
<control type="group">
<top>45</top>
<height>150</height>
<control type="button" id="205">
<visible>True</visible>
<description>Sign in Connect</description>
<texturenofocus border="5" colordiffuse="ff0b8628">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff13a134">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30600][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>350</width>
<height>50</height>
<left>50</left>
<onup>155</onup>
<ondown>206</ondown>
</control>
<control type="button" id="206">
<description>Manually add server</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30611][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<top>55</top>
<width>350</width>
<height>50</height>
<left>50</left>
<onup condition="Control.IsVisible(205)">205</onup>
<onup condition="!Control.IsVisible(205)">155</onup>
<ondown>201</ondown>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff525252">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<top>110</top>
<width>350</width>
<height>50</height>
<left>50</left>
<onup>206</onup>
</control>
</control>
<control type="group" id="202">
<top>100%</top>
<visible>False</visible>
<control type="image">
<description>Message box</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
<top>20</top>
</control>
<control type="label" id="203">
<description>Message</description>
<textcolor>white</textcolor>
<font>font10</font>
<aligny>center</aligny>
<align>center</align>
<height>50</height>
<top>20</top>
</control>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

@ -0,0 +1,198 @@
<?xml version="1.0" encoding="UTF-8"?>
<window>
<defaultcontrol always="true">155</defaultcontrol>
<zorder>0</zorder>
<include>dialogeffect</include>
<controls>
<control type="image">
<description>Background fade</description>
<width>100%</width>
<height>100%</height>
<texture>emby-bg-fade.png</texture>
</control>
<control type="group">
<width>715</width>
<left>32%</left>
<top>20%</top>
<control type="image">
<description>Background box</description>
<texture border="6" colordiffuse="ff111111">white.png</texture>
<width>100%</width>
<height>525</height>
</control>
<control type="image">
<description>Emby logo</description>
<texture>logo-white.png</texture>
<aspectratio>keep</aspectratio>
<width>120</width>
<height>49</height>
<top>30</top>
<left>25</left>
</control>
<control type="label">
<description>Please sign in</description>
<label>$ADDON[plugin.video.emby 30612]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<align>center</align>
<top>80</top>
<width>100%</width>
</control>
<control type="group">
<top>100</top>
<width>620</width>
<height>245</height>
<left>50</left>
<control type="list" id="155">
<description>Select User</description>
<focusposition>0</focusposition>
<width>100%</width>
<top>40</top>
<onleft>155</onleft>
<onright>155</onright>
<ondown>200</ondown>
<pagecontrol>60</pagecontrol>
<orientation>horizontal</orientation>
<scrolltime tween="sine" easing="out">250</scrolltime>
<itemlayout width="155">
<control type="group">
<width>150</width>
<control type="image">
<description>User image</description>
<colordiffuse>ff888888</colordiffuse>
<info>ListItem.Icon</info>
<aspectratio>keep</aspectratio>
<width>100%</width>
<height>150</height>
</control>
<control type="image">
<description>Background label</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
<top>150</top>
</control>
<control type="label">
<width>100%</width>
<align>center</align>
<height>50</height>
<top>150</top>
<font>font10</font>
<textcolor>white</textcolor>
<info>ListItem.Label</info>
</control>
</control>
</itemlayout>
<focusedlayout width="155">
<control type="group">
<width>150</width>
<control type="image">
<description>User image</description>
<info>ListItem.Icon</info>
<aspectratio>keep</aspectratio>
<width>100%</width>
<height>150</height>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="image">
<description>User image</description>
<colordiffuse>ff888888</colordiffuse>
<info>ListItem.Icon</info>
<aspectratio>keep</aspectratio>
<width>100%</width>
<height>150</height>
<visible>!Control.HasFocus(155)</visible>
</control>
<control type="image">
<description>Background label</description>
<texture colordiffuse="ff333333">white.png</texture>
<width>100%</width>
<height>50</height>
<top>150</top>
<visible>Control.HasFocus(155)</visible>
</control>
<control type="image">
<description>Background label</description>
<texture colordiffuse="ff222222">white.png</texture>
<width>100%</width>
<height>50</height>
<top>150</top>
<visible>!Control.HasFocus(155)</visible>
</control>
<control type="label">
<width>100%</width>
<align>center</align>
<height>50</height>
<top>150</top>
<font>font10</font>
<textcolor>white</textcolor>
<info>ListItem.Label</info>
</control>
</control>
</focusedlayout>
</control>
<control type="scrollbar" id="60">
<top>100%</top>
<width>615</width>
<height>5</height>
<onleft>155</onleft>
<onleft>60</onleft>
<onright>60</onright>
<texturesliderbackground colordiffuse="ff000000" border="4">box.png</texturesliderbackground>
<texturesliderbar colordiffuse="ff222222" border="4">box.png</texturesliderbar>
<texturesliderbarfocus colordiffuse="ff222222" border="4">box.png</texturesliderbarfocus>
<showonepage>false</showonepage>
<orientation>horizontal</orientation>
</control>
<control type="group">
<width>615</width>
<height>325</height>
<top>100%</top>
<control type="group">
<control type="button" id="200">
<description>Manual Login button</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff585858">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30540][/B][/UPPERCASE]</label>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>35</top>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<ondown>201</ondown>
<onup>155</onup>
</control>
<control type="button" id="201">
<description>Cancel</description>
<texturenofocus border="5" colordiffuse="ff464646">box.png</texturenofocus>
<texturefocus border="5" colordiffuse="ff585858">box.png</texturefocus>
<label>[UPPERCASE][B]$ADDON[plugin.video.emby 30606][/B][/UPPERCASE]</label>
<font>font10</font>
<textcolor>ffa6a6a6</textcolor>
<focusedcolor>white</focusedcolor>
<align>center</align>
<width>100%</width>
<height>50</height>
<top>90</top>
<onup>200</onup>
</control>
</control>
</control>
</control>
</control>
</controls>
</window>

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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()