Connection manager, part 2

This commit is contained in:
tomkat83 2017-07-16 15:22:08 +02:00
parent 962ce6da1e
commit 7a9e0611ed
9 changed files with 200 additions and 456 deletions

View file

@ -1158,12 +1158,12 @@ msgstr ""
# Server selection dialog: button text to sign in or sign out of plex.tv
msgctxt "#30600"
msgid "Toggle plex.tv sign-in"
msgid "Sign-in to plex.tv"
msgstr ""
# Server selection dialog: button text to add server manually
msgctxt "#30601"
msgid "Manually add server"
msgid "Manually add PMS"
msgstr ""
# Button text, e.g. to cancel a dialog

View file

@ -56,6 +56,7 @@ class Monitor_Window(Thread):
except:
log.error('Failed to execute function %s with params %s'
% (function, params))
raise
def run(self):
thread_stopped = self.thread_stopped

View file

@ -2,13 +2,21 @@
###############################################################################
from logging import getLogger
from hashlib import md5
import json
import requests
from struct import pack
import socket
import time
from datetime import datetime
import xml.etree.ElementTree as etree
from Queue import Queue
from threading import Thread
from xbmc import sleep
import credentials as cred
from utils import tryDecode
from PlexFunctions import PMSHttpsEnabled
###############################################################################
@ -35,6 +43,11 @@ CONNECTIONMODE = {
'Manual': 2
}
# multicast to PMS
IP_PLEXGDM = '239.0.0.250'
PORT_PLEXGDM = 32414
MSG_PLEXGDM = 'M-SEARCH * HTTP/1.0'
###############################################################################
@ -52,15 +65,16 @@ def getServerAddress(server, mode):
class ConnectionManager(object):
default_timeout = 20
default_timeout = 30
apiClients = []
minServerVersion = "3.0.5930"
minServerVersion = "1.7.0.0"
connectUser = None
# Token for plex.tv
plexToken = None
def __init__(self, appName, appVersion, deviceName, deviceId,
capabilities=None, devicePixelRatio=None):
log.info("Begin ConnectionManager constructor")
log.debug("Instantiating")
self.credentialProvider = cred.Credentials()
self.appName = appName
@ -144,8 +158,7 @@ class ConnectionManager(object):
if server is None or systemInfo is None:
return
server['Name'] = systemInfo['ServerName']
server['Id'] = systemInfo['Id']
server['Id'] = systemInfo.attrib['machineIdentifier']
if systemInfo.get('LocalAddress'):
server['LocalAddress'] = systemInfo['LocalAddress']
@ -155,16 +168,11 @@ class ConnectionManager(object):
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')
headers['Accept'] = '*/*'
headers['Content-type'] = request.get(
'contentType',
"application/x-www-form-urlencoded")
def requestUrl(self, request):
@ -192,9 +200,10 @@ class ConnectionManager(object):
else:
try:
return r.json()
except ValueError:
r.content # Read response to release connection
return etree.fromstring(r.content)
except etree.ParseError:
# Read response to release connection
r.content
return
def _requests(self, action, **kwargs):
@ -212,78 +221,67 @@ class ConnectionManager(object):
def getConnectUrl(self, handler):
return "https://connect.emby.media/service/%s" % handler
def _findServers(self, foundServers):
@staticmethod
def _findServers(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:
for server in foundServers:
if '200 OK' not in server['data']:
continue
ip = server['from'][0]
info = {'LastCONNECTIONMODE': CONNECTIONMODE['Local']}
for line in server['data'].split('\n'):
if line.startswith('Name:'):
info['Name'] = tryDecode(line.split(':')[1].strip())
elif line.startswith('Port:'):
info['Port'] = line.split(':')[1].strip()
elif line.startswith('Resource-Identifier:'):
info['Id'] = line.split(':')[1].strip()
elif line.startswith('Updated-At:'):
pass
return None
elif line.startswith('Version:'):
pass
# Need to check whether we need HTTPS or only HTTP
https = PMSHttpsEnabled('%s:%s' % (ip, info['Port']))
if https is None:
# Error contacting url. Skip for now
continue
elif https is True:
info['LocalAddress'] = 'https://%s:%s' % (ip, info['Port'])
else:
info['LocalAddress'] = 'http://%s:%s' % (ip, info['Port'])
servers.append(info)
return servers
def _serverDiscovery(self):
"""
PlexGDM
"""
# setup socket for discovery -> multicast message
GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
GDM.settimeout(2.0)
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)
# Set the time-to-live for messages to 2 for local network
ttl = pack('b', 2)
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
servers = []
try:
sock.sendto(MESSAGE, MULTI_GROUP)
except Exception as error:
log.error(error)
return servers
# Send data to the multicast group
GDM.sendto(MSG_PLEXGDM, (IP_PLEXGDM, PORT_PLEXGDM))
# Look for responses from all recipients
while True:
try:
data, addr = sock.recvfrom(1024) # buffer size
servers.append(json.loads(data))
data, server = GDM.recvfrom(1024)
servers.append({'from': server, 'data': 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)
break
except:
# Probably error: (101, 'Network is unreachable')
log.error('Could not find Plex servers using GDM')
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
finally:
GDM.close()
return servers
def _normalizeAddress(self, address):
@ -345,15 +343,11 @@ class ConnectionManager(object):
self.credentialProvider.getCredentials(credentials)
def _tryConnect(self, url, timeout=None, options={}):
url = self.getEmbyServerUrl(url, "system/info/public")
log.info("tryConnect url: %s" % url)
url = '%s/identity' % url
log.debug("tryConnect url: %s" % url)
return self.requestUrl({
'type': "GET",
'url': url,
'dataType': "json",
'timeout': timeout,
'ssl': options.get('ssl')
})
@ -361,6 +355,61 @@ class ConnectionManager(object):
def _addAppInfoToConnectRequest(self):
return "%s/%s" % (self.appName, self.appVersion)
def __get_PMS_servers_from_plex_tv(self):
"""
Retrieves Plex Media Servers from plex.tv/pms/resources
"""
servers = []
xml = self.requestUrl({
'url': 'https://plex.tv/api/resources?includeHttps=1',
'type': 'GET',
'headers': {'X-Plex-Token': self.plexToken},
'timeout': 5.0,
'ssl': True})
try:
xml.attrib
except AttributeError:
log.error('Could not get list of PMS from plex.tv')
return servers
maxAgeSeconds = 2*60*60*24
for device in xml.findall('Device'):
if 'server' not in device.attrib.get('provides'):
# No PMS - skip
continue
cons = device.find('Connection')
if cons is None:
# no valid connection - skip
continue
# check MyPlex data age - skip if >2 days
server = {'Name': device.attrib.get('name')}
infoAge = time.time() - int(device.attrib.get('lastSeenAt'))
if infoAge > maxAgeSeconds:
log.info("Server %s not seen for 2 days - skipping."
% server['Name'])
continue
server['Id'] = device.attrib['clientIdentifier']
server['ConnectServerId'] = device.attrib['clientIdentifier']
# server['AccessToken'] = device.attrib['accessToken']
server['ExchangeToken'] = device.attrib['accessToken']
# One's own Plex home?
server['UserLinkType'] = 'Guest' if device.attrib['owned'] == '0' \
else 'LinkedUser'
# Foreign PMS' user name
server['UserId'] = device.attrib.get('sourceTitle')
for con in cons:
if con.attrib['local'] == '1':
# Local LAN address; there might be several!!
server['LocalAddress'] = con.attrib['uri']
else:
server['RemoteAddress'] = con.attrib['uri']
# Additional stuff, not yet implemented
server['local'] = device.attrib.get('publicAddressMatches')
servers.append(server)
return servers
def _getConnectServers(self, credentials):
log.info("Begin getConnectServers")
@ -375,7 +424,6 @@ class ConnectionManager(object):
'type': "GET",
'url': url,
'dataType': "json",
'headers': {
'X-Connect-UserToken': credentials['ConnectAccessToken']
}
@ -396,18 +444,16 @@ class ConnectionManager(object):
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)
if self.plexToken:
connectServers = self.__get_PMS_servers_from_plex_tv()
self._mergeServers(servers, connectServers)
foundServers = self._findServers(self._serverDiscovery())
self._mergeServers(servers, foundServers)
servers = self._filterServers(servers, connectServers)
@ -550,8 +596,9 @@ class ConnectionManager(object):
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'])
if self._compareVersions(self._getMinServerVersion(),
result.attrib['version']) == 1:
log.warn("minServerVersion requirement not met. Server version: %s" % result.attrib['version'])
return {
'State': CONNECTIONSTATE['ServerUpdateNeeded'],
'Servers': [server]
@ -616,7 +663,6 @@ class ConnectionManager(object):
'type': "GET",
'url': self.getEmbyServerUrl(url, "System/Info"),
'ssl': options.get('ssl'),
'dataType': "json",
'headers': {
'X-MediaBrowser-Token': server['AccessToken']
}
@ -631,7 +677,6 @@ class ConnectionManager(object):
'type': "GET",
'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']),
'ssl': options.get('ssl'),
'dataType': "json",
'headers': {
'X-MediaBrowser-Token': server['AccessToken']
}
@ -658,7 +703,6 @@ class ConnectionManager(object):
'nameOrEmail': username,
'password': md5
},
'dataType': "json"
}
try:
result = self.requestUrl(request)
@ -695,7 +739,6 @@ class ConnectionManager(object):
'type': "GET",
'url': url,
'dataType': "json",
'headers': {
'X-Connect-UserToken': accessToken
}
@ -718,7 +761,6 @@ class ConnectionManager(object):
'url': url,
'type': "GET",
'dataType': "json",
'ssl': options.get('ssl'),
'params': {
'ConnectUserId': credentials['ConnectUserId']

View file

@ -1,14 +1,20 @@
# -*- coding: utf-8 -*-
###############################################################################
from logging import getLogger
from copy import deepcopy
from os import makedirs
from xbmc import getIPAddress
# from connect.connectionmanager import ConnectionManager
from downloadutils import DownloadUtils
from dialogs.serverconnect import ServerConnect
from dialogs.servermanual import ServerManual
from connect.plex_tv import plex_tv_sign_in_with_pin
import connect.connectionmanager as connectionmanager
from userclient import UserClient
from utils import window, settings, tryEncode, language as lang, dialog
from utils import window, settings, tryEncode, language as lang, dialog, \
exists_dir
from PlexFunctions import GetMachineIdentifier, get_pms_settings, \
check_connection
import variables as v
@ -18,6 +24,7 @@ import state
log = getLogger("PLEX."+__name__)
STATE = connectionmanager.CONNECTIONSTATE
XML_PATH = (tryEncode(v.ADDON_PATH), "default", "1080i")
###############################################################################
@ -55,6 +62,7 @@ def get_plex_login_from_settings():
class ConnectManager(object):
# Borg
__shared_state = {}
state = {}
def __init__(self):
# Borg
@ -68,19 +76,36 @@ class ConnectManager(object):
plexdict = get_plex_login_from_settings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plexLogin = plexdict['plexLogin']
self.plexToken = plexdict['plexToken']
self.plexid = plexdict['plexid']
# Token for the PMS, not plex.tv
self.__connect = connectionmanager.ConnectionManager(
appName="Kodi",
appVersion=v.ADDON_VERSION,
deviceName=v.DEVICENAME,
deviceId=window('plex_client_Id'))
self.pms_token = settings('accessToken')
self.plexToken = plexdict['plexToken']
self.__connect.plexToken = self.plexToken
if self.plexToken:
log.debug('Found a plex.tv token in the settings')
if not exists_dir(v.ADDON_PATH_DATA):
makedirs(v.ADDON_PATH_DATA)
self.__connect.setFilePath(v.ADDON_PATH_DATA)
if state.CONNECT_STATE:
self.state = state.CONNECT_STATE
else:
self.state = self.__connect.connect()
log.debug("Started with: %s", self.state)
state.CONNECT_STATE = deepcopy(self.state)
def update_state(self):
self.state = self.__connect.connect({'updateDateLastAccessed': False})
return self.get_state()
def get_sate(self):
window('emby_state.json', value=self.state)
def get_state(self):
state.CONNECT_STATE = deepcopy(self.state)
return self.state
def get_server(self, server, options={}):
@ -98,13 +123,14 @@ class ConnectManager(object):
"""
Will return selected server or raise RuntimeError
"""
status = self.__connect.connect({'enableAutoLogin': False})
dia = ServerConnect("script-plex-connect-server.xml", *XML_PATH)
kwargs = {
'connect_manager': None, # self._connect
'connect_manager': self.__connect,
'username': state.PLEX_USERNAME,
'user_image': state.PLEX_USER_IMAGE,
# 'servers': state.get('Servers') or [],
# 'emby_connect': False if user else True
'servers': status.get('Servers') or [],
'plex_connect': False if status.get('ConnectUser') else True
}
dia.set_args(**kwargs)
dia.doModal()
@ -113,12 +139,9 @@ class ConnectManager(object):
log.debug("Server selected")
return dia.get_server()
elif dia._is_connect_login():
elif dia.is_connect_login():
log.debug("Login to plex.tv")
try:
self._login_connect()
except RuntimeError:
pass
self.plex_tv_signin()
return self.select_servers()
elif dia.is_manual_server():
@ -134,7 +157,7 @@ class ConnectManager(object):
def manual_server(self):
# Return server or raise error
dia = ServerManual("script-plex-connect-server-manual.xml", *XML_PATH)
dia._set_connect_manager(self.__connect)
dia.set_connect_manager(self.__connect)
dia.doModal()
if dia._is_connected():
@ -142,19 +165,6 @@ class ConnectManager(object):
else:
raise RuntimeError("Server is not connected")
def _login_connect(self):
# Return connect user or raise error
dia = LoginConnect("script-emby-connect-login.xml", *XML_PATH)
dia._set_connect_manager(self.__connect)
dia.doModal()
self.update_state()
if dia.is_logged_in():
return dia.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]
@ -233,7 +243,7 @@ class ConnectManager(object):
# Update the token in data.txt
self.__connect.credentialProvider.getCredentials(credentials)
def _get_connect_servers(self):
def get_connect_servers(self):
connect_servers = []
servers = self.__connect.getAvailableServers()
@ -580,8 +590,7 @@ class ConnectManager(object):
"""
Returns a list of servers from GDM and possibly plex.tv
"""
self.discoverPMS(xbmc.getIPAddress(),
plexToken=self.plexToken)
self.discoverPMS(getIPAddress(), plexToken=self.plexToken)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
log.debug('PMS serverlist: %s' % serverlist)
return serverlist

View file

@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
##################################################################################################
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
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 set_connect_manager(self, connect_manager):
self.connect_manager = connect_manager
def is_logged_in(self):
return True if self._user else False
def get_user(self):
return self._user
def onInit(self):
self.user_field = self._add_editcontrol(725, 385, 40, 500)
self.setFocus(self.user_field)
self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1)
self.signin_button = self.getControl(SIGN_IN)
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)
self.password_field.controlUp(self.user_field)
self.password_field.controlDown(self.signin_button)
self.signin_button.controlUp(self.password_field)
self.remind_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 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 (self.error == ERROR['Empty']
and self.user_field.getText() and self.password_field.getText()):
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="User",
font="font10",
textColor="ff525252",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"),
isPassword=password)
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def _login(self, username, password):
result = self.connect_manager.loginToConnect(username, password)
if result is False:
self._error(ERROR['Invalid'], lang(33009))
return False
else:
self._user = result
return True
def _error(self, state, message):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True')
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('False')

View file

@ -7,12 +7,14 @@ from logging import getLogger
import xbmc
import xbmcgui
import connect.connectionmanager as connectionmanager
from utils import language as lang
###############################################################################
log = getLogger("PLEX."+__name__)
CONN_STATE = connectionmanager.CONNECTIONSTATE
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
@ -25,7 +27,7 @@ CANCEL = 201
MESSAGE_BOX = 202
MESSAGE = 203
BUSY = 204
EMBY_CONNECT = 205
PLEX_CONNECT = 205
MANUAL_SERVER = 206
###############################################################################
@ -42,7 +44,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
_manual_server = False
def set_args(self, **kwargs):
# connect_manager, username, user_image, servers, emby_connect
# connect_manager, username, user_image, servers, plex_connect
for key, value in kwargs.iteritems():
setattr(self, key, value)
@ -74,8 +76,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
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]"+'plex.tv user change'+"[/B][/UPPERCASE]")
if not self.plex_connect: # Change connect user
self.getControl(PLEX_CONNECT).setLabel("[UPPERCASE][B]"+'plex.tv user change'+"[/B][/UPPERCASE]")
if self.servers:
self.setFocus(self.list_)
@ -107,7 +109,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
def onClick(self, control):
if control == EMBY_CONNECT:
if control == PLEX_CONNECT:
self.connect_manager.clearData()
self._connect_login = True
self.close()

View file

@ -38,3 +38,6 @@ PLEX_USER_ID = None
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user
PLEX_TRANSIENT_TOKEN = None
# Used by connectmanager.py
CONNECT_STATE = {}

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import xbmc
from xbmcaddon import Addon
from os.path import join
# Paths are in unicode, otherwise Windows will throw fits
# For any file operations with KODI function, use encoded strings!
@ -32,6 +33,7 @@ KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
ADDON_PATH_DATA = join(KODI_PROFILE, 'addon_data', ADDON_ID, '')
if xbmc.getCondVisibility('system.platform.osx'):
PLATFORM = "MacOSX"

View file

@ -1,179 +0,0 @@
<?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>15%</top>
<control type="image">
<description>Background box</description>
<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>30</top>
<left>25</left>
</control>
<control type="group">
<width>500</width>
<left>50</left>
<control type="label">
<description>Sign in emby connect</description>
<label>$ADDON[plugin.video.emby 30600]</label>
<textcolor>white</textcolor>
<font>font12</font>
<aligny>top</aligny>
<top>115</top>
</control>
<control type="group">
<top>190</top>
<control type="label">
<description>Username email</description>
<label>$ADDON[plugin.video.emby 30543]</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>275</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>385</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 type="group">
<description>Disclaimer</description>
<top>510</top>
<control type="label">
<description>Disclaimer label</description>
<label>$ADDON[plugin.video.emby 30603]</label>
<font>font10</font>
<textcolor>ff464646</textcolor>
<wrapmultiline>true</wrapmultiline>
<aligny>top</aligny>
<width>340</width>
<height>100%</height>
</control>
<control type="group">
<control type="label">
<description>Scan me</description>
<label>[UPPERCASE]$ADDON[plugin.video.emby 30604][/UPPERCASE]</label>
<font>font12</font>
<textcolor>ff0b8628</textcolor>
<aligny>top</aligny>
<width>200</width>
<top>120</top>
<left>230</left>
</control>
<control type="image">
<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>