Merge pull request #618 from croneter/pickle

Fix rare EOFError and PKC starting wrong video as a consequence
This commit is contained in:
croneter 2019-01-26 09:22:40 +01:00 committed by GitHub
commit 15c0322f27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 180 additions and 157 deletions

View file

@ -4,8 +4,8 @@
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
<import addon="script.module.defusedxml" version="0.5.0"/> <import addon="script.module.defusedxml" version="0.5.0"/>
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.7" /> <import addon="plugin.video.plexkodiconnect.movies" version="2.0.8" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.8" /> <import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.9" />
</requires> </requires>
<extension point="xbmc.python.pluginsource" library="default.py"> <extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides> <provides>video audio image</provides>

View file

@ -40,9 +40,10 @@ def main():
'kodi_id': kodi_id, 'kodi_id': kodi_id,
'kodi_type': kodi_type 'kodi_type': kodi_type
} }
while window.getProperty('plex_command'): while window.getProperty('plexkodiconnect.command'):
sleep(20) sleep(20)
window.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(args)) window.setProperty('plexkodiconnect.command',
'CONTEXT_menu?%s' % urlencode(args))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -5,18 +5,18 @@ from __future__ import absolute_import, division, unicode_literals
import logging import logging
from sys import argv from sys import argv
from urlparse import parse_qsl from urlparse import parse_qsl
from xbmc import sleep, executebuiltin
from xbmcgui import ListItem, getCurrentWindowId
from xbmcplugin import setResolvedUrl
from resources.lib import entrypoint, utils, pickler, pkc_listitem, \ import xbmc
variables as v, loghandler import xbmcgui
import xbmcplugin
from resources.lib import entrypoint, utils, transfer, variables as v, loghandler
from resources.lib.tools import unicode_paths from resources.lib.tools import unicode_paths
############################################################################### ###############################################################################
loghandler.config() loghandler.config()
log = logging.getLogger('PLEX.default') LOG = logging.getLogger('PLEX.default')
############################################################################### ###############################################################################
@ -27,7 +27,7 @@ class Main():
# MAIN ENTRY POINT # MAIN ENTRY POINT
# @utils.profiling() # @utils.profiling()
def __init__(self): def __init__(self):
log.debug('Full sys.argv received: %s', argv) LOG.debug('Full sys.argv received: %s', argv)
# Parse parameters # Parse parameters
path = unicode_paths.decode(argv[0]) path = unicode_paths.decode(argv[0])
arguments = unicode_paths.decode(argv[2]) arguments = unicode_paths.decode(argv[2])
@ -73,23 +73,23 @@ class Main():
# Hack so we can store this path in the Kodi DB # Hack so we can store this path in the Kodi DB
handle = ('plugin://%s?mode=extras&plex_id=%s' handle = ('plugin://%s?mode=extras&plex_id=%s'
% (v.ADDON_ID, params.get('plex_id'))) % (v.ADDON_ID, params.get('plex_id')))
if getCurrentWindowId() == 10025: if xbmcgui.getCurrentWindowId() == 10025:
# Video Window # Video Window
executebuiltin('Container.Update(\"%s\")' % handle) xbmc.executebuiltin('Container.Update(\"%s\")' % handle)
else: else:
executebuiltin('ActivateWindow(videos, \"%s\")' % handle) xbmc.executebuiltin('ActivateWindow(videos, \"%s\")' % handle)
elif mode == 'extras': elif mode == 'extras':
entrypoint.extras(plex_id=params.get('plex_id')) entrypoint.extras(plex_id=params.get('plex_id'))
elif mode == 'settings': elif mode == 'settings':
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) xbmc.executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
elif mode == 'enterPMS': elif mode == 'enterPMS':
entrypoint.create_new_pms() entrypoint.create_new_pms()
elif mode == 'reset': elif mode == 'reset':
utils.plex_command('RESET-PKC') transfer.plex_command('RESET-PKC')
elif mode == 'togglePlexTV': elif mode == 'togglePlexTV':
entrypoint.toggle_plex_tv_sign_in() entrypoint.toggle_plex_tv_sign_in()
@ -102,15 +102,15 @@ class Main():
elif mode in ('manualsync', 'repair'): elif mode in ('manualsync', 'repair'):
if mode == 'repair': if mode == 'repair':
log.info('Requesting repair lib sync') LOG.info('Requesting repair lib sync')
utils.plex_command('repair-scan') transfer.plex_command('repair-scan')
elif mode == 'manualsync': elif mode == 'manualsync':
log.info('Requesting full library scan') LOG.info('Requesting full library scan')
utils.plex_command('full-scan') transfer.plex_command('full-scan')
elif mode == 'texturecache': elif mode == 'texturecache':
log.info('Requesting texture caching of all textures') LOG.info('Requesting texture caching of all textures')
utils.plex_command('textures-scan') transfer.plex_command('textures-scan')
elif mode == 'chooseServer': elif mode == 'chooseServer':
entrypoint.choose_pms_server() entrypoint.choose_pms_server()
@ -119,8 +119,8 @@ class Main():
self.deviceid() self.deviceid()
elif mode == 'fanart': elif mode == 'fanart':
log.info('User requested fanarttv refresh') LOG.info('User requested fanarttv refresh')
utils.plex_command('fanart-scan') transfer.plex_command('fanart-scan')
elif '/extrafanart' in path: elif '/extrafanart' in path:
plexpath = arguments[1:] plexpath = arguments[1:]
@ -150,51 +150,52 @@ class Main():
""" """
request = '%s&handle=%s' % (argv[2], HANDLE) request = '%s&handle=%s' % (argv[2], HANDLE)
# Put the request into the 'queue' # Put the request into the 'queue'
utils.plex_command('PLAY-%s' % request) transfer.plex_command('PLAY-%s' % request)
if HANDLE == -1: if HANDLE == -1:
# Handle -1 received, not waiting for main thread # Handle -1 received, not waiting for main thread
return return
# Wait for the result # Wait for the result from the main PKC thread
while not pickler.pickl_window('plex_result'): result = transfer.wait_for_transfer()
sleep(50)
result = pickler.unpickle_me()
if result is None: if result is None:
log.error('Error encountered, aborting') LOG.error('Error encountered, aborting')
utils.dialog('notification', utils.dialog('notification',
heading='{plex}', heading='{plex}',
message=utils.lang(30128), message=utils.lang(30128),
icon='{error}', icon='{error}',
time=3000) time=3000)
setResolvedUrl(HANDLE, False, ListItem()) xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem())
elif result.listitem: elif result is True:
listitem = pkc_listitem.convert_pkc_to_listitem(result.listitem) pass
setResolvedUrl(HANDLE, True, listitem) else:
# Received a xbmcgui.ListItem()
xbmcplugin.setResolvedUrl(HANDLE, True, result)
@staticmethod @staticmethod
def deviceid(): def deviceid():
deviceId_old = pickler.pickl_window('plex_client_Id') window = xbmcgui.Window(10000)
deviceId_old = window.getProperty('plex_client_Id')
from resources.lib import clientinfo from resources.lib import clientinfo
try: try:
deviceId = clientinfo.getDeviceId(reset=True) deviceId = clientinfo.getDeviceId(reset=True)
except Exception as e: except Exception as e:
log.error('Failed to generate a new device Id: %s' % e) LOG.error('Failed to generate a new device Id: %s' % e)
utils.messageDialog(utils.lang(29999), utils.lang(33032)) utils.messageDialog(utils.lang(29999), utils.lang(33032))
else: else:
log.info('Successfully removed old device ID: %s New deviceId:' LOG.info('Successfully removed old device ID: %s New deviceId:'
'%s' % (deviceId_old, deviceId)) '%s' % (deviceId_old, deviceId))
# 'Kodi will now restart to apply the changes' # 'Kodi will now restart to apply the changes'
utils.messageDialog(utils.lang(29999), utils.lang(33033)) utils.messageDialog(utils.lang(29999), utils.lang(33033))
executebuiltin('RestartApp') xbmc.executebuiltin('RestartApp')
if __name__ == '__main__': if __name__ == '__main__':
log.info('%s started' % v.ADDON_ID) LOG.info('%s started' % v.ADDON_ID)
try: try:
v.database_paths() v.database_paths()
except RuntimeError as err: except RuntimeError as err:
# Database does not exists # Database does not exists
log.error('The current Kodi version is incompatible') LOG.error('The current Kodi version is incompatible')
log.error('Error: %s', err) LOG.error('Error: %s', err)
else: else:
Main() Main()
log.info('%s stopped' % v.ADDON_ID) LOG.info('%s stopped' % v.ADDON_ID)

View file

@ -14,6 +14,7 @@ from xbmcgui import ListItem
from . import utils from . import utils
from . import path_ops from . import path_ops
from . import transfer
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from .plex_api import API from .plex_api import API
from . import plex_functions as PF from . import plex_functions as PF
@ -33,7 +34,7 @@ def choose_pms_server():
Lets user choose from list of PMS Lets user choose from list of PMS
""" """
LOG.info("Choosing PMS server requested, starting") LOG.info("Choosing PMS server requested, starting")
utils.plex_command('choose_pms_server') transfer.plex_command('choose_pms_server')
def toggle_plex_tv_sign_in(): def toggle_plex_tv_sign_in():
@ -42,7 +43,7 @@ def toggle_plex_tv_sign_in():
Or signs in to plex.tv if the user was not logged in before. Or signs in to plex.tv if the user was not logged in before.
""" """
LOG.info('Toggle of Plex.tv sign-in requested') LOG.info('Toggle of Plex.tv sign-in requested')
utils.plex_command('toggle_plex_tv_sign_in') transfer.plex_command('toggle_plex_tv_sign_in')
def directory_item(label, path, folder=True): def directory_item(label, path, folder=True):
@ -128,7 +129,7 @@ def switch_plex_user():
# position = 0 # position = 0
# utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) # utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
LOG.info("Plex home user switch requested") LOG.info("Plex home user switch requested")
utils.plex_command('switch_plex_user') transfer.plex_command('switch_plex_user')
def create_listitem(item, append_show_title=False, append_sxxexx=False): def create_listitem(item, append_show_title=False, append_sxxexx=False):
@ -905,4 +906,4 @@ def create_new_pms():
Opens dialogs for the user the plug in the PMS details Opens dialogs for the user the plug in the PMS details
""" """
LOG.info('Request to manually enter new PMS address') LOG.info('Request to manually enter new PMS address')
utils.plex_command('enter_new_pms_address') transfer.plex_command('enter_new_pms_address')

View file

@ -1,76 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from cPickle import dumps, loads
from xbmcgui import Window
from xbmc import log, LOGDEBUG
###############################################################################
WINDOW = Window(10000)
PREFIX = 'PLEX.pickler: '
###############################################################################
def try_encode(input_str, encoding='utf-8'):
"""
Will try to encode input_str (in unicode) to encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
COPY to avoid importing utils on calling default.py
"""
if isinstance(input_str, str):
# already encoded
return input_str
try:
input_str = input_str.encode(encoding, "ignore")
except TypeError:
input_str = input_str.encode()
return input_str
def pickl_window(property, value=None, clear=False):
"""
Get or set window property - thread safe! For use with Pickle
Property and value must be string
"""
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
else:
return try_encode(WINDOW.getProperty(property))
def pickle_me(obj, window_var='plex_result'):
"""
Pickles the obj to the window variable. Use to transfer Python
objects between different PKC python instances (e.g. if default.py is
called and you'd want to use the service.py instance)
obj can be pretty much any Python object. However, classes and
functions won't work. See the Pickle documentation
"""
log('%sStart pickling' % PREFIX, level=LOGDEBUG)
pickl_window(window_var, value=dumps(obj))
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
def unpickle_me(window_var='plex_result'):
"""
Unpickles a Python object from the window variable window_var.
Will then clear the window variable!
"""
result = pickl_window(window_var)
pickl_window(window_var, clear=True)
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
obj = loads(result)
log('%sSuccessfully unpickled' % PREFIX, level=LOGDEBUG)
return obj
class Playback_Successful(object):
"""
Used to communicate with another PKC Python instance
"""
listitem = None

View file

@ -16,9 +16,8 @@ from .kodi_db import KodiVideoDB
from . import playlist_func as PL from . import playlist_func as PL
from . import playqueue as PQ from . import playqueue as PQ
from . import json_rpc as js from . import json_rpc as js
from . import pickler from . import transfer
from .playutils import PlayUtils from .playutils import PlayUtils
from .pkc_listitem import PKCListItem
from . import variables as v from . import variables as v
from . import app from . import app
@ -261,11 +260,11 @@ def _ensure_resolve(abort=False):
""" """
if RESOLVE: if RESOLVE:
if not abort: if not abort:
result = pickler.Playback_Successful() # Releases the other Python thread without a ListItem
pickler.pickle_me(result) transfer.send(True)
else: else:
# Shows PKC error message # Shows PKC error message
pickler.pickle_me(None) transfer.send(None)
if abort: if abort:
# Reset some playback variables # Reset some playback variables
app.PLAYSTATE.context_menu_play = False app.PLAYSTATE.context_menu_play = False
@ -383,8 +382,7 @@ def _conclude_playback(playqueue, pos):
return PKC listitem attached to result return PKC listitem attached to result
""" """
LOG.info('Concluding playback for playqueue position %s', pos) LOG.info('Concluding playback for playqueue position %s', pos)
result = pickler.Playback_Successful() listitem = transfer.PKCListItem()
listitem = PKCListItem()
item = playqueue.items[pos] item = playqueue.items[pos]
if item.xml is not None: if item.xml is not None:
# Got a Plex element # Got a Plex element
@ -399,7 +397,7 @@ def _conclude_playback(playqueue, pos):
if not playurl: if not playurl:
LOG.info('Did not get a playurl, aborting playback silently') LOG.info('Did not get a playurl, aborting playback silently')
app.PLAYSTATE.resume_playback = False app.PLAYSTATE.resume_playback = False
pickler.pickle_me(result) transfer.send(True)
return return
listitem.setPath(utils.try_encode(playurl)) listitem.setPath(utils.try_encode(playurl))
if item.playmethod == 'DirectStream': if item.playmethod == 'DirectStream':
@ -427,8 +425,7 @@ def _conclude_playback(playqueue, pos):
listitem.setProperty('StartOffset', str(item.offset)) listitem.setProperty('StartOffset', str(item.offset))
listitem.setProperty('resumetime', str(item.offset)) listitem.setProperty('resumetime', str(item.offset))
# Reset the resumable flag # Reset the resumable flag
result.listitem = listitem transfer.send(listitem)
pickler.pickle_me(result)
LOG.info('Done concluding playback') LOG.info('Done concluding playback')
@ -447,7 +444,6 @@ def process_indirect(key, offset, resolve=True):
key, offset, resolve) key, offset, resolve)
global RESOLVE global RESOLVE
RESOLVE = resolve RESOLVE = resolve
result = pickler.Playback_Successful()
if key.startswith('http') or key.startswith('{server}'): if key.startswith('http') or key.startswith('{server}'):
xml = DU().downloadUrl(key) xml = DU().downloadUrl(key)
elif key.startswith('/system/services'): elif key.startswith('/system/services'):
@ -464,7 +460,7 @@ def process_indirect(key, offset, resolve=True):
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset))
# Todo: implement offset # Todo: implement offset
api = API(xml[0]) api = API(xml[0])
listitem = PKCListItem() listitem = transfer.PKCListItem()
api.create_listitem(listitem) api.create_listitem(listitem)
playqueue = PQ.get_playqueue_from_type( playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
@ -488,8 +484,7 @@ def process_indirect(key, offset, resolve=True):
listitem.setPath(utils.try_encode(playurl)) listitem.setPath(utils.try_encode(playurl))
playqueue.items.append(item) playqueue.items.append(item)
if resolve is True: if resolve is True:
result.listitem = listitem transfer.send(listitem)
pickler.pickle_me(result)
else: else:
thread = Thread(target=app.APP.player.play, thread = Thread(target=app.APP.player.play,
args={'item': utils.try_encode(playurl), args={'item': utils.try_encode(playurl),

View file

@ -7,7 +7,7 @@ from urlparse import parse_qsl
from . import playback from . import playback
from . import context_entry from . import context_entry
from . import pickler from . import transfer
from . import backgroundthread from . import backgroundthread
############################################################################### ###############################################################################
@ -33,7 +33,7 @@ class PlaybackTask(backgroundthread.Task):
except ValueError: except ValueError:
# E.g. other add-ons scanning for Extras folder # E.g. other add-ons scanning for Extras folder
LOG.debug('Detected 3rd party add-on call - ignoring') LOG.debug('Detected 3rd party add-on call - ignoring')
pickler.pickle_me(pickler.Playback_Successful()) transfer.send(True)
return return
params = dict(parse_qsl(params)) params = dict(parse_qsl(params))
mode = params.get('mode') mode = params.get('mode')

View file

@ -29,7 +29,7 @@ LOG = logging.getLogger("PLEX.service")
WINDOW_PROPERTIES = ( WINDOW_PROPERTIES = (
"plex_dbScan", "pms_token", "plex_token", "pms_server", "plex_dbScan", "pms_token", "plex_token", "pms_server",
"plex_authenticated", "plex_restricteduser", "plex_allows_mediaDeletion", "plex_authenticated", "plex_restricteduser", "plex_allows_mediaDeletion",
"plex_command", "plex_result") "plexkodiconnect.command", "plex_result")
# "Start from beginning", "Play from beginning" # "Start from beginning", "Play from beginning"
STRINGS = (utils.try_encode(utils.lang(12021)), STRINGS = (utils.try_encode(utils.lang(12021)),
@ -393,11 +393,11 @@ class Service():
break break
# Check for PKC commands from other Python instances # Check for PKC commands from other Python instances
plex_command = utils.window('plex_command') plex_command = utils.window('plexkodiconnect.command')
if plex_command: if plex_command:
# Commands/user interaction received from other PKC Python # Commands/user interaction received from other PKC Python
# instances (default.py and context.py instead of service.py) # instances (default.py and context.py instead of service.py)
utils.window('plex_command', clear=True) utils.window('plexkodiconnect.command', clear=True)
task = None task = None
if plex_command.startswith('PLAY-'): if plex_command.startswith('PLAY-'):
# Add-on path playback! # Add-on path playback!

View file

@ -1,9 +1,120 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
Used to shovel data from separate Kodi Python instances to the main thread
and vice versa.
"""
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from xbmcgui import ListItem from logging import getLogger
import json
from . import utils import xbmc
import xbmcgui
LOG = getLogger('PLEX.transfer')
MONITOR = xbmc.Monitor()
WINDOW = xbmcgui.Window(10000)
WINDOW_RESULT = 'plexkodiconnect.result'.encode('utf-8')
WINDOW_COMMAND = 'plexkodiconnect.command'.encode('utf-8')
def cast(func, value):
"""
Cast the specified value to the specified type (returned by func). Currently this
only support int, float, bool. Should be extended if needed.
Parameters:
func (func): Calback function to used cast to type (int, bool, float).
value (any): value to be cast and returned.
"""
if value is not None:
if func == bool:
return bool(int(value))
elif func == unicode:
if isinstance(value, (int, long, float)):
return unicode(value)
else:
return value.decode('utf-8')
elif func == str:
if isinstance(value, (int, long, float)):
return str(value)
else:
return value.encode('utf-8')
elif func in (int, float):
try:
return func(value)
except ValueError:
return float('nan')
return func(value)
return value
def kodi_window(property, value=None, clear=False):
"""
Get or set window property - thread safe! value must be string
"""
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value)
else:
return WINDOW.getProperty(property)
def plex_command(value):
"""
Used to funnel states between different Python instances. NOT really thread
safe - let's hope the Kodi user can't click fast enough
"""
while kodi_window(WINDOW_COMMAND):
if MONITOR.waitForAbort(20):
return
kodi_window(WINDOW_COMMAND, value=value)
def serialize(obj):
if isinstance(obj, PKCListItem):
return {'type': 'PKCListItem', 'data': obj.data}
else:
return {'type': 'other', 'data': obj}
return
def de_serialize(answ):
if answ['type'] == 'PKCListItem':
result = PKCListItem()
result.data = answ['data']
return convert_pkc_to_listitem(result)
elif answ['type'] == 'other':
return answ['data']
else:
raise NotImplementedError('Not implemented: %s' % answ)
def send(pkc_listitem):
"""
Pickles the obj to the window variable. Use to transfer Python
objects between different PKC python instances (e.g. if default.py is
called and you'd want to use the service.py instance)
obj can be pretty much any Python object. However, classes and
functions won't work. See the Pickle documentation
"""
LOG.debug('Sending: %s', pkc_listitem)
kodi_window(WINDOW_RESULT,
value=json.dumps(serialize(pkc_listitem)))
def wait_for_transfer():
result = ''
while not result:
result = kodi_window(WINDOW_RESULT)
if result:
kodi_window(WINDOW_RESULT, clear=True)
LOG.debug('Received')
result = json.loads(result)
return de_serialize(result)
elif MONITOR.waitForAbort(0.05):
return
def convert_pkc_to_listitem(pkc_listitem): def convert_pkc_to_listitem(pkc_listitem):
@ -11,9 +122,9 @@ def convert_pkc_to_listitem(pkc_listitem):
Insert a PKCListItem() and you will receive a valid XBMC listitem Insert a PKCListItem() and you will receive a valid XBMC listitem
""" """
data = pkc_listitem.data data = pkc_listitem.data
listitem = ListItem(label=data.get('label'), listitem = xbmcgui.ListItem(label=data.get('label'),
label2=data.get('label2'), label2=data.get('label2'),
path=data.get('path')) path=data.get('path'))
if data['info']: if data['info']:
listitem.setInfo(**data['info']) listitem.setInfo(**data['info'])
for stream in data['stream_info']: for stream in data['stream_info']:
@ -23,7 +134,7 @@ def convert_pkc_to_listitem(pkc_listitem):
if data['art']: if data['art']:
listitem.setArt(data['art']) listitem.setArt(data['art'])
for key, value in data['property'].iteritems(): for key, value in data['property'].iteritems():
listitem.setProperty(key, utils.cast(str, value)) listitem.setProperty(key, cast(str, value))
if data['subtitles']: if data['subtitles']:
listitem.setSubtitles(data['subtitles']) listitem.setSubtitles(data['subtitles'])
return listitem return listitem

View file

@ -104,16 +104,6 @@ def window(prop, value=None, clear=False, windowid=10000):
return try_decode(win.getProperty(prop)) return try_decode(win.getProperty(prop))
def plex_command(value):
"""
Used to funnel states between different Python instances. NOT really thread
safe - let's hope the Kodi user can't click fast enough
"""
while window('plex_command'):
xbmc.sleep(20)
window('plex_command', value=value)
def settings(setting, value=None): def settings(setting, value=None):
""" """
Get or add addon setting. Returns unicode Get or add addon setting. Returns unicode