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
|
@ -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]:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
1
resources/lib/connect/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Dummy file to make this directory a package.
|
802
resources/lib/connect/connectionmanager.py
Normal 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("&", '&')
|
||||
password = password.replace("/", '\')
|
||||
password = password.replace("!", '!')
|
||||
password = password.replace("$", '$')
|
||||
password = password.replace("\"", '"')
|
||||
password = password.replace("<", '<')
|
||||
password = password.replace(">", '>')
|
||||
password = password.replace("'", ''')
|
||||
|
||||
return password
|
||||
|
||||
def clearData(self):
|
||||
|
||||
log.info("connection manager clearing data")
|
||||
|
||||
self.connectUser = None
|
||||
credentials = self.credentialProvider.getCredentials()
|
||||
credentials['ConnectAccessToken'] = None
|
||||
credentials['ConnectUserId'] = None
|
||||
credentials['Servers'] = []
|
||||
self.credentialProvider.getCredentials(credentials)
|
146
resources/lib/connect/credentials.py
Normal 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
|
175
resources/lib/connectmanager.py
Normal 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")
|
|
@ -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
|
||||
|
|
|
@ -2,52 +2,63 @@
|
|||
|
||||
##################################################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
from utils import language as lang
|
||||
|
||||
##################################################################################################
|
||||
|
||||
log = logging.getLogger("EMBY."+__name__)
|
||||
addon = xbmcaddon.Addon('plugin.video.emby')
|
||||
|
||||
ACTION_PARENT_DIR = 9
|
||||
ACTION_PREVIOUS_MENU = 10
|
||||
ACTION_BACK = 92
|
||||
SIGN_IN = 200
|
||||
REMIND_LATER = 201
|
||||
CANCEL = 201
|
||||
ERROR_TOGGLE = 202
|
||||
ERROR_MSG = 203
|
||||
ERROR = {
|
||||
'Invalid': 1,
|
||||
'Empty': 2
|
||||
}
|
||||
|
||||
##################################################################################################
|
||||
|
||||
|
||||
class LoginConnect(xbmcgui.WindowXMLDialog):
|
||||
|
||||
_user = None
|
||||
error = None
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
|
||||
def __add_editcontrol(self, x, y, height, width, password=0):
|
||||
|
||||
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
|
||||
control = xbmcgui.ControlEdit(0,0,0,0,
|
||||
label="User",
|
||||
font="font10",
|
||||
textColor="ff464646",
|
||||
focusTexture=os.path.join(media, "button-focus.png"),
|
||||
noFocusTexture=os.path.join(media, "button-focus.png"),
|
||||
isPassword=password)
|
||||
def set_connect_manager(self, connect_manager):
|
||||
self.connect_manager = connect_manager
|
||||
|
||||
control.setPosition(x,y)
|
||||
control.setHeight(height)
|
||||
control.setWidth(width)
|
||||
def is_logged_in(self):
|
||||
return True if self._user else False
|
||||
|
||||
def get_user(self):
|
||||
return self._user
|
||||
|
||||
self.addControl(control)
|
||||
return control
|
||||
|
||||
def onInit(self):
|
||||
|
||||
self.user_field = self.__add_editcontrol(685,385,40,500)
|
||||
|
||||
self.user_field = self._add_editcontrol(725, 385, 40, 500)
|
||||
self.setFocus(self.user_field)
|
||||
self.password_field = self.__add_editcontrol(685,470,40,500, password=1)
|
||||
self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1)
|
||||
self.signin_button = self.getControl(SIGN_IN)
|
||||
self.remind_button = self.getControl(REMIND_LATER)
|
||||
self.remind_button = self.getControl(CANCEL)
|
||||
self.error_toggle = self.getControl(ERROR_TOGGLE)
|
||||
self.error_msg = self.getControl(ERROR_MSG)
|
||||
|
||||
self.user_field.controlUp(self.remind_button)
|
||||
self.user_field.controlDown(self.password_field)
|
||||
|
@ -60,17 +71,66 @@ class LoginConnect(xbmcgui.WindowXMLDialog):
|
|||
|
||||
if control == SIGN_IN:
|
||||
# Sign in to emby connect
|
||||
self.user = self.user_field.getText()
|
||||
__password = self.password_field.getText()
|
||||
self._disable_error()
|
||||
|
||||
### REVIEW ONCE CONNECT MODULE IS MADE
|
||||
self.close()
|
||||
user = self.user_field.getText()
|
||||
password = self.password_field.getText()
|
||||
|
||||
elif control == REMIND_LATER:
|
||||
if not user or not password:
|
||||
# Display error
|
||||
self._error(ERROR['Empty'], lang(30608))
|
||||
log.error("Username or password cannot be null")
|
||||
|
||||
elif self._login(user, password):
|
||||
self.close()
|
||||
|
||||
elif control == CANCEL:
|
||||
# Remind me later
|
||||
self.close()
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if action == ACTION_BACK:
|
||||
self.close()
|
||||
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')
|
||||
|
|
144
resources/lib/dialog/loginmanual.py
Normal 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')
|
144
resources/lib/dialog/serverconnect.py
Normal 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
|
145
resources/lib/dialog/servermanual.py
Normal 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')
|
100
resources/lib/dialog/usersconnect.py
Normal 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)
|
|
@ -10,7 +10,7 @@ import xbmc
|
|||
import xbmcgui
|
||||
|
||||
import clientinfo
|
||||
from utils import window, settings
|
||||
from utils import window, settings, language as lang
|
||||
|
||||
##################################################################################################
|
||||
|
||||
|
@ -40,11 +40,6 @@ class DownloadUtils():
|
|||
self.__dict__ = self._shared_state
|
||||
|
||||
|
||||
def setUsername(self, username):
|
||||
# Reserved for userclient only
|
||||
self.username = username
|
||||
log.debug("Set username: %s" % username)
|
||||
|
||||
def setUserId(self, userId):
|
||||
# Reserved for userclient only
|
||||
self.userId = userId
|
||||
|
@ -60,12 +55,10 @@ class DownloadUtils():
|
|||
self.token = token
|
||||
log.debug("Set token: %s" % token)
|
||||
|
||||
def setSSL(self, ssl, sslclient):
|
||||
def setSSL(self, ssl):
|
||||
# Reserved for userclient only
|
||||
self.sslverify = ssl
|
||||
self.sslclient = sslclient
|
||||
log.debug("Verify SSL host certificate: %s" % ssl)
|
||||
log.debug("SSL client side certificate: %s" % sslclient)
|
||||
log.debug("Verify SSL verify/certificate: %s" % ssl)
|
||||
|
||||
|
||||
def postCapabilities(self, deviceId):
|
||||
|
@ -149,8 +142,6 @@ class DownloadUtils():
|
|||
# If user enabled host certificate verification
|
||||
try:
|
||||
verify = self.sslverify
|
||||
if self.sslclient is not None:
|
||||
verify = self.sslclient
|
||||
except:
|
||||
log.info("Could not load SSL settings.")
|
||||
|
||||
|
@ -214,47 +205,31 @@ class DownloadUtils():
|
|||
default_link = ""
|
||||
|
||||
try:
|
||||
if authenticate:
|
||||
if self.s is not None:
|
||||
session = self.s
|
||||
else:
|
||||
# request session does not exists
|
||||
# Get user information
|
||||
self.userId = window('emby_currUser')
|
||||
self.server = window('emby_server%s' % self.userId)
|
||||
self.token = window('emby_accessToken%s' % self.userId)
|
||||
verifyssl = False
|
||||
|
||||
if self.s is not None:
|
||||
session = self.s
|
||||
else:
|
||||
# request session does not exists
|
||||
# Get user information
|
||||
self.userId = window('emby_currUser')
|
||||
self.server = window('emby_server%s' % self.userId)
|
||||
self.token = window('emby_accessToken%s' % self.userId)
|
||||
verifyssl = False
|
||||
|
||||
# IF user enables ssl verification
|
||||
if settings('sslverify') == "true":
|
||||
verifyssl = True
|
||||
if settings('sslcert') != "None":
|
||||
verifyssl = settings('sslcert')
|
||||
|
||||
kwargs.update({
|
||||
'verify': verifyssl,
|
||||
'headers': self.getHeader()
|
||||
})
|
||||
|
||||
# Replace for the real values
|
||||
url = url.replace("{server}", self.server)
|
||||
url = url.replace("{UserId}", self.userId)
|
||||
|
||||
else: # User is not authenticated
|
||||
# If user enables ssl verification
|
||||
try:
|
||||
verifyssl = self.sslverify
|
||||
if self.sslclient is not None:
|
||||
verifyssl = self.sslclient
|
||||
except AttributeError:
|
||||
verifyssl = False
|
||||
# IF user enables ssl verification
|
||||
if settings('sslverify') == "true":
|
||||
verifyssl = True
|
||||
if settings('sslcert') != "None":
|
||||
verifyssl = settings('sslcert')
|
||||
|
||||
kwargs.update({
|
||||
'verify': verifyssl,
|
||||
'headers': self.getHeader(authenticate=False)
|
||||
'headers': self.getHeader(authenticate)
|
||||
})
|
||||
|
||||
# Replace for the real values
|
||||
url = url.replace("{server}", self.server)
|
||||
url = url.replace("{UserId}", self.userId)
|
||||
|
||||
##### PREPARE REQUEST #####
|
||||
kwargs.update({
|
||||
'url': url,
|
||||
|
@ -310,13 +285,15 @@ class DownloadUtils():
|
|||
# Emby server errors
|
||||
if r.headers['X-Application-Error-Code'] == "ParentalControl":
|
||||
# Parental control - access restricted
|
||||
if status != "restricted":
|
||||
xbmcgui.Dialog().notification(
|
||||
heading=lang(29999),
|
||||
message="Access restricted.",
|
||||
icon=xbmcgui.NOTIFICATION_ERROR,
|
||||
time=5000)
|
||||
|
||||
window('emby_serverStatus', value="restricted")
|
||||
xbmcgui.Dialog().notification(
|
||||
heading="Emby server",
|
||||
message="Access restricted.",
|
||||
icon=xbmcgui.NOTIFICATION_ERROR,
|
||||
time=5000)
|
||||
return False
|
||||
raise Warning('restricted')
|
||||
|
||||
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
|
||||
# User tried to do something his emby account doesn't allow
|
||||
|
@ -330,7 +307,7 @@ class DownloadUtils():
|
|||
heading="Error connecting",
|
||||
message="Unauthorized.",
|
||||
icon=xbmcgui.NOTIFICATION_ERROR)
|
||||
return 401
|
||||
raise Warning('401')
|
||||
|
||||
elif r.status_code in (301, 302):
|
||||
# Redirects
|
||||
|
|
|
@ -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():
|
||||
|
||||
|
|
|
@ -2,174 +2,125 @@
|
|||
|
||||
#################################################################################################
|
||||
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
import clientinfo
|
||||
import downloadutils
|
||||
import connectmanager
|
||||
import connect.connectionmanager as connectionmanager
|
||||
import userclient
|
||||
from utils import settings, language as lang, passwordsXML
|
||||
|
||||
#################################################################################################
|
||||
|
||||
log = logging.getLogger("EMBY."+__name__)
|
||||
STATE = connectionmanager.ConnectionState
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class InitialSetup():
|
||||
class InitialSetup(object):
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.addonId = clientinfo.ClientInfo().getAddonId()
|
||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
||||
self.userClient = userclient.UserClient()
|
||||
self.addon_id = clientinfo.ClientInfo().getAddonId()
|
||||
self.user_client = userclient.UserClient()
|
||||
self.connectmanager = connectmanager.ConnectManager()
|
||||
|
||||
|
||||
def setup(self):
|
||||
# Check server, user, direct paths, music, direct stream if not direct path.
|
||||
addonId = self.addonId
|
||||
addon_id = self.addon_id
|
||||
dialog = xbmcgui.Dialog()
|
||||
|
||||
##### SERVER INFO #####
|
||||
|
||||
|
||||
log.debug("Initial setup called.")
|
||||
server = self.userClient.getServer()
|
||||
|
||||
if server:
|
||||
log.debug("Server is already set.")
|
||||
###$ Begin transition phase $###
|
||||
if settings('server') == "":
|
||||
current_server = self.user_client.get_server()
|
||||
self.connectmanager.get_server(current_server)
|
||||
self.user_client.get_userid()
|
||||
self.user_client.get_token()
|
||||
###$ End transition phase $###
|
||||
|
||||
current_state = self.connectmanager.get_state()
|
||||
if current_state['State'] == STATE['SignedIn']:
|
||||
server = current_state['Servers'][0]
|
||||
server_address = self.connectmanager.get_address(server)
|
||||
self._set_server(server_address, server['Name'])
|
||||
self._set_user(server['UserId'], server['AccessToken'])
|
||||
return
|
||||
|
||||
log.debug("Looking for server...")
|
||||
server = self.getServerDetails()
|
||||
log.debug("Found: %s" % server)
|
||||
|
||||
try:
|
||||
prefix, ip, port = server.replace("/", "").split(":")
|
||||
except Exception: # Failed to retrieve server information
|
||||
log.error("getServerDetails failed.")
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
|
||||
return
|
||||
else:
|
||||
server_confirm = dialog.yesno(
|
||||
heading=lang(29999),
|
||||
line1=lang(33034),
|
||||
line2="%s %s" % (lang(30169), server))
|
||||
if server_confirm:
|
||||
# Correct server found
|
||||
log.info("Server is selected. Saving the information.")
|
||||
settings('ipaddress', value=ip)
|
||||
settings('port', value=port)
|
||||
server = self.connectmanager.select_servers()
|
||||
log.info("Server: %s", server)
|
||||
|
||||
if prefix == "https":
|
||||
settings('https', value="true")
|
||||
except RuntimeError as error:
|
||||
log.exception(error)
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id)
|
||||
return
|
||||
|
||||
else:
|
||||
server_address = self.connectmanager.get_address(server)
|
||||
self._set_server(server_address, server['Name'])
|
||||
|
||||
if not server.get('AccessToken') and not server.get('UserId'):
|
||||
try:
|
||||
user = self.connectmanager.login(server)
|
||||
log.info("User authenticated: %s", user)
|
||||
except RuntimeError as error:
|
||||
log.exception(error)
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id)
|
||||
return
|
||||
settings('username', value=user['User']['Name'])
|
||||
self._set_user(user['User']['Id'], user['AccessToken'])
|
||||
else:
|
||||
# User selected no or cancelled the dialog
|
||||
log.info("No server selected.")
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
|
||||
return
|
||||
|
||||
##### USER INFO #####
|
||||
|
||||
log.info("Getting user list.")
|
||||
|
||||
result = self.doUtils("%s/emby/Users/Public?format=json" % server, authenticate=False)
|
||||
if result == "":
|
||||
log.info("Unable to connect to %s" % server)
|
||||
return
|
||||
|
||||
log.debug("Response: %s" % result)
|
||||
# Process the list of users
|
||||
usernames = []
|
||||
users_hasPassword = []
|
||||
|
||||
for user in result:
|
||||
# Username
|
||||
name = user['Name']
|
||||
usernames.append(name)
|
||||
# Password
|
||||
if user['HasPassword']:
|
||||
name = "%s (secure)" % name
|
||||
users_hasPassword.append(name)
|
||||
|
||||
log.info("Presenting user list: %s" % users_hasPassword)
|
||||
user_select = dialog.select(lang(30200), users_hasPassword)
|
||||
if user_select > -1:
|
||||
selected_user = usernames[user_select]
|
||||
log.info("Selected user: %s" % selected_user)
|
||||
settings('username', value=selected_user)
|
||||
else:
|
||||
log.info("No user selected.")
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
|
||||
return
|
||||
user = self.connectmanager.get_state()
|
||||
settings('connectUsername', value=user['ConnectUser']['Name'])
|
||||
self._set_user(server['UserId'], server['AccessToken'])
|
||||
|
||||
##### ADDITIONAL PROMPTS #####
|
||||
|
||||
directPaths = dialog.yesno(
|
||||
heading=lang(30511),
|
||||
line1=lang(33035),
|
||||
nolabel=lang(33036),
|
||||
yeslabel=lang(33037))
|
||||
if directPaths:
|
||||
direct_paths = dialog.yesno(heading=lang(30511),
|
||||
line1=lang(33035),
|
||||
nolabel=lang(33036),
|
||||
yeslabel=lang(33037))
|
||||
if direct_paths:
|
||||
log.info("User opted to use direct paths.")
|
||||
settings('useDirectPaths', value="1")
|
||||
|
||||
# ask for credentials
|
||||
credentials = dialog.yesno(
|
||||
heading=lang(30517),
|
||||
line1= lang(33038))
|
||||
credentials = dialog.yesno(heading=lang(30517), line1=lang(33038))
|
||||
if credentials:
|
||||
log.info("Presenting network credentials dialog.")
|
||||
passwordsXML()
|
||||
|
||||
musicDisabled = dialog.yesno(
|
||||
heading=lang(29999),
|
||||
line1=lang(33039))
|
||||
if musicDisabled:
|
||||
|
||||
music_disabled = dialog.yesno(heading=lang(29999), line1=lang(33039))
|
||||
if music_disabled:
|
||||
log.info("User opted to disable Emby music library.")
|
||||
settings('enableMusic', value="false")
|
||||
else:
|
||||
# Only prompt if the user didn't select direct paths for videos
|
||||
if not directPaths:
|
||||
musicAccess = dialog.yesno(
|
||||
heading=lang(29999),
|
||||
line1=lang(33040))
|
||||
if musicAccess:
|
||||
if not direct_paths:
|
||||
music_access = dialog.yesno(heading=lang(29999), line1=lang(33040))
|
||||
if music_access:
|
||||
log.info("User opted to direct stream music.")
|
||||
settings('streamMusic', value="true")
|
||||
|
||||
def getServerDetails(self):
|
||||
|
||||
log.info("Getting Server Details from Network")
|
||||
|
||||
MULTI_GROUP = ("<broadcast>", 7359)
|
||||
MESSAGE = "who is EmbyServer?"
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(6.0)
|
||||
@classmethod
|
||||
def _set_server(cls, server, name):
|
||||
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
|
||||
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
|
||||
|
||||
log.debug("MultiGroup : %s" % str(MULTI_GROUP))
|
||||
log.debug("Sending UDP Data: %s" % MESSAGE)
|
||||
sock.sendto(MESSAGE, MULTI_GROUP)
|
||||
|
||||
try:
|
||||
data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
|
||||
log.info("Received Response: %s" % data)
|
||||
except Exception:
|
||||
log.error("No UDP Response")
|
||||
return None
|
||||
else:
|
||||
# Get the address
|
||||
data = json.loads(data)
|
||||
return data['Address']
|
||||
settings('serverName', value=name)
|
||||
settings('server', value=server)
|
||||
log.info("Saved server information: %s", server)
|
||||
|
||||
@classmethod
|
||||
def _set_user(cls, user_id, token):
|
||||
|
||||
settings('userId', value=user_id)
|
||||
settings('token', value=token)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#################################################################################################
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
import xbmc
|
||||
|
||||
|
@ -571,4 +572,20 @@ class Read_EmbyServer():
|
|||
def deleteItem(self, itemid):
|
||||
|
||||
url = "{server}/emby/Items/%s?format=json" % itemid
|
||||
self.doUtils(url, action_type="DELETE")
|
||||
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
|
|
@ -2,18 +2,16 @@
|
|||
|
||||
##################################################################################################
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
|
||||
import artwork
|
||||
import clientinfo
|
||||
import connectmanager
|
||||
import downloadutils
|
||||
import read_embyserver as embyserver
|
||||
from utils import window, settings, language as lang
|
||||
|
||||
##################################################################################################
|
||||
|
@ -22,426 +20,276 @@ log = logging.getLogger("EMBY."+__name__)
|
|||
|
||||
##################################################################################################
|
||||
|
||||
|
||||
class UserClient(threading.Thread):
|
||||
|
||||
# Borg - multiple instances, shared state
|
||||
_shared_state = {}
|
||||
_shared_state = {} # Borg
|
||||
|
||||
stop_thread = False
|
||||
auth = True
|
||||
retry = 0
|
||||
_stop_thread = False
|
||||
_user = None
|
||||
_server = None
|
||||
|
||||
currUser = None
|
||||
currUserId = None
|
||||
currServer = None
|
||||
currToken = None
|
||||
HasAccess = True
|
||||
AdditionalUser = []
|
||||
|
||||
userSettings = None
|
||||
_auth = True
|
||||
_has_access = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.__dict__ = self._shared_state
|
||||
self.addon = xbmcaddon.Addon()
|
||||
|
||||
self.doUtils = downloadutils.DownloadUtils()
|
||||
self.doutils = downloadutils.DownloadUtils()
|
||||
self.download = self.doutils.downloadUrl
|
||||
self.emby = embyserver.Read_EmbyServer()
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@classmethod
|
||||
def get_username(cls):
|
||||
return settings('username') or settings('connectUsername') or None
|
||||
|
||||
def getAdditionalUsers(self):
|
||||
def get_user(self, data=None):
|
||||
|
||||
additionalUsers = settings('additionalUsers')
|
||||
if data is not None:
|
||||
self._user = data
|
||||
self._set_user_server()
|
||||
|
||||
if additionalUsers:
|
||||
self.AdditionalUser = additionalUsers.split(',')
|
||||
return self._user
|
||||
|
||||
def getUsername(self):
|
||||
def get_server_details(self):
|
||||
return self._server
|
||||
|
||||
username = settings('username')
|
||||
@classmethod
|
||||
def get_server(cls):
|
||||
|
||||
if not username:
|
||||
log.debug("No username saved.")
|
||||
return ""
|
||||
|
||||
return username
|
||||
|
||||
def getLogLevel(self):
|
||||
|
||||
try:
|
||||
logLevel = int(settings('logLevel'))
|
||||
except ValueError:
|
||||
logLevel = 0
|
||||
|
||||
return logLevel
|
||||
|
||||
def getUserId(self):
|
||||
|
||||
username = self.getUsername()
|
||||
w_userId = window('emby_currUser')
|
||||
s_userId = settings('userId%s' % username)
|
||||
|
||||
# Verify the window property
|
||||
if w_userId:
|
||||
if not s_userId:
|
||||
# Save access token if it's missing from settings
|
||||
settings('userId%s' % username, value=w_userId)
|
||||
log.debug("Returning userId from WINDOW for username: %s UserId: %s"
|
||||
% (username, w_userId))
|
||||
return w_userId
|
||||
# Verify the settings
|
||||
elif s_userId:
|
||||
log.debug("Returning userId from SETTINGS for username: %s userId: %s"
|
||||
% (username, s_userId))
|
||||
return s_userId
|
||||
# No userId found
|
||||
else:
|
||||
log.info("No userId saved for username: %s." % username)
|
||||
|
||||
def getServer(self, prefix=True):
|
||||
|
||||
alternate = settings('altip') == "true"
|
||||
if alternate:
|
||||
# Alternate host
|
||||
HTTPS = settings('secondhttps') == "true"
|
||||
host = settings('secondipaddress')
|
||||
port = settings('secondport')
|
||||
else:
|
||||
# Original host
|
||||
HTTPS = settings('https') == "true"
|
||||
###$ Begin transition phase $###
|
||||
if settings('server') == "":
|
||||
http = "https" if settings('https') == "true" else "http"
|
||||
host = settings('ipaddress')
|
||||
port = settings('port')
|
||||
|
||||
server = host + ":" + port
|
||||
if host and port:
|
||||
settings('server', value="%s://%s:%s" % (http, host, port))
|
||||
###$ End transition phase $###
|
||||
|
||||
if not host:
|
||||
log.debug("No server information saved.")
|
||||
return False
|
||||
return settings('server') or None
|
||||
|
||||
# If https is true
|
||||
if prefix and HTTPS:
|
||||
server = "https://%s" % server
|
||||
return server
|
||||
# If https is false
|
||||
elif prefix and not HTTPS:
|
||||
server = "http://%s" % server
|
||||
return server
|
||||
# If only the host:port is required
|
||||
elif not prefix:
|
||||
return server
|
||||
def verify_server(self):
|
||||
|
||||
def getToken(self):
|
||||
|
||||
username = self.getUsername()
|
||||
userId = self.getUserId()
|
||||
w_token = window('emby_accessToken%s' % userId)
|
||||
s_token = settings('accessToken')
|
||||
|
||||
# Verify the window property
|
||||
if w_token:
|
||||
if not s_token:
|
||||
# Save access token if it's missing from settings
|
||||
settings('accessToken', value=w_token)
|
||||
log.debug("Returning accessToken from WINDOW for username: %s accessToken: %s"
|
||||
% (username, w_token))
|
||||
return w_token
|
||||
# Verify the settings
|
||||
elif s_token:
|
||||
log.debug("Returning accessToken from SETTINGS for username: %s accessToken: %s"
|
||||
% (username, s_token))
|
||||
window('emby_accessToken%s' % username, value=s_token)
|
||||
return s_token
|
||||
else:
|
||||
log.info("No token found.")
|
||||
return ""
|
||||
|
||||
def getSSLverify(self):
|
||||
# Verify host certificate
|
||||
s_sslverify = settings('sslverify')
|
||||
if settings('altip') == "true":
|
||||
s_sslverify = settings('secondsslverify')
|
||||
|
||||
if s_sslverify == "true":
|
||||
url = "%s/emby/Users/Public?format=json" % self.get_server()
|
||||
result = self.download(url, authenticate=False)
|
||||
if result != "": # Specific verification, due to possibility of returning empty dict
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def getSSL(self):
|
||||
# Client side certificate
|
||||
s_cert = settings('sslcert')
|
||||
if settings('altip') == "true":
|
||||
s_cert = settings('secondsslcert')
|
||||
|
||||
if s_cert == "None":
|
||||
return None
|
||||
else:
|
||||
return s_cert
|
||||
|
||||
def setUserPref(self):
|
||||
|
||||
doUtils = self.doUtils.downloadUrl
|
||||
|
||||
result = doUtils("{server}/emby/Users/{UserId}?format=json")
|
||||
self.userSettings = result
|
||||
# Set user image for skin display
|
||||
if result.get('PrimaryImageTag'):
|
||||
window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result['Id'], 'Primary'))
|
||||
|
||||
# Set resume point max
|
||||
result = doUtils("{server}/emby/System/Configuration?format=json")
|
||||
settings('markPlayed', value=str(result['MaxResumePct']))
|
||||
|
||||
def getPublicUsers(self):
|
||||
# Get public Users
|
||||
url = "%s/emby/Users/Public?format=json" % self.getServer()
|
||||
result = self.doUtils.downloadUrl(url, authenticate=False)
|
||||
if result != "":
|
||||
return result
|
||||
else:
|
||||
# Server connection failed
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_ssl(cls):
|
||||
"""
|
||||
Returns boolean value or path to certificate
|
||||
True: Verify ssl
|
||||
False: Don't verify connection
|
||||
"""
|
||||
certificate = settings('sslcert')
|
||||
if certificate != "None":
|
||||
return certificate
|
||||
|
||||
def hasAccess(self):
|
||||
# hasAccess is verified in service.py
|
||||
result = self.doUtils.downloadUrl("{server}/emby/Users?format=json")
|
||||
return True if settings('sslverify') == "true" else False
|
||||
|
||||
if result == False:
|
||||
# Access is restricted, set in downloadutils.py via exception
|
||||
log.info("Access is restricted.")
|
||||
self.HasAccess = False
|
||||
def get_access(self):
|
||||
|
||||
elif window('emby_online') != "true":
|
||||
# Server connection failed
|
||||
pass
|
||||
if not self._has_access:
|
||||
self._set_access()
|
||||
|
||||
elif window('emby_serverStatus') == "restricted":
|
||||
log.info("Access is granted.")
|
||||
self.HasAccess = True
|
||||
window('emby_serverStatus', clear=True)
|
||||
xbmcgui.Dialog().notification(lang(29999), lang(33007))
|
||||
return self._has_access
|
||||
|
||||
def loadCurrUser(self, authenticated=False):
|
||||
def _set_access(self):
|
||||
|
||||
doUtils = self.doUtils
|
||||
username = self.getUsername()
|
||||
userId = self.getUserId()
|
||||
try:
|
||||
self.download("{server}/emby/Users?format=json")
|
||||
except Warning as error:
|
||||
if self._has_access and "restricted" in error:
|
||||
self._has_access = False
|
||||
log.info("Access is restricted")
|
||||
else:
|
||||
if not self._has_access:
|
||||
self._has_access = True
|
||||
window('emby_serverStatus', clear=True)
|
||||
log.info("Access is granted")
|
||||
xbmcgui.Dialog().notification(lang(29999), lang(33007))
|
||||
|
||||
# Only to be used if token exists
|
||||
self.currUserId = userId
|
||||
self.currServer = self.getServer()
|
||||
self.currToken = self.getToken()
|
||||
self.ssl = self.getSSLverify()
|
||||
self.sslcert = self.getSSL()
|
||||
@classmethod
|
||||
def get_userid(cls):
|
||||
|
||||
# Test the validity of current token
|
||||
if authenticated == False:
|
||||
url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
|
||||
window('emby_currUser', value=userId)
|
||||
window('emby_accessToken%s' % userId, value=self.currToken)
|
||||
result = doUtils.downloadUrl(url)
|
||||
###$ Begin transition phase $###
|
||||
if settings('userId') == "":
|
||||
settings('userId', value=settings('userId%s' % settings('username')))
|
||||
###$ End transition phase $###
|
||||
|
||||
if result == 401:
|
||||
# Token is no longer valid
|
||||
self.resetClient()
|
||||
return False
|
||||
return settings('userId') or None
|
||||
|
||||
# Set to windows property
|
||||
window('emby_currUser', value=userId)
|
||||
window('emby_accessToken%s' % userId, value=self.currToken)
|
||||
window('emby_server%s' % userId, value=self.currServer)
|
||||
window('emby_server_%s' % userId, value=self.getServer(prefix=False))
|
||||
@classmethod
|
||||
def get_token(cls):
|
||||
|
||||
# Set DownloadUtils values
|
||||
doUtils.setUsername(username)
|
||||
doUtils.setUserId(self.currUserId)
|
||||
doUtils.setServer(self.currServer)
|
||||
doUtils.setToken(self.currToken)
|
||||
doUtils.setSSL(self.ssl, self.sslcert)
|
||||
# parental control - let's verify if access is restricted
|
||||
self.hasAccess()
|
||||
# Start DownloadUtils session
|
||||
doUtils.startSession()
|
||||
self.getAdditionalUsers()
|
||||
# Set user preferences in settings
|
||||
self.currUser = username
|
||||
self.setUserPref()
|
||||
###$ Begin transition phase $###
|
||||
if settings('token') == "":
|
||||
settings('token', value=settings('accessToken'))
|
||||
###$ End transition phase $###
|
||||
|
||||
return settings('token') or None
|
||||
|
||||
def authenticate(self):
|
||||
def _set_user_server(self):
|
||||
|
||||
dialog = xbmcgui.Dialog()
|
||||
self._server = self.download("{server}/emby/System/Configuration?format=json")
|
||||
settings('markPlayed', value=str(self._server['MaxResumePct']))
|
||||
|
||||
# Get /profile/addon_data
|
||||
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
|
||||
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
|
||||
self._user = self.download("{server}/emby/Users/{UserId}?format=json")
|
||||
if "PrimaryImageTag" in self._user:
|
||||
window('EmbyUserImage',
|
||||
value=artwork.Artwork().getUserArtwork(self._user['Id'], 'Primary'))
|
||||
|
||||
username = self.getUsername()
|
||||
server = self.getServer()
|
||||
def _authenticate(self):
|
||||
|
||||
# If there's no settings.xml
|
||||
if not hasSettings:
|
||||
log.info("No settings.xml found.")
|
||||
self.auth = False
|
||||
return
|
||||
# If no user information
|
||||
elif not server or not username:
|
||||
log.info("Missing server information.")
|
||||
self.auth = False
|
||||
return
|
||||
# If there's a token, load the user
|
||||
elif self.getToken():
|
||||
result = self.loadCurrUser()
|
||||
if not self.get_server() or not self.get_username():
|
||||
log.info('missing server or user information')
|
||||
self._auth = False
|
||||
|
||||
if result == False:
|
||||
pass
|
||||
elif self.get_token():
|
||||
try:
|
||||
self._load_user()
|
||||
except Warning:
|
||||
log.info("token is invalid")
|
||||
else:
|
||||
log.info("Current user: %s" % self.currUser)
|
||||
log.info("Current userId: %s" % self.currUserId)
|
||||
log.debug("Current accessToken: %s" % self.currToken)
|
||||
log.info("current user: %s", self.get_username())
|
||||
log.info("current userid: %s", self.get_userid())
|
||||
log.debug("current token: %s", self.get_token())
|
||||
return
|
||||
|
||||
##### AUTHENTICATE USER #####
|
||||
server = self.get_server()
|
||||
username = self.get_username().decode('utf-8')
|
||||
users = self.emby.getUsers(server)
|
||||
user_found = None
|
||||
|
||||
users = self.getPublicUsers()
|
||||
password = ""
|
||||
|
||||
# Find user in list
|
||||
for user in users:
|
||||
name = user['Name']
|
||||
|
||||
if username.decode('utf-8') in name:
|
||||
# If user has password
|
||||
if user['HasPassword'] == True:
|
||||
password = dialog.input(
|
||||
heading="%s %s" % (lang(33008), username.decode('utf-8')),
|
||||
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
# If password dialog is cancelled
|
||||
if not password:
|
||||
log.warn("No password entered.")
|
||||
window('emby_serverStatus', value="Stop")
|
||||
self.auth = False
|
||||
return
|
||||
if username == user['Name']:
|
||||
user_found = user
|
||||
break
|
||||
else:
|
||||
# Manual login, user is hidden
|
||||
password = dialog.input(
|
||||
heading="%s %s" % (lang(33008), username.decode('utf-8')),
|
||||
option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
sha1 = hashlib.sha1(password)
|
||||
sha1 = sha1.hexdigest()
|
||||
|
||||
# Authenticate username and password
|
||||
data = {'username': username, 'password': sha1}
|
||||
log.debug(data)
|
||||
|
||||
url = "%s/emby/Users/AuthenticateByName?format=json" % server
|
||||
result = self.doUtils.downloadUrl(url, postBody=data, action_type="POST", authenticate=False)
|
||||
|
||||
try:
|
||||
log.info("Auth response: %s" % result)
|
||||
accessToken = result['AccessToken']
|
||||
|
||||
except (KeyError, TypeError):
|
||||
log.info("Failed to retrieve the api key.")
|
||||
accessToken = None
|
||||
|
||||
if accessToken is not None:
|
||||
self.currUser = username
|
||||
dialog.notification(lang(29999),
|
||||
"%s %s!" % (lang(33000), self.currUser.decode('utf-8')))
|
||||
settings('accessToken', value=accessToken)
|
||||
settings('userId%s' % username, value=result['User']['Id'])
|
||||
log.info("User Authenticated: %s" % accessToken)
|
||||
self.loadCurrUser(authenticated=True)
|
||||
window('emby_serverStatus', clear=True)
|
||||
self.retry = 0
|
||||
user = self.connectmanager.login_manual(server, user_found)
|
||||
except RuntimeError:
|
||||
window('emby_serverStatus', value="stop")
|
||||
self._auth = False
|
||||
return
|
||||
else:
|
||||
log.error("User authentication failed.")
|
||||
settings('accessToken', value="")
|
||||
settings('userId%s' % username, value="")
|
||||
dialog.ok(lang(33001), lang(33009))
|
||||
log.info("user: %s", user)
|
||||
settings('username', value=user['User']['Name'])
|
||||
settings('token', value=user['AccessToken'])
|
||||
settings('userId', value=user['User']['Id'])
|
||||
xbmcgui.Dialog().notification(lang(29999),
|
||||
"%s %s!" % (lang(33000), username))
|
||||
self._load_user(authenticated=True)
|
||||
window('emby_serverStatus', clear=True)
|
||||
|
||||
# Give two attempts at entering password
|
||||
if self.retry == 2:
|
||||
log.info("Too many retries. "
|
||||
"You can retry by resetting attempts in the addon settings.")
|
||||
window('emby_serverStatus', value="Stop")
|
||||
dialog.ok(lang(33001), lang(33010))
|
||||
def _load_user(self, authenticated=False):
|
||||
|
||||
self.retry += 1
|
||||
self.auth = False
|
||||
doutils = self.doutils
|
||||
|
||||
def resetClient(self):
|
||||
userid = self.get_userid()
|
||||
server = self.get_server()
|
||||
token = self.get_token()
|
||||
|
||||
log.info("Reset UserClient authentication.")
|
||||
if self.currToken is not None:
|
||||
# In case of 401, removed saved token
|
||||
settings('accessToken', value="")
|
||||
window('emby_accessToken%s' % self.getUserId(), clear=True)
|
||||
self.currToken = None
|
||||
log.info("User token has been removed.")
|
||||
# Set properties
|
||||
window('emby_currUser', value=userid)
|
||||
window('emby_server%s' % userid, value=server)
|
||||
window('emby_accessToken%s' % userid, value=token)
|
||||
|
||||
self.auth = True
|
||||
self.currUser = None
|
||||
# Test the validity of the current token
|
||||
if not authenticated:
|
||||
try:
|
||||
self.download("{server}/emby/Users/{UserId}?format=json")
|
||||
except Warning as error:
|
||||
if "401" in error:
|
||||
# Token is not longer valid
|
||||
raise
|
||||
|
||||
# Set downloadutils.py values
|
||||
doutils.setUserId(userid)
|
||||
doutils.setServer(server)
|
||||
doutils.setToken(token)
|
||||
doutils.setSSL(self.get_ssl())
|
||||
|
||||
# Start downloadutils.py session
|
||||
doutils.startSession()
|
||||
|
||||
# Set _user and _server
|
||||
# verify user access
|
||||
try:
|
||||
self._set_access()
|
||||
self._set_user_server()
|
||||
except Warning: # We don't need to raise any exceptions
|
||||
pass
|
||||
|
||||
def _reset_client(self):
|
||||
|
||||
log.info("reset UserClient authentication")
|
||||
|
||||
settings('accessToken', value="")
|
||||
window('emby_accessToken', clear=True)
|
||||
|
||||
log.info("user token revoked.")
|
||||
|
||||
self._user = None
|
||||
self.auth = None
|
||||
|
||||
def run(self):
|
||||
|
||||
monitor = xbmc.Monitor()
|
||||
self.connectmanager = connectmanager.ConnectManager()
|
||||
|
||||
log.warn("----===## Starting UserClient ##===----")
|
||||
|
||||
while not monitor.abortRequested():
|
||||
while not self._stop_thread:
|
||||
|
||||
status = window('emby_serverStatus')
|
||||
if status:
|
||||
# Verify the connection status to server
|
||||
if status == "restricted":
|
||||
# Parental control is restricting access
|
||||
self.HasAccess = False
|
||||
self._has_access = False
|
||||
|
||||
elif status == "401":
|
||||
# Unauthorized access, revoke token
|
||||
window('emby_serverStatus', value="Auth")
|
||||
self.resetClient()
|
||||
window('emby_serverStatus', value="auth")
|
||||
self._reset_client()
|
||||
|
||||
if self.auth and (self.currUser is None):
|
||||
if self._auth and self._user is None:
|
||||
# Try to authenticate user
|
||||
status = window('emby_serverStatus')
|
||||
if not status or status == "Auth":
|
||||
if not status or status == "auth":
|
||||
# Set auth flag because we no longer need
|
||||
# to authenticate the user
|
||||
self.auth = False
|
||||
self.authenticate()
|
||||
self._auth = False
|
||||
self._authenticate()
|
||||
|
||||
|
||||
if not self.auth and (self.currUser is None):
|
||||
if not self._auth and self._user is None:
|
||||
# If authenticate failed.
|
||||
server = self.getServer()
|
||||
username = self.getUsername()
|
||||
server = self.get_server()
|
||||
username = self.get_username()
|
||||
status = window('emby_serverStatus')
|
||||
|
||||
# The status Stop is for when user cancelled password dialog.
|
||||
if server and username and status != "Stop":
|
||||
if server and username and status != "stop":
|
||||
# Only if there's information found to login
|
||||
log.debug("Server found: %s" % server)
|
||||
log.debug("Username found: %s" % username)
|
||||
self.auth = True
|
||||
|
||||
|
||||
if self.stop_thread == True:
|
||||
# If stopping the client didn't work
|
||||
break
|
||||
log.info("Server found: %s", server)
|
||||
log.info("Username found: %s", username)
|
||||
self._auth = True
|
||||
|
||||
if monitor.waitForAbort(1):
|
||||
# Abort was requested while waiting. We should exit
|
||||
break
|
||||
|
||||
self.doUtils.stopSession()
|
||||
self.doutils.stopSession()
|
||||
log.warn("##===---- UserClient Stopped ----===##")
|
||||
|
||||
def stopClient(self):
|
||||
# When emby for kodi terminates
|
||||
self.stop_thread = True
|
||||
def stop_client(self):
|
||||
self._stop_thread = True
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
|
@ -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,30 +148,32 @@
|
|||
<wrapmultiline>true</wrapmultiline>
|
||||
<aligny>top</aligny>
|
||||
<width>340</width>
|
||||
<height>100%</height>
|
||||
</control>
|
||||
|
||||
<control type="image">
|
||||
<description>qrcode</description>
|
||||
<texture>qrcode_disclaimer.png</texture>
|
||||
<width>140</width>
|
||||
<height>140</height>
|
||||
<top>10</top>
|
||||
<left>360</left>
|
||||
</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="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 type="image">
|
||||
<description>qrcode</description>
|
||||
<texture>qrcode_disclaimer.png</texture>
|
||||
<width>140</width>
|
||||
<height>140</height>
|
||||
<top>10</top>
|
||||
<left>360</left>
|
||||
</control>
|
||||
</control>
|
||||
</control>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
</controls>
|
||||
</window>
|
|
@ -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>
|
280
resources/skins/default/1080i/script-emby-connect-server.xml
Normal 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>
|
198
resources/skins/default/1080i/script-emby-connect-users.xml
Normal 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>
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
resources/skins/default/media/fading_circle.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/skins/default/media/network.png
Normal file
After Width: | Height: | Size: 727 B |
BIN
resources/skins/default/media/user_image.png
Normal file
After Width: | Height: | Size: 662 B |
BIN
resources/skins/default/media/userflyoutdefault.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
resources/skins/default/media/userflyoutdefault2.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
resources/skins/default/media/wifi.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
22
service.py
|
@ -43,7 +43,7 @@ log = logging.getLogger("EMBY.service")
|
|||
#################################################################################################
|
||||
|
||||
|
||||
class Service():
|
||||
class Service(object):
|
||||
|
||||
welcome_msg = True
|
||||
server_online = True
|
||||
|
@ -59,7 +59,7 @@ class Service():
|
|||
|
||||
self.clientInfo = clientinfo.ClientInfo()
|
||||
self.addonName = self.clientInfo.getAddonName()
|
||||
logLevel = userclient.UserClient().getLogLevel()
|
||||
logLevel = settings('logLevel')
|
||||
self.monitor = xbmc.Monitor()
|
||||
|
||||
window('emby_logLevel', value=str(logLevel))
|
||||
|
@ -128,7 +128,7 @@ class Service():
|
|||
|
||||
# Emby server is online
|
||||
# Verify if user is set and has access to the server
|
||||
if (user.currUser is not None) and user.HasAccess:
|
||||
if user.get_user() is not None and user.get_access():
|
||||
|
||||
# If an item is playing
|
||||
if xbmc.Player().isPlaying():
|
||||
|
@ -166,7 +166,7 @@ class Service():
|
|||
# Reset authentication warnings
|
||||
self.welcome_msg = False
|
||||
# Get additional users
|
||||
additionalUsers = user.AdditionalUser
|
||||
additionalUsers = settings('additionalUsers')
|
||||
if additionalUsers:
|
||||
add = ", %s" % ", ".join(additionalUsers)
|
||||
else:
|
||||
|
@ -174,7 +174,7 @@ class Service():
|
|||
xbmcgui.Dialog().notification(
|
||||
heading=lang(29999),
|
||||
message=("%s %s%s!"
|
||||
% (lang(33000), user.currUser.decode('utf-8'),
|
||||
% (lang(33000), user.get_username().decode('utf-8'),
|
||||
add.decode('utf-8'))),
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
time=2000,
|
||||
|
@ -194,7 +194,7 @@ class Service():
|
|||
library.start()
|
||||
else:
|
||||
|
||||
if (user.currUser is None) and self.warn_auth:
|
||||
if (user.get_user() is None) and self.warn_auth:
|
||||
# Alert user is not authenticated and suppress future warning
|
||||
self.warn_auth = False
|
||||
log.info("Not authenticated yet.")
|
||||
|
@ -202,10 +202,8 @@ class Service():
|
|||
# User access is restricted.
|
||||
# Keep verifying until access is granted
|
||||
# unless server goes offline or Kodi is shut down.
|
||||
while user.HasAccess == False:
|
||||
while not user.get_access():
|
||||
# Verify access with an API call
|
||||
user.hasAccess()
|
||||
|
||||
if window('emby_online') != "true":
|
||||
# Server went offline
|
||||
break
|
||||
|
@ -218,11 +216,11 @@ class Service():
|
|||
# or Kodi is shut down.
|
||||
while not monitor.abortRequested():
|
||||
|
||||
if user.getServer() == False:
|
||||
if user.get_server() is None:
|
||||
# No server info set in add-on settings
|
||||
pass
|
||||
|
||||
elif user.getPublicUsers() == False:
|
||||
elif not user.verify_server():
|
||||
# Server is offline.
|
||||
# Alert the user and suppress future warning
|
||||
if self.server_online:
|
||||
|
@ -287,7 +285,7 @@ class Service():
|
|||
##### Emby thread is terminating. #####
|
||||
|
||||
if self.userclient_running:
|
||||
user.stopClient()
|
||||
user.stop_client()
|
||||
|
||||
if self.library_running:
|
||||
library.stopThread()
|
||||
|
|