First attempt at server select dialog

This commit is contained in:
croneter 2018-09-30 13:16:48 +02:00
parent 32927931c4
commit 59040f3b3e
6 changed files with 599 additions and 72 deletions

View file

@ -11,9 +11,8 @@ from threading import Thread
from xbmc import sleep from xbmc import sleep
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from .plexapi.base import PlexServer, Connection
from . import plex_tv from . import utils, plex_tv, variables as v
from . import variables as v
############################################################################### ###############################################################################
LOG = getLogger('PLEX.plex_functions') LOG = getLogger('PLEX.plex_functions')
@ -193,7 +192,7 @@ def discover_pms(token=None):
""" """
LOG.info('Start discovery of Plex Media Servers') LOG.info('Start discovery of Plex Media Servers')
# Look first for local PMS in the LAN # Look first for local PMS in the LAN
local_pms_list = _plex_gdm() local_pms_list = plex_gdm()
LOG.debug('PMS found in the local LAN using Plex GDM: %s', local_pms_list) LOG.debug('PMS found in the local LAN using Plex GDM: %s', local_pms_list)
# Get PMS from plex.tv # Get PMS from plex.tv
if token: if token:
@ -236,7 +235,7 @@ def _log_pms(pms_list):
LOG.debug('Found the following PMS: %s', log_list) LOG.debug('Found the following PMS: %s', log_list)
def _plex_gdm(): def plex_gdm():
""" """
PlexGDM - looks for PMS in the local LAN and returns a list of the PMS found PlexGDM - looks for PMS in the local LAN and returns a list of the PMS found
""" """
@ -278,44 +277,54 @@ def _plex_gdm():
# Check if we had a positive HTTP response # Check if we had a positive HTTP response
if '200 OK' not in response['data']: if '200 OK' not in response['data']:
continue continue
pms = { connection = Connection(local=True)
'ip': response['from'][0], # Local LAN IP from GDM
'scheme': None, connection.address = response['from'][0]
'local': True, # Since we found it using GDM pms = PlexServer()
'product': None, pms.presence = True
'baseURL': None, pms.connections.append(connection)
'name': None,
'version': None,
'token': None,
'ownername': None,
'device': None,
'platform': None,
'owned': None,
'relay': None,
'presence': True, # Since we're talking to the PMS
'httpsRequired': None,
}
for line in response['data'].split('\n'): for line in response['data'].split('\n'):
if 'Content-Type:' in line: try:
pms['product'] = utils.try_decode(line.split(':')[1].strip()) kind, info = line.split(':', 1)
elif 'Host:' in line: except ValueError:
pms['baseURL'] = line.split(':')[1].strip() continue
elif 'Name:' in line: else:
pms['name'] = utils.try_decode(line.split(':')[1].strip()) kind, info = kind.strip(), info.strip()
elif 'Port:' in line: if kind == 'Name':
pms['port'] = line.split(':')[1].strip() pms.name = info
elif 'Resource-Identifier:' in line: elif kind == 'Resource-Identifier':
pms['machineIdentifier'] = line.split(':')[1].strip() pms.clientIdentifier = info
elif 'Version:' in line: elif kind == 'Content-Type':
pms['version'] = line.split(':')[1].strip() pms.product = info
elif kind == 'Version':
pms.productVersion = info
elif kind == 'Updated-At':
pms.lastSeenAt = int(info)
elif kind == 'Host':
# Example: "Host: <....sfe...>.plex.direct"
connection.uri = info
elif kind == 'Port':
connection.port = int(info)
# Assume https
connection.protocol = 'https'
if connection.uri != connection.address:
# The PMS might return both local IP and plex.direct address
alt_connection = deepcopy(connection)
alt_connection.uri = 'https://%s:%s' % (connection.address,
connection.port)
pms.connections.append(alt_connection)
connection.uri = 'https://%s:%s' % (connection.uri,
connection.port)
pms_list.append(pms) pms_list.append(pms)
LOG.debug('Found PMS in the LAN: %s: %s', pms, pms.connections)
return pms_list return pms_list
def _pms_list_from_plex_tv(token): def pms_from_plex_tv(token):
""" """
get Plex media Server List from plex.tv/pms/resources get Plex media Server List from plex.tv/pms/resources
""" """
pms_list = []
xml = DU().downloadUrl('https://plex.tv/api/resources', xml = DU().downloadUrl('https://plex.tv/api/resources',
authenticate=False, authenticate=False,
parameters={'includeHttps': 1}, parameters={'includeHttps': 1},
@ -324,49 +333,20 @@ def _pms_list_from_plex_tv(token):
xml.attrib xml.attrib
except AttributeError: except AttributeError:
LOG.error('Could not get list of PMS from plex.tv') LOG.error('Could not get list of PMS from plex.tv')
return [] return pms_list
from Queue import Queue
queue = Queue()
thread_queue = []
max_age_in_seconds = 2 * 60 * 60 * 24
for device in xml.findall('Device'): for device in xml.findall('Device'):
if 'server' not in device.get('provides'): if 'server' not in device.get('provides', ''):
# No PMS - skip # No PMS - skip
continue continue
if device.find('Connection') is None: if device.find('Connection') is None:
# no valid connection - skip # no valid connection - skip
continue continue
# check MyPlex data age - skip if >2 days pms = PlexServer(xml=device)
info_age = time() - int(device.get('lastSeenAt')) pms_list.append(pms)
if info_age > max_age_in_seconds: return pms_list
LOG.debug("Skip server %s not seen for 2 days", device.get('name'))
continue
pms = {
'machineIdentifier': device.get('clientIdentifier'),
'name': device.get('name'),
'token': device.get('accessToken'),
'ownername': device.get('sourceTitle'),
'product': device.get('product'), # e.g. 'Plex Media Server'
'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e..'
'device': device.get('device'), # e.g. 'PC' or 'Windows'
'platform': device.get('platform'), # e.g. 'Windows', 'Android'
'local': device.get('publicAddressMatches') == '1',
'owned': device.get('owned') == '1',
'relay': device.get('relay') == '1',
'presence': device.get('presence') == '1',
'httpsRequired': device.get('httpsRequired') == '1',
'connections': []
}
# Try a local connection first, no matter what plex.tv tells us
for connection in device.findall('Connection'):
if connection.get('local') == '1':
pms['connections'].append(connection)
# Then try non-local
for connection in device.findall('Connection'):
if connection.get('local') != '1':
pms['connections'].append(connection)
# Spawn threads to ping each PMS simultaneously # Spawn threads to ping each PMS simultaneously
thread = Thread(target=_poke_pms, args=(pms, queue)) thread = Thread(target=_poke_pms, args=(pms, queue))
thread_queue.append(thread) thread_queue.append(thread)

View file

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

View file

@ -0,0 +1,126 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from ..utils import cast, to_list
LOG = getLogger('PLEX.' + __name__)
class Connection(object):
def __init__(self, xml=None, **kwargs):
self.protocol = None
self.address = None
self.port = None
self.uri = None
self.local = None
if xml:
self.load_from_xml(xml)
# Set all remaining attributes set on Class instantiation
for key, value in kwargs.items():
setattr(self, key, value)
def __unicode__(self):
return '<Connection {self.uri}>'.format(self=self)
def __repr__(self):
return self.__unicode__().encode('utf-8')
def __eq__(self, other):
return self.uri == other.uri
def __ne__(self, other):
return self.uri != other.uri
def load_from_xml(self, xml):
"""
Throw in an etree xml-element to load PMS settings from it
"""
if xml.tag != 'Connection':
raise RuntimeError('Did not receive Connection xml but %s'
% xml.tag)
self.protocol = cast(unicode, xml.get('protocol'))
self.address = cast(unicode, xml.get('address'))
self.port = cast(int, xml.get('port'))
self.uri = cast(unicode, xml.get('uri'))
self.local = cast(bool, xml.get('local'))
class PlexServer(object):
def __init__(self, xml=None, **kwargs):
# Information from plex.tv
self.name = None
self.clientIdentifier = None
self.provides = set()
self.owned = None
self.home = None
self.httpsRequired = None
self.synced = None
self.relay = None
self.publicAddressMatches = None
self.presence = None
self.accessToken = None
self.product = None # Usually "Plex Media Server"
self.ownerId = None # User id of the owner of this PMS
self.owner = None # User name of the (foreign!) owner
self.productVersion = None
self.platform = None
self.platformVersion = None
self.device = None
self.createdAt = None
self.lastSeenAt = None
# Connection info
self.connections = []
if xml:
self.load_from_xml(xml)
# Set all remaining attributes set on Class instantiation
for key, value in kwargs.items():
setattr(self, key, value)
def load_from_xml(self, xml):
"""
Throw in an etree xml-element to load PMS settings from it
"""
if xml.tag != 'Device':
raise RuntimeError('Did not receive Device xml but %s' % xml.tag)
self.name = cast(unicode, xml.get('name'))
self.clientIdentifier = cast(unicode, xml.get('clientIdentifier'))
self.provides = set(to_list(cast(unicode, xml.get('provides'))))
self.owned = cast(bool, xml.get('owned'))
self.home = cast(bool, xml.get('home'))
self.httpsRequired = cast(bool, xml.get('httpsRequired'))
self.synced = cast(bool, xml.get('synced'))
self.relay = cast(bool, xml.get('relay'))
self.publicAddressMatches = cast(bool,
xml.get('publicAddressMatches'))
self.presence = cast(bool, xml.get('presence'))
self.accessToken = cast(unicode, xml.get('accessToken'))
self.product = cast(unicode, xml.get('product'))
self.ownerId = cast(int, xml.get('ownerId'))
self.owner = cast(unicode, xml.get('sourceTitle'))
self.productVersion = cast(unicode, xml.get('productVersion'))
self.platform = cast(unicode, xml.get('platform'))
self.platformVersion = cast(unicode, xml.get('platformVersion'))
self.device = cast(unicode, xml.get('device'))
self.createdAt = cast(int, xml.get('createdAt'))
self.lastSeenAt = cast(int, xml.get('lastSeenAt'))
for connection in xml.findall('Connection'):
self.connections.append(Connection(xml=connection))
def __unicode__(self):
return '<PlexServer {self.name}:{self.clientIdentifier}>'.format(self=self)
def __repr__(self):
return self.__unicode__().encode('utf-8')
def __eq__(self, other):
return self.clientIdentifier == other.clientIdentifier
def __ne__(self, other):
return self.clientIdentifier != other.clientIdentifier

View file

@ -249,6 +249,46 @@ def ERROR(txt='', hide_tb=False, notify=False):
return short return short
def to_list(value, itemcast=None, delim=','):
"""
Returns a list of unicodes from the specified value [unicode].
Parameters:
value [unicode]: (comma) delimited string to convert to list.
itemcast (func): Function to cast each list item to (default unicode).
delim (str): string delimiter (optional; default ',').
"""
value = value or ''
itemcast = itemcast or unicode
return [itemcast(item) for item in value.split(delim) if item != '']
def cast(func, value):
"""
Cast the specified value to the specified type (returned by func).
Currently supports int, float, bool, unicode (if str supplied), str
Will return None if value=None
Parameters:
func (func): Calback function to used cast to type
value (any): value to be cast and returned.
"""
if value is None:
return value
if func == bool:
return bool(int(value))
elif func in (int, float):
try:
return func(value)
except ValueError:
return float('nan')
elif func == unicode:
return value.decode('utf-8')
elif func == str:
return value.encode('utf-8')
return func(value)
class AttributeDict(dict): class AttributeDict(dict):
""" """
Turns an etree xml response's xml.attrib into an object with attributes Turns an etree xml response's xml.attrib into an object with attributes

View file

@ -0,0 +1,224 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
:module: plexkodiconnect.userselect
:synopsis: This module shows a dialog to let one choose between different Plex
(home) users
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
import xbmcgui
from . import kodigui
from .connection import plexapp
from .. import backgroundthread, utils, plex_functions as PF, variables as v
LOG = getLogger('PLEX.' + __name__)
class UserThumbTask(backgroundthread.Task):
def setup(self, users, callback):
self.users = users
self.callback = callback
return self
def run(self):
for user in self.users:
if self.isCanceled():
return
thumb, back = user.thumb, ''
self.callback(user, thumb, back)
class ServerListItem(kodigui.ManagedListItem):
def init(self):
self.dataSource.on('completed:reachability', self.onUpdate)
self.dataSource.on('started:reachability', self.onUpdate)
return self
def safeSetProperty(self, key, value):
# For if we catch the item in the middle of being removed
try:
self.setProperty(key, value)
return True
except AttributeError:
return False
def safeSetLabel(self, value):
# For if we catch the item in the middle of being removed
try:
self.setLabel(value)
return True
except AttributeError:
return False
def onUpdate(self, **kwargs):
if not self.listItem: # ex. can happen on Kodi shutdown
return
if not self.dataSource.isSupported or not self.dataSource.isReachable():
if self.dataSource.pendingReachabilityRequests > 0:
self.safeSetProperty('status', 'refreshing.gif')
else:
self.safeSetProperty('status', 'unreachable.png')
else:
self.safeSetProperty('status', self.dataSource.isSecure and 'secure.png' or '')
self.safeSetProperty('current', plexapp.SERVERMANAGER.selectedServer == self.dataSource and '1' or '')
self.safeSetLabel(self.dataSource.name)
def onDestroy(self):
self.dataSource.off('completed:reachability', self.onUpdate)
self.dataSource.off('started:reachability', self.onUpdate)
class ServerSelectWindow(kodigui.BaseWindow):
xmlFile = 'script-plex-server_select.xml'
path = v.ADDON_PATH
theme = 'Main'
res = '1080i'
width = 1920
height = 1080
USER_LIST_ID = 101
PIN_ENTRY_GROUP_ID = 400
HOME_BUTTON_ID = 500
SERVER_LIST_ID = 260
def __init__(self, *args, **kwargs):
self.tasks = None
self.server = None
self.aborted = False
self.serverList = None
kodigui.BaseWindow.__init__(self, *args, **kwargs)
def onFirstInit(self):
self.serverList = kodigui.ManagedControlList(self,
self.SERVER_LIST_ID,
10)
self.start()
def onAction(self, action):
try:
ID = action.getId()
if 57 < ID < 68:
if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)):
item = self.userList.getSelectedItem()
if not item.dataSource.isProtected:
return
self.setFocusId(self.PIN_ENTRY_GROUP_ID)
self.pinEntryClicked(ID + 142)
return
elif 142 <= ID <= 149: # JumpSMS action
if not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)):
item = self.userList.getSelectedItem()
if not item.dataSource.isProtected:
return
self.setFocusId(self.PIN_ENTRY_GROUP_ID)
self.pinEntryClicked(ID + 60)
return
elif ID in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_BACKSPACE):
if xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.PIN_ENTRY_GROUP_ID)):
self.pinEntryClicked(211)
return
except:
utils.ERROR()
kodigui.BaseWindow.onAction(self, action)
def onClick(self, controlID):
if controlID == self.USER_LIST_ID:
item = self.userList.getSelectedItem()
if item.dataSource.isProtected:
self.setFocusId(self.PIN_ENTRY_GROUP_ID)
else:
self.userSelected(item)
elif 200 < controlID < 212:
self.pinEntryClicked(controlID)
elif controlID == self.HOME_BUTTON_ID:
self.home_button_clicked()
def onFocus(self, controlID):
if controlID == self.USER_LIST_ID:
item = self.userList.getSelectedItem()
item.setProperty('editing.pin', '')
def showServers(self, from_refresh=False, mouse=False):
selection = None
if from_refresh:
mli = self.serverList.getSelectedItem()
if mli:
selection = mli.dataSource
else:
plexapp.refreshResources()
servers = sorted(
plexapp.SERVERMANAGER.getServers(),
key=lambda x: (x.owned and '0' or '1') + x.name.lower()
)
servers = PF.plex_gdm()
items = []
for s in servers:
item = ServerListItem(s.name,
not s.owned and s.owner or '',
data_source=s).init()
item.onUpdate()
item.setProperty(
'current',
plexapp.SERVERMANAGER.selectedServer == s and '1' or '')
items.append(item)
if len(items) > 1:
items[0].setProperty('first', '1')
items[-1].setProperty('last', '1')
elif items:
items[0].setProperty('only', '1')
self.serverList.replaceItems(items)
self.getControl(800).setHeight((min(len(items), 9) * 100) + 80)
if selection:
for mli in self.serverList:
if mli.dataSource == selection:
self.serverList.selectItem(mli.pos())
if not from_refresh and items and not mouse:
self.setFocusId(self.SERVER_LIST_ID)
def start(self):
self.setProperty('busy', '1')
self.showServers()
self.setProperty('initialized', '1')
self.setProperty('busy', '')
def home_button_clicked(self):
"""
Action taken if user clicked the home button
"""
self.server = None
self.aborted = True
self.doClose()
def finished(self):
for task in self.tasks:
task.cancel()
def start():
"""
Hit this function to open a dialog to choose the Plex user
Returns
=======
tuple (server, aborted)
server : PlexServer
Or None if server switch failed or aborted by the server
aborted : bool
True if the server cancelled the dialog
"""
w = ServerSelectWindow.open()
server, aborted = w.server, w.aborted
del w
return server, aborted

View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<window>
<defaultcontrol>100</defaultcontrol>
<coordinates>
<system>1</system>
<posx>0</posx>
<posy>0</posy>
</coordinates>
<backgroundcolor>0xff111111</backgroundcolor>
<controls>
<!-- Background -->
<control type="group">
<control type="image">
<posx>0</posx>
<posy>0</posy>
<width>1920</width>
<height>1080</height>
<texture colordiffuse="80FFFFFF">script.plex/home/background-fallback.png</texture>
</control>
<control type="image">
<posx>0</posx>
<posy>0</posy>
<width>1920</width>
<height>1080</height>
<fadetime>1000</fadetime>
<texture background="true">$INFO[Window.Property(background)]</texture>
</control>
</control>
<!-- Top bar -->
<control type="group">
<defaultcontrol always="true">201</defaultcontrol>
<posx>0</posx>
<posy>0</posy>
<width>1920</width>
<height>135</height>
<control type="image">
<posx>0</posx>
<posy>0</posy>
<width>1920</width>
<height>135</height>
<texture>plugin.video.plexkodiconnect/white-square.png</texture>
<colordiffuse>19000000</colordiffuse>
</control>
<control type="grouplist">
<posx>20</posx>
<posy>-5.5</posy>
<width>1040</width>
<height>170</height>
<align>left</align>
<itemgap>60</itemgap>
<orientation>horizontal</orientation>
<ondown>101</ondown>
<usecontrolcoords>true</usecontrolcoords>
<control type="group">
<posx>40</posx>
<posy>34.5</posy>
<width>124</width>
<height>66</height>
<control type="button" id="500">
<posx>0</posx>
<posy>0</posy>
<width>124</width>
<height>66</height>
<ondown>101</ondown>
<onright>101</onright>
<align>right</align>
<aligny>center</aligny>
<texturefocus>-</texturefocus>
<texturenofocus>-</texturenofocus>
<label> </label>
</control>
<control type="image">
<visible>!String.IsEmpty(Window.Property(dropdown))</visible>
<posx>0</posx>
<posy>0</posy>
<width>124</width>
<height>66</height>
<texture colordiffuse="FFCC7B19" border="10">plugin.video.plexkodiconnect/white-square-rounded.png</texture>
</control>
<control type="group">
<posx>27</posx>
<posy>13</posy>
<control type="image">
<visible>!Control.HasFocus(500) + String.IsEmpty(Window.Property(dropdown))</visible>
<posx>0</posx>
<posy>0</posy>
<width>90</width>
<height>90</height>
<texture>plugin.video.plexkodiconnect/home/type/home.png</texture>
</control>
<control type="image">
<visible>Control.HasFocus(500) | !String.IsEmpty(Window.Property(dropdown))</visible>
<posx>-40</posx>
<posy>-40</posy>
<width>170</width>
<height>170</height>
<texture>plugin.video.plexkodiconnect/home/type/home-selected.png</texture>
</control>
</control>
</control>
<control type="label">
<posx>13</posx>
<posy>50</posy>
<width max="500">auto</width>
<height>66</height>
<font>font12</font>
<align>left</align>
<aligny>center</aligny>
<textcolor>FFFFFFFF</textcolor>
<label>[UPPERCASE]$ADDON[plugin.video.plexkodiconnect 33000][/UPPERCASE]</label>
</control>
</control>
<control type="label">
<right>213</right>
<posy>35</posy>
<width>200</width>
<height>65</height>
<font>font12</font>
<align>right</align>
<aligny>center</aligny>
<textcolor>FFFFFFFF</textcolor>
<label>$INFO[System.Time]</label>
</control>
<control type="image">
<posx>153r</posx>
<posy>54</posy>
<width>93</width>
<height>30</height>
<texture>plugin.video.plexkodiconnect/home/plex.png</texture>
</control>
</control>
<control type="group">
<visible>!String.IsEmpty(Window.Property(busy))</visible>
<control type="image">
<posx>840</posx>
<posy>465</posy>
<width>240</width>
<height>150</height>
<texture>script.plex/busy-back.png</texture>
<colordiffuse>A0FFFFFF</colordiffuse>
</control>
<control type="image">
<posx>915</posx>
<posy>521</posy>
<width>90</width>
<height>38</height>
<texture diffuse="script.plex/busy-diffuse.png">script.plex/busy.gif</texture>
</control>
</control>
</controls>
</window>