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="script.module.requests" version="2.9.1" />
<import addon="script.module.defusedxml" version="0.5.0"/>
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.7" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.8" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.8" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.9" />
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides>

View file

@ -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__":

View file

@ -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)

View file

@ -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')

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 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),

View file

@ -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')

View file

@ -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!

View file

@ -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

View file

@ -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