Connection manager, part 2
This commit is contained in:
parent
962ce6da1e
commit
7a9e0611ed
9 changed files with 200 additions and 456 deletions
|
@ -1158,12 +1158,12 @@ msgstr ""
|
||||||
|
|
||||||
# Server selection dialog: button text to sign in or sign out of plex.tv
|
# Server selection dialog: button text to sign in or sign out of plex.tv
|
||||||
msgctxt "#30600"
|
msgctxt "#30600"
|
||||||
msgid "Toggle plex.tv sign-in"
|
msgid "Sign-in to plex.tv"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# Server selection dialog: button text to add server manually
|
# Server selection dialog: button text to add server manually
|
||||||
msgctxt "#30601"
|
msgctxt "#30601"
|
||||||
msgid "Manually add server"
|
msgid "Manually add PMS"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# Button text, e.g. to cancel a dialog
|
# Button text, e.g. to cancel a dialog
|
||||||
|
|
|
@ -56,6 +56,7 @@ class Monitor_Window(Thread):
|
||||||
except:
|
except:
|
||||||
log.error('Failed to execute function %s with params %s'
|
log.error('Failed to execute function %s with params %s'
|
||||||
% (function, params))
|
% (function, params))
|
||||||
|
raise
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
thread_stopped = self.thread_stopped
|
thread_stopped = self.thread_stopped
|
||||||
|
|
|
@ -2,13 +2,21 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
|
from struct import pack
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
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
|
import credentials as cred
|
||||||
|
from utils import tryDecode
|
||||||
|
from PlexFunctions import PMSHttpsEnabled
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -35,6 +43,11 @@ CONNECTIONMODE = {
|
||||||
'Manual': 2
|
'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):
|
class ConnectionManager(object):
|
||||||
|
default_timeout = 30
|
||||||
default_timeout = 20
|
|
||||||
apiClients = []
|
apiClients = []
|
||||||
minServerVersion = "3.0.5930"
|
minServerVersion = "1.7.0.0"
|
||||||
connectUser = None
|
connectUser = None
|
||||||
|
# Token for plex.tv
|
||||||
|
plexToken = None
|
||||||
|
|
||||||
def __init__(self, appName, appVersion, deviceName, deviceId,
|
def __init__(self, appName, appVersion, deviceName, deviceId,
|
||||||
capabilities=None, devicePixelRatio=None):
|
capabilities=None, devicePixelRatio=None):
|
||||||
log.info("Begin ConnectionManager constructor")
|
log.debug("Instantiating")
|
||||||
|
|
||||||
self.credentialProvider = cred.Credentials()
|
self.credentialProvider = cred.Credentials()
|
||||||
self.appName = appName
|
self.appName = appName
|
||||||
|
@ -144,8 +158,7 @@ class ConnectionManager(object):
|
||||||
if server is None or systemInfo is None:
|
if server is None or systemInfo is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
server['Name'] = systemInfo['ServerName']
|
server['Id'] = systemInfo.attrib['machineIdentifier']
|
||||||
server['Id'] = systemInfo['Id']
|
|
||||||
|
|
||||||
if systemInfo.get('LocalAddress'):
|
if systemInfo.get('LocalAddress'):
|
||||||
server['LocalAddress'] = systemInfo['LocalAddress']
|
server['LocalAddress'] = systemInfo['LocalAddress']
|
||||||
|
@ -155,16 +168,11 @@ class ConnectionManager(object):
|
||||||
server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}]
|
server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}]
|
||||||
|
|
||||||
def _getHeaders(self, request):
|
def _getHeaders(self, request):
|
||||||
|
|
||||||
headers = request.setdefault('headers', {})
|
headers = request.setdefault('headers', {})
|
||||||
|
headers['Accept'] = '*/*'
|
||||||
if request.get('dataType') == "json":
|
headers['Content-type'] = request.get(
|
||||||
headers['Accept'] = "application/json"
|
'contentType',
|
||||||
request.pop('dataType')
|
"application/x-www-form-urlencoded")
|
||||||
|
|
||||||
headers['X-Application'] = self._addAppInfoToConnectRequest()
|
|
||||||
headers['Content-type'] = request.get('contentType',
|
|
||||||
'application/x-www-form-urlencoded; charset=UTF-8')
|
|
||||||
|
|
||||||
def requestUrl(self, request):
|
def requestUrl(self, request):
|
||||||
|
|
||||||
|
@ -192,9 +200,10 @@ class ConnectionManager(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return r.json()
|
return etree.fromstring(r.content)
|
||||||
except ValueError:
|
except etree.ParseError:
|
||||||
r.content # Read response to release connection
|
# Read response to release connection
|
||||||
|
r.content
|
||||||
return
|
return
|
||||||
|
|
||||||
def _requests(self, action, **kwargs):
|
def _requests(self, action, **kwargs):
|
||||||
|
@ -212,79 +221,68 @@ class ConnectionManager(object):
|
||||||
def getConnectUrl(self, handler):
|
def getConnectUrl(self, handler):
|
||||||
return "https://connect.emby.media/service/%s" % handler
|
return "https://connect.emby.media/service/%s" % handler
|
||||||
|
|
||||||
def _findServers(self, foundServers):
|
@staticmethod
|
||||||
|
def _findServers(foundServers):
|
||||||
servers = []
|
servers = []
|
||||||
|
for server in foundServers:
|
||||||
for foundServer in foundServers:
|
if '200 OK' not in server['data']:
|
||||||
|
continue
|
||||||
server = self._convertEndpointAddressToManualAddress(foundServer)
|
ip = server['from'][0]
|
||||||
|
info = {'LastCONNECTIONMODE': CONNECTIONMODE['Local']}
|
||||||
info = {
|
for line in server['data'].split('\n'):
|
||||||
'Id': foundServer['Id'],
|
if line.startswith('Name:'):
|
||||||
'LocalAddress': server or foundServer['Address'],
|
info['Name'] = tryDecode(line.split(':')[1].strip())
|
||||||
'Name': foundServer['Name']
|
elif line.startswith('Port:'):
|
||||||
}
|
info['Port'] = line.split(':')[1].strip()
|
||||||
info['LastCONNECTIONMODE'] = CONNECTIONMODE['Manual'] if info.get('ManualAddress') else CONNECTIONMODE['Local']
|
elif line.startswith('Resource-Identifier:'):
|
||||||
|
info['Id'] = line.split(':')[1].strip()
|
||||||
servers.append(info)
|
elif line.startswith('Updated-At:'):
|
||||||
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
|
pass
|
||||||
|
elif line.startswith('Version:'):
|
||||||
return None
|
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):
|
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)
|
# Set the time-to-live for messages to 2 for local network
|
||||||
MESSAGE = "who is EmbyServer?"
|
ttl = pack('b', 2)
|
||||||
|
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
sock.settimeout(1.0) # This controls the socket.timeout exception
|
|
||||||
|
|
||||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
|
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
||||||
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
|
|
||||||
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
|
|
||||||
|
|
||||||
log.debug("MultiGroup : %s" % str(MULTI_GROUP))
|
|
||||||
log.debug("Sending UDP Data: %s" % MESSAGE)
|
|
||||||
|
|
||||||
servers = []
|
servers = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock.sendto(MESSAGE, MULTI_GROUP)
|
# Send data to the multicast group
|
||||||
except Exception as error:
|
GDM.sendto(MSG_PLEXGDM, (IP_PLEXGDM, PORT_PLEXGDM))
|
||||||
log.error(error)
|
# Look for responses from all recipients
|
||||||
return servers
|
while True:
|
||||||
|
try:
|
||||||
while True:
|
data, server = GDM.recvfrom(1024)
|
||||||
try:
|
servers.append({'from': server, 'data': data})
|
||||||
data, addr = sock.recvfrom(1024) # buffer size
|
except socket.timeout:
|
||||||
servers.append(json.loads(data))
|
break
|
||||||
|
except:
|
||||||
except socket.timeout:
|
# Probably error: (101, 'Network is unreachable')
|
||||||
log.info("Found Servers: %s" % servers)
|
log.error('Could not find Plex servers using GDM')
|
||||||
return servers
|
import traceback
|
||||||
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
except Exception as e:
|
finally:
|
||||||
log.error("Error trying to find servers: %s" % e)
|
GDM.close()
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
def _normalizeAddress(self, address):
|
def _normalizeAddress(self, address):
|
||||||
# Attempt to correct bad input
|
# Attempt to correct bad input
|
||||||
|
@ -345,15 +343,11 @@ class ConnectionManager(object):
|
||||||
self.credentialProvider.getCredentials(credentials)
|
self.credentialProvider.getCredentials(credentials)
|
||||||
|
|
||||||
def _tryConnect(self, url, timeout=None, options={}):
|
def _tryConnect(self, url, timeout=None, options={}):
|
||||||
|
url = '%s/identity' % url
|
||||||
url = self.getEmbyServerUrl(url, "system/info/public")
|
log.debug("tryConnect url: %s" % url)
|
||||||
log.info("tryConnect url: %s" % url)
|
|
||||||
|
|
||||||
return self.requestUrl({
|
return self.requestUrl({
|
||||||
|
|
||||||
'type': "GET",
|
'type': "GET",
|
||||||
'url': url,
|
'url': url,
|
||||||
'dataType': "json",
|
|
||||||
'timeout': timeout,
|
'timeout': timeout,
|
||||||
'ssl': options.get('ssl')
|
'ssl': options.get('ssl')
|
||||||
})
|
})
|
||||||
|
@ -361,6 +355,61 @@ class ConnectionManager(object):
|
||||||
def _addAppInfoToConnectRequest(self):
|
def _addAppInfoToConnectRequest(self):
|
||||||
return "%s/%s" % (self.appName, self.appVersion)
|
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):
|
def _getConnectServers(self, credentials):
|
||||||
|
|
||||||
log.info("Begin getConnectServers")
|
log.info("Begin getConnectServers")
|
||||||
|
@ -375,7 +424,6 @@ class ConnectionManager(object):
|
||||||
|
|
||||||
'type': "GET",
|
'type': "GET",
|
||||||
'url': url,
|
'url': url,
|
||||||
'dataType': "json",
|
|
||||||
'headers': {
|
'headers': {
|
||||||
'X-Connect-UserToken': credentials['ConnectAccessToken']
|
'X-Connect-UserToken': credentials['ConnectAccessToken']
|
||||||
}
|
}
|
||||||
|
@ -396,18 +444,16 @@ class ConnectionManager(object):
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
def getAvailableServers(self):
|
def getAvailableServers(self):
|
||||||
|
|
||||||
log.info("Begin getAvailableServers")
|
log.info("Begin getAvailableServers")
|
||||||
|
|
||||||
# Clone the array
|
|
||||||
credentials = self.credentialProvider.getCredentials()
|
credentials = self.credentialProvider.getCredentials()
|
||||||
|
|
||||||
connectServers = self._getConnectServers(credentials)
|
|
||||||
foundServers = self._findServers(self._serverDiscovery())
|
|
||||||
|
|
||||||
servers = list(credentials['Servers'])
|
servers = list(credentials['Servers'])
|
||||||
|
|
||||||
|
if self.plexToken:
|
||||||
|
connectServers = self.__get_PMS_servers_from_plex_tv()
|
||||||
|
self._mergeServers(servers, connectServers)
|
||||||
|
foundServers = self._findServers(self._serverDiscovery())
|
||||||
self._mergeServers(servers, foundServers)
|
self._mergeServers(servers, foundServers)
|
||||||
self._mergeServers(servers, connectServers)
|
|
||||||
|
|
||||||
servers = self._filterServers(servers, connectServers)
|
servers = self._filterServers(servers, connectServers)
|
||||||
|
|
||||||
|
@ -550,8 +596,9 @@ class ConnectionManager(object):
|
||||||
return self._testNextCONNECTIONMODE(tests, index+1, server, options)
|
return self._testNextCONNECTIONMODE(tests, index+1, server, options)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
if self._compareVersions(self._getMinServerVersion(), result['Version']) == 1:
|
if self._compareVersions(self._getMinServerVersion(),
|
||||||
log.warn("minServerVersion requirement not met. Server version: %s" % result['Version'])
|
result.attrib['version']) == 1:
|
||||||
|
log.warn("minServerVersion requirement not met. Server version: %s" % result.attrib['version'])
|
||||||
return {
|
return {
|
||||||
'State': CONNECTIONSTATE['ServerUpdateNeeded'],
|
'State': CONNECTIONSTATE['ServerUpdateNeeded'],
|
||||||
'Servers': [server]
|
'Servers': [server]
|
||||||
|
@ -616,7 +663,6 @@ class ConnectionManager(object):
|
||||||
'type': "GET",
|
'type': "GET",
|
||||||
'url': self.getEmbyServerUrl(url, "System/Info"),
|
'url': self.getEmbyServerUrl(url, "System/Info"),
|
||||||
'ssl': options.get('ssl'),
|
'ssl': options.get('ssl'),
|
||||||
'dataType': "json",
|
|
||||||
'headers': {
|
'headers': {
|
||||||
'X-MediaBrowser-Token': server['AccessToken']
|
'X-MediaBrowser-Token': server['AccessToken']
|
||||||
}
|
}
|
||||||
|
@ -631,7 +677,6 @@ class ConnectionManager(object):
|
||||||
'type': "GET",
|
'type': "GET",
|
||||||
'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']),
|
'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']),
|
||||||
'ssl': options.get('ssl'),
|
'ssl': options.get('ssl'),
|
||||||
'dataType': "json",
|
|
||||||
'headers': {
|
'headers': {
|
||||||
'X-MediaBrowser-Token': server['AccessToken']
|
'X-MediaBrowser-Token': server['AccessToken']
|
||||||
}
|
}
|
||||||
|
@ -658,7 +703,6 @@ class ConnectionManager(object):
|
||||||
'nameOrEmail': username,
|
'nameOrEmail': username,
|
||||||
'password': md5
|
'password': md5
|
||||||
},
|
},
|
||||||
'dataType': "json"
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result = self.requestUrl(request)
|
result = self.requestUrl(request)
|
||||||
|
@ -695,7 +739,6 @@ class ConnectionManager(object):
|
||||||
|
|
||||||
'type': "GET",
|
'type': "GET",
|
||||||
'url': url,
|
'url': url,
|
||||||
'dataType': "json",
|
|
||||||
'headers': {
|
'headers': {
|
||||||
'X-Connect-UserToken': accessToken
|
'X-Connect-UserToken': accessToken
|
||||||
}
|
}
|
||||||
|
@ -718,7 +761,6 @@ class ConnectionManager(object):
|
||||||
|
|
||||||
'url': url,
|
'url': url,
|
||||||
'type': "GET",
|
'type': "GET",
|
||||||
'dataType': "json",
|
|
||||||
'ssl': options.get('ssl'),
|
'ssl': options.get('ssl'),
|
||||||
'params': {
|
'params': {
|
||||||
'ConnectUserId': credentials['ConnectUserId']
|
'ConnectUserId': credentials['ConnectUserId']
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from copy import deepcopy
|
||||||
|
from os import makedirs
|
||||||
|
|
||||||
|
from xbmc import getIPAddress
|
||||||
|
|
||||||
# from connect.connectionmanager import ConnectionManager
|
# from connect.connectionmanager import ConnectionManager
|
||||||
from downloadutils import DownloadUtils
|
from downloadutils import DownloadUtils
|
||||||
from dialogs.serverconnect import ServerConnect
|
from dialogs.serverconnect import ServerConnect
|
||||||
from dialogs.servermanual import ServerManual
|
from dialogs.servermanual import ServerManual
|
||||||
from connect.plex_tv import plex_tv_sign_in_with_pin
|
from connect.plex_tv import plex_tv_sign_in_with_pin
|
||||||
|
import connect.connectionmanager as connectionmanager
|
||||||
from userclient import UserClient
|
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, \
|
from PlexFunctions import GetMachineIdentifier, get_pms_settings, \
|
||||||
check_connection
|
check_connection
|
||||||
import variables as v
|
import variables as v
|
||||||
|
@ -18,6 +24,7 @@ import state
|
||||||
|
|
||||||
log = getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
STATE = connectionmanager.CONNECTIONSTATE
|
||||||
XML_PATH = (tryEncode(v.ADDON_PATH), "default", "1080i")
|
XML_PATH = (tryEncode(v.ADDON_PATH), "default", "1080i")
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -55,6 +62,7 @@ def get_plex_login_from_settings():
|
||||||
class ConnectManager(object):
|
class ConnectManager(object):
|
||||||
# Borg
|
# Borg
|
||||||
__shared_state = {}
|
__shared_state = {}
|
||||||
|
state = {}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Borg
|
# Borg
|
||||||
|
@ -68,19 +76,36 @@ class ConnectManager(object):
|
||||||
plexdict = get_plex_login_from_settings()
|
plexdict = get_plex_login_from_settings()
|
||||||
self.myplexlogin = plexdict['myplexlogin'] == 'true'
|
self.myplexlogin = plexdict['myplexlogin'] == 'true'
|
||||||
self.plexLogin = plexdict['plexLogin']
|
self.plexLogin = plexdict['plexLogin']
|
||||||
self.plexToken = plexdict['plexToken']
|
|
||||||
self.plexid = plexdict['plexid']
|
self.plexid = plexdict['plexid']
|
||||||
# Token for the PMS, not plex.tv
|
# 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.pms_token = settings('accessToken')
|
||||||
|
self.plexToken = plexdict['plexToken']
|
||||||
|
self.__connect.plexToken = self.plexToken
|
||||||
if self.plexToken:
|
if self.plexToken:
|
||||||
log.debug('Found a plex.tv token in the settings')
|
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):
|
def update_state(self):
|
||||||
self.state = self.__connect.connect({'updateDateLastAccessed': False})
|
self.state = self.__connect.connect({'updateDateLastAccessed': False})
|
||||||
return self.get_state()
|
return self.get_state()
|
||||||
|
|
||||||
def get_sate(self):
|
def get_state(self):
|
||||||
window('emby_state.json', value=self.state)
|
state.CONNECT_STATE = deepcopy(self.state)
|
||||||
return self.state
|
return self.state
|
||||||
|
|
||||||
def get_server(self, server, options={}):
|
def get_server(self, server, options={}):
|
||||||
|
@ -98,13 +123,14 @@ class ConnectManager(object):
|
||||||
"""
|
"""
|
||||||
Will return selected server or raise RuntimeError
|
Will return selected server or raise RuntimeError
|
||||||
"""
|
"""
|
||||||
|
status = self.__connect.connect({'enableAutoLogin': False})
|
||||||
dia = ServerConnect("script-plex-connect-server.xml", *XML_PATH)
|
dia = ServerConnect("script-plex-connect-server.xml", *XML_PATH)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'connect_manager': None, # self._connect
|
'connect_manager': self.__connect,
|
||||||
'username': state.PLEX_USERNAME,
|
'username': state.PLEX_USERNAME,
|
||||||
'user_image': state.PLEX_USER_IMAGE,
|
'user_image': state.PLEX_USER_IMAGE,
|
||||||
# 'servers': state.get('Servers') or [],
|
'servers': status.get('Servers') or [],
|
||||||
# 'emby_connect': False if user else True
|
'plex_connect': False if status.get('ConnectUser') else True
|
||||||
}
|
}
|
||||||
dia.set_args(**kwargs)
|
dia.set_args(**kwargs)
|
||||||
dia.doModal()
|
dia.doModal()
|
||||||
|
@ -113,12 +139,9 @@ class ConnectManager(object):
|
||||||
log.debug("Server selected")
|
log.debug("Server selected")
|
||||||
return dia.get_server()
|
return dia.get_server()
|
||||||
|
|
||||||
elif dia._is_connect_login():
|
elif dia.is_connect_login():
|
||||||
log.debug("Login to plex.tv")
|
log.debug("Login to plex.tv")
|
||||||
try:
|
self.plex_tv_signin()
|
||||||
self._login_connect()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
return self.select_servers()
|
return self.select_servers()
|
||||||
|
|
||||||
elif dia.is_manual_server():
|
elif dia.is_manual_server():
|
||||||
|
@ -134,7 +157,7 @@ class ConnectManager(object):
|
||||||
def manual_server(self):
|
def manual_server(self):
|
||||||
# Return server or raise error
|
# Return server or raise error
|
||||||
dia = ServerManual("script-plex-connect-server-manual.xml", *XML_PATH)
|
dia = ServerManual("script-plex-connect-server-manual.xml", *XML_PATH)
|
||||||
dia._set_connect_manager(self.__connect)
|
dia.set_connect_manager(self.__connect)
|
||||||
dia.doModal()
|
dia.doModal()
|
||||||
|
|
||||||
if dia._is_connected():
|
if dia._is_connected():
|
||||||
|
@ -142,19 +165,6 @@ class ConnectManager(object):
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Server is not connected")
|
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):
|
def login(self, server=None):
|
||||||
# Return user or raise error
|
# Return user or raise error
|
||||||
server = server or self.state['Servers'][0]
|
server = server or self.state['Servers'][0]
|
||||||
|
@ -233,7 +243,7 @@ class ConnectManager(object):
|
||||||
# Update the token in data.txt
|
# Update the token in data.txt
|
||||||
self.__connect.credentialProvider.getCredentials(credentials)
|
self.__connect.credentialProvider.getCredentials(credentials)
|
||||||
|
|
||||||
def _get_connect_servers(self):
|
def get_connect_servers(self):
|
||||||
|
|
||||||
connect_servers = []
|
connect_servers = []
|
||||||
servers = self.__connect.getAvailableServers()
|
servers = self.__connect.getAvailableServers()
|
||||||
|
@ -580,8 +590,7 @@ class ConnectManager(object):
|
||||||
"""
|
"""
|
||||||
Returns a list of servers from GDM and possibly plex.tv
|
Returns a list of servers from GDM and possibly plex.tv
|
||||||
"""
|
"""
|
||||||
self.discoverPMS(xbmc.getIPAddress(),
|
self.discoverPMS(getIPAddress(), plexToken=self.plexToken)
|
||||||
plexToken=self.plexToken)
|
|
||||||
serverlist = self.plx.returnServerList(self.plx.g_PMS)
|
serverlist = self.plx.returnServerList(self.plx.g_PMS)
|
||||||
log.debug('PMS serverlist: %s' % serverlist)
|
log.debug('PMS serverlist: %s' % serverlist)
|
||||||
return serverlist
|
return serverlist
|
||||||
|
|
|
@ -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')
|
|
|
@ -7,12 +7,14 @@ from logging import getLogger
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
|
||||||
|
import connect.connectionmanager as connectionmanager
|
||||||
from utils import language as lang
|
from utils import language as lang
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
CONN_STATE = connectionmanager.CONNECTIONSTATE
|
||||||
ACTION_PARENT_DIR = 9
|
ACTION_PARENT_DIR = 9
|
||||||
ACTION_PREVIOUS_MENU = 10
|
ACTION_PREVIOUS_MENU = 10
|
||||||
ACTION_BACK = 92
|
ACTION_BACK = 92
|
||||||
|
@ -25,7 +27,7 @@ CANCEL = 201
|
||||||
MESSAGE_BOX = 202
|
MESSAGE_BOX = 202
|
||||||
MESSAGE = 203
|
MESSAGE = 203
|
||||||
BUSY = 204
|
BUSY = 204
|
||||||
EMBY_CONNECT = 205
|
PLEX_CONNECT = 205
|
||||||
MANUAL_SERVER = 206
|
MANUAL_SERVER = 206
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -42,7 +44,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||||
_manual_server = False
|
_manual_server = False
|
||||||
|
|
||||||
def set_args(self, **kwargs):
|
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():
|
for key, value in kwargs.iteritems():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
@ -74,8 +76,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||||
if self.user_image is not None:
|
if self.user_image is not None:
|
||||||
self.getControl(USER_IMAGE).setImage(self.user_image)
|
self.getControl(USER_IMAGE).setImage(self.user_image)
|
||||||
|
|
||||||
if not self.emby_connect: # Change connect user
|
if not self.plex_connect: # Change connect user
|
||||||
self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+'plex.tv user change'+"[/B][/UPPERCASE]")
|
self.getControl(PLEX_CONNECT).setLabel("[UPPERCASE][B]"+'plex.tv user change'+"[/B][/UPPERCASE]")
|
||||||
|
|
||||||
if self.servers:
|
if self.servers:
|
||||||
self.setFocus(self.list_)
|
self.setFocus(self.list_)
|
||||||
|
@ -107,7 +109,7 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||||
|
|
||||||
def onClick(self, control):
|
def onClick(self, control):
|
||||||
|
|
||||||
if control == EMBY_CONNECT:
|
if control == PLEX_CONNECT:
|
||||||
self.connect_manager.clearData()
|
self.connect_manager.clearData()
|
||||||
self._connect_login = True
|
self._connect_login = True
|
||||||
self.close()
|
self.close()
|
||||||
|
|
|
@ -38,3 +38,6 @@ PLEX_USER_ID = None
|
||||||
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
|
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
|
||||||
# another user playing something! Token identifies user
|
# another user playing something! Token identifies user
|
||||||
PLEX_TRANSIENT_TOKEN = None
|
PLEX_TRANSIENT_TOKEN = None
|
||||||
|
|
||||||
|
# Used by connectmanager.py
|
||||||
|
CONNECT_STATE = {}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcaddon import Addon
|
from xbmcaddon import Addon
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
# Paths are in unicode, otherwise Windows will throw fits
|
# Paths are in unicode, otherwise Windows will throw fits
|
||||||
# For any file operations with KODI function, use encoded strings!
|
# 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])
|
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||||
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
||||||
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
|
KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile"))
|
||||||
|
ADDON_PATH_DATA = join(KODI_PROFILE, 'addon_data', ADDON_ID, '')
|
||||||
|
|
||||||
if xbmc.getCondVisibility('system.platform.osx'):
|
if xbmc.getCondVisibility('system.platform.osx'):
|
||||||
PLATFORM = "MacOSX"
|
PLATFORM = "MacOSX"
|
||||||
|
|
|
@ -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>
|
|
Loading…
Reference in a new issue