From 69cb09e009c1988f74c7ac2b6bcce6796868b75d Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 26 Jan 2019 08:43:51 +0100 Subject: [PATCH] Replace cPickle communication with JSON --- contextmenu.py | 5 +- default.py | 77 +++++------ resources/lib/entrypoint.py | 9 +- resources/lib/pickler.py | 76 ----------- resources/lib/playback.py | 23 ++-- resources/lib/playback_starter.py | 4 +- resources/lib/service_entry.py | 6 +- .../lib/{pkc_listitem.py => transfer.py} | 123 +++++++++++++++++- resources/lib/utils.py | 10 -- 9 files changed, 178 insertions(+), 155 deletions(-) delete mode 100644 resources/lib/pickler.py rename resources/lib/{pkc_listitem.py => transfer.py} (76%) diff --git a/contextmenu.py b/contextmenu.py index 8d6976da..dfc7546e 100644 --- a/contextmenu.py +++ b/contextmenu.py @@ -40,9 +40,10 @@ def main(): 'kodi_id': kodi_id, 'kodi_type': kodi_type } - while window.getProperty('plex_command'): + while window.getProperty('plexkodiconnect.command'): sleep(20) - window.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(args)) + window.setProperty('plexkodiconnect.command', + 'CONTEXT_menu?%s' % urlencode(args)) if __name__ == "__main__": diff --git a/default.py b/default.py index 5423c57e..5668a751 100644 --- a/default.py +++ b/default.py @@ -5,18 +5,18 @@ from __future__ import absolute_import, division, unicode_literals import logging from sys import argv 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, \ - variables as v, loghandler +import xbmc +import xbmcgui +import xbmcplugin + +from resources.lib import entrypoint, utils, transfer, variables as v, loghandler from resources.lib.tools import unicode_paths ############################################################################### loghandler.config() -log = logging.getLogger('PLEX.default') +LOG = logging.getLogger('PLEX.default') ############################################################################### @@ -27,7 +27,7 @@ class Main(): # MAIN ENTRY POINT # @utils.profiling() def __init__(self): - log.debug('Full sys.argv received: %s', argv) + LOG.debug('Full sys.argv received: %s', argv) # Parse parameters path = unicode_paths.decode(argv[0]) arguments = unicode_paths.decode(argv[2]) @@ -73,23 +73,23 @@ class Main(): # Hack so we can store this path in the Kodi DB handle = ('plugin://%s?mode=extras&plex_id=%s' % (v.ADDON_ID, params.get('plex_id'))) - if getCurrentWindowId() == 10025: + if xbmcgui.getCurrentWindowId() == 10025: # Video Window - executebuiltin('Container.Update(\"%s\")' % handle) + xbmc.executebuiltin('Container.Update(\"%s\")' % handle) else: - executebuiltin('ActivateWindow(videos, \"%s\")' % handle) + xbmc.executebuiltin('ActivateWindow(videos, \"%s\")' % handle) elif mode == 'extras': entrypoint.extras(plex_id=params.get('plex_id')) elif mode == 'settings': - executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) + xbmc.executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) elif mode == 'enterPMS': entrypoint.create_new_pms() elif mode == 'reset': - utils.plex_command('RESET-PKC') + transfer.plex_command('RESET-PKC') elif mode == 'togglePlexTV': entrypoint.toggle_plex_tv_sign_in() @@ -102,15 +102,15 @@ class Main(): elif mode in ('manualsync', 'repair'): if mode == 'repair': - log.info('Requesting repair lib sync') - utils.plex_command('repair-scan') + LOG.info('Requesting repair lib sync') + transfer.plex_command('repair-scan') elif mode == 'manualsync': - log.info('Requesting full library scan') - utils.plex_command('full-scan') + LOG.info('Requesting full library scan') + transfer.plex_command('full-scan') elif mode == 'texturecache': - log.info('Requesting texture caching of all textures') - utils.plex_command('textures-scan') + LOG.info('Requesting texture caching of all textures') + transfer.plex_command('textures-scan') elif mode == 'chooseServer': entrypoint.choose_pms_server() @@ -119,8 +119,8 @@ class Main(): self.deviceid() elif mode == 'fanart': - log.info('User requested fanarttv refresh') - utils.plex_command('fanart-scan') + LOG.info('User requested fanarttv refresh') + transfer.plex_command('fanart-scan') elif '/extrafanart' in path: plexpath = arguments[1:] @@ -150,51 +150,52 @@ class Main(): """ request = '%s&handle=%s' % (argv[2], HANDLE) # Put the request into the 'queue' - utils.plex_command('PLAY-%s' % request) + transfer.plex_command('PLAY-%s' % request) if HANDLE == -1: # Handle -1 received, not waiting for main thread return - # Wait for the result - while not pickler.pickl_window('plex_result'): - sleep(50) - result = pickler.unpickle_me() + # Wait for the result from the main PKC thread + result = transfer.wait_for_transfer() if result is None: - log.error('Error encountered, aborting') + LOG.error('Error encountered, aborting') utils.dialog('notification', heading='{plex}', message=utils.lang(30128), icon='{error}', time=3000) - setResolvedUrl(HANDLE, False, ListItem()) - elif result.listitem: - listitem = pkc_listitem.convert_pkc_to_listitem(result.listitem) - setResolvedUrl(HANDLE, True, listitem) + xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem()) + elif result is True: + pass + else: + # Received a xbmcgui.ListItem() + xbmcplugin.setResolvedUrl(HANDLE, True, result) @staticmethod 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 try: deviceId = clientinfo.getDeviceId(reset=True) 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)) 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)) # 'Kodi will now restart to apply the changes' utils.messageDialog(utils.lang(29999), utils.lang(33033)) - executebuiltin('RestartApp') + xbmc.executebuiltin('RestartApp') if __name__ == '__main__': - log.info('%s started' % v.ADDON_ID) + LOG.info('%s started' % v.ADDON_ID) try: v.database_paths() except RuntimeError as err: # Database does not exists - log.error('The current Kodi version is incompatible') - log.error('Error: %s', err) + LOG.error('The current Kodi version is incompatible') + LOG.error('Error: %s', err) else: Main() - log.info('%s stopped' % v.ADDON_ID) + LOG.info('%s stopped' % v.ADDON_ID) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 2971fc25..affa969c 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -14,6 +14,7 @@ from xbmcgui import ListItem from . import utils from . import path_ops +from . import transfer from .downloadutils import DownloadUtils as DU from .plex_api import API from . import plex_functions as PF @@ -33,7 +34,7 @@ def choose_pms_server(): Lets user choose from list of PMS """ 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(): @@ -42,7 +43,7 @@ def toggle_plex_tv_sign_in(): Or signs in to plex.tv if the user was not logged in before. """ 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): @@ -128,7 +129,7 @@ def switch_plex_user(): # position = 0 # utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) 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): @@ -905,4 +906,4 @@ def create_new_pms(): Opens dialogs for the user the plug in the PMS details """ LOG.info('Request to manually enter new PMS address') - utils.plex_command('enter_new_pms_address') + transfer.plex_command('enter_new_pms_address') diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py deleted file mode 100644 index bf836663..00000000 --- a/resources/lib/pickler.py +++ /dev/null @@ -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 diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 8e3b84b8..40e9015b 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -16,9 +16,8 @@ from .kodi_db import KodiVideoDB from . import playlist_func as PL from . import playqueue as PQ from . import json_rpc as js -from . import pickler +from . import transfer from .playutils import PlayUtils -from .pkc_listitem import PKCListItem from . import variables as v from . import app @@ -261,11 +260,11 @@ def _ensure_resolve(abort=False): """ if RESOLVE: if not abort: - result = pickler.Playback_Successful() - pickler.pickle_me(result) + # Releases the other Python thread without a ListItem + transfer.send(True) else: # Shows PKC error message - pickler.pickle_me(None) + transfer.send(None) if abort: # Reset some playback variables app.PLAYSTATE.context_menu_play = False @@ -383,8 +382,7 @@ def _conclude_playback(playqueue, pos): return PKC listitem attached to result """ LOG.info('Concluding playback for playqueue position %s', pos) - result = pickler.Playback_Successful() - listitem = PKCListItem() + listitem = transfer.PKCListItem() item = playqueue.items[pos] if item.xml is not None: # Got a Plex element @@ -399,7 +397,7 @@ def _conclude_playback(playqueue, pos): if not playurl: LOG.info('Did not get a playurl, aborting playback silently') app.PLAYSTATE.resume_playback = False - pickler.pickle_me(result) + transfer.send(True) return listitem.setPath(utils.try_encode(playurl)) if item.playmethod == 'DirectStream': @@ -427,8 +425,7 @@ def _conclude_playback(playqueue, pos): listitem.setProperty('StartOffset', str(item.offset)) listitem.setProperty('resumetime', str(item.offset)) # Reset the resumable flag - result.listitem = listitem - pickler.pickle_me(result) + transfer.send(listitem) LOG.info('Done concluding playback') @@ -447,7 +444,6 @@ def process_indirect(key, offset, resolve=True): key, offset, resolve) global RESOLVE RESOLVE = resolve - result = pickler.Playback_Successful() if key.startswith('http') or key.startswith('{server}'): xml = DU().downloadUrl(key) 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)) # Todo: implement offset api = API(xml[0]) - listitem = PKCListItem() + listitem = transfer.PKCListItem() api.create_listitem(listitem) playqueue = PQ.get_playqueue_from_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)) playqueue.items.append(item) if resolve is True: - result.listitem = listitem - pickler.pickle_me(result) + transfer.send(listitem) else: thread = Thread(target=app.APP.player.play, args={'item': utils.try_encode(playurl), diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index a6f67804..b8673bc6 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -7,7 +7,7 @@ from urlparse import parse_qsl from . import playback from . import context_entry -from . import pickler +from . import transfer from . import backgroundthread ############################################################################### @@ -33,7 +33,7 @@ class PlaybackTask(backgroundthread.Task): except ValueError: # E.g. other add-ons scanning for Extras folder LOG.debug('Detected 3rd party add-on call - ignoring') - pickler.pickle_me(pickler.Playback_Successful()) + transfer.send(True) return params = dict(parse_qsl(params)) mode = params.get('mode') diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index 56fe0fc5..82e50381 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -29,7 +29,7 @@ LOG = logging.getLogger("PLEX.service") WINDOW_PROPERTIES = ( "plex_dbScan", "pms_token", "plex_token", "pms_server", "plex_authenticated", "plex_restricteduser", "plex_allows_mediaDeletion", - "plex_command", "plex_result") + "plexkodiconnect.command", "plex_result") # "Start from beginning", "Play from beginning" STRINGS = (utils.try_encode(utils.lang(12021)), @@ -393,11 +393,11 @@ class Service(): break # Check for PKC commands from other Python instances - plex_command = utils.window('plex_command') + plex_command = utils.window('plexkodiconnect.command') if plex_command: # Commands/user interaction received from other PKC Python # instances (default.py and context.py instead of service.py) - utils.window('plex_command', clear=True) + utils.window('plexkodiconnect.command', clear=True) task = None if plex_command.startswith('PLAY-'): # Add-on path playback! diff --git a/resources/lib/pkc_listitem.py b/resources/lib/transfer.py similarity index 76% rename from resources/lib/pkc_listitem.py rename to resources/lib/transfer.py index 015b9d1a..236e4a14 100644 --- a/resources/lib/pkc_listitem.py +++ b/resources/lib/transfer.py @@ -1,9 +1,120 @@ #!/usr/bin/env python # -*- 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 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): @@ -11,9 +122,9 @@ def convert_pkc_to_listitem(pkc_listitem): Insert a PKCListItem() and you will receive a valid XBMC listitem """ data = pkc_listitem.data - listitem = ListItem(label=data.get('label'), - label2=data.get('label2'), - path=data.get('path')) + listitem = xbmcgui.ListItem(label=data.get('label'), + label2=data.get('label2'), + path=data.get('path')) if data['info']: listitem.setInfo(**data['info']) for stream in data['stream_info']: @@ -23,7 +134,7 @@ def convert_pkc_to_listitem(pkc_listitem): if data['art']: listitem.setArt(data['art']) for key, value in data['property'].iteritems(): - listitem.setProperty(key, utils.cast(str, value)) + listitem.setProperty(key, cast(str, value)) if data['subtitles']: listitem.setSubtitles(data['subtitles']) return listitem diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 762bd1eb..c7bddd97 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -104,16 +104,6 @@ def window(prop, value=None, clear=False, windowid=10000): 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): """ Get or add addon setting. Returns unicode