Merge pull request #1073 from croneter/fix-userswitch

Optimize threads by using events instead of a polling mechanism. Fixes PKC becoming unresponsive, e.g. when switching users
This commit is contained in:
croneter 2019-11-30 16:27:06 +01:00 committed by GitHub
commit f4ea051c81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 194 additions and 243 deletions

View file

@ -124,19 +124,16 @@ class App(object):
if block: if block:
while True: while True:
for thread in self.threads: for thread in self.threads:
if not thread.suspend_reached: if not thread.is_suspended():
LOG.debug('Waiting for thread to suspend: %s', thread) LOG.debug('Waiting for thread to suspend: %s', thread)
# Send suspend signal again in case self.threads # Send suspend signal again in case self.threads
# changed # changed
thread.suspend() thread.suspend(block=True)
if self.monitor.waitForAbort(0.1):
return True
break
else: else:
break break
return xbmc.abortRequested return xbmc.abortRequested
def resume_threads(self, block=True): def resume_threads(self):
""" """
Resume all thread activity with or without blocking. Resume all thread activity with or without blocking.
Returns True only if PKC shutdown requested Returns True only if PKC shutdown requested
@ -144,16 +141,6 @@ class App(object):
LOG.debug('Resuming threads: %s', self.threads) LOG.debug('Resuming threads: %s', self.threads)
for thread in self.threads: for thread in self.threads:
thread.resume() thread.resume()
if block:
while True:
for thread in self.threads:
if thread.suspend_reached:
LOG.debug('Waiting for thread to resume: %s', thread)
if self.monitor.waitForAbort(0.1):
return True
break
else:
break
return xbmc.abortRequested return xbmc.abortRequested
def stop_threads(self, block=True): def stop_threads(self, block=True):
@ -163,7 +150,7 @@ class App(object):
""" """
LOG.debug('Killing threads: %s', self.threads) LOG.debug('Killing threads: %s', self.threads)
for thread in self.threads: for thread in self.threads:
thread.abort() thread.cancel()
if block: if block:
while self.threads: while self.threads:
LOG.debug('Waiting for threads to exit: %s', self.threads) LOG.debug('Waiting for threads to exit: %s', self.threads)

View file

@ -33,8 +33,8 @@ class ImageCachingThread(backgroundthread.KillableThread):
if not utils.settings('imageSyncDuringPlayback') == 'true': if not utils.settings('imageSyncDuringPlayback') == 'true':
self.suspend_points.append((app.APP, 'is_playing_video')) self.suspend_points.append((app.APP, 'is_playing_video'))
def isSuspended(self): def should_suspend(self):
return any(getattr(obj, txt) for obj, txt in self.suspend_points) return any(getattr(obj, attrib) for obj, attrib in self.suspend_points)
@staticmethod @staticmethod
def _url_generator(kind, kodi_type): def _url_generator(kind, kodi_type):
@ -73,18 +73,26 @@ class ImageCachingThread(backgroundthread.KillableThread):
app.APP.deregister_caching_thread(self) app.APP.deregister_caching_thread(self)
LOG.info("---===### Stopped ImageCachingThread ###===---") LOG.info("---===### Stopped ImageCachingThread ###===---")
def _run(self): def _loop(self):
kinds = [KodiVideoDB] kinds = [KodiVideoDB]
if app.SYNC.enable_music: if app.SYNC.enable_music:
kinds.append(KodiMusicDB) kinds.append(KodiMusicDB)
for kind in kinds: for kind in kinds:
for kodi_type in ('poster', 'fanart'): for kodi_type in ('poster', 'fanart'):
for url in self._url_generator(kind, kodi_type): for url in self._url_generator(kind, kodi_type):
if self.wait_while_suspended(): if self.should_suspend() or self.should_cancel():
return return False
cache_url(url) cache_url(url)
# Toggles Image caching completed to Yes # Toggles Image caching completed to Yes
utils.settings('plex_status_image_caching', value=utils.lang(107)) utils.settings('plex_status_image_caching', value=utils.lang(107))
return True
def _run(self):
while True:
if self._loop():
break
if self.wait_while_suspended():
break
def cache_url(url): def cache_url(url):

View file

@ -13,131 +13,95 @@ LOG = getLogger('PLEX.threads')
class KillableThread(threading.Thread): class KillableThread(threading.Thread):
'''A thread class that supports raising exception in the thread from
another thread.
'''
# def _get_my_tid(self):
# """determines this (self's) thread id
# CAREFUL : this function is executed in the context of the caller
# thread, to get the identity of the thread represented by this
# instance.
# """
# if not self.isAlive():
# raise threading.ThreadError("the thread is not active")
# return self.ident
# def _raiseExc(self, exctype):
# """Raises the given exception type in the context of this thread.
# If the thread is busy in a system call (time.sleep(),
# socket.accept(), ...), the exception is simply ignored.
# If you are sure that your exception should terminate the thread,
# one way to ensure that it works is:
# t = ThreadWithExc( ... )
# ...
# t.raiseExc( SomeException )
# while t.isAlive():
# time.sleep( 0.1 )
# t.raiseExc( SomeException )
# If the exception is to be caught by the thread, you need a way to
# check that your thread has caught it.
# CAREFUL : this function is executed in the context of the
# caller thread, to raise an excpetion in the context of the
# thread represented by this instance.
# """
# _async_raise(self._get_my_tid(), exctype)
def kill(self, force_and_wait=False):
pass
# try:
# self._raiseExc(KillThreadException)
# if force_and_wait:
# time.sleep(0.1)
# while self.isAlive():
# self._raiseExc(KillThreadException)
# time.sleep(0.1)
# except threading.ThreadError:
# pass
# def onKilled(self):
# pass
# def run(self):
# try:
# self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
# except KillThreadException:
# self.onKilled()
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
self._canceled = False self._canceled = False
# Set to True to set the thread to suspended
self._suspended = False self._suspended = False
# Thread will return True only if suspended state is reached self._is_not_suspended = threading.Event()
self.suspend_reached = False self._is_not_suspended.set()
self._suspension_reached = threading.Event()
self._is_not_asleep = threading.Event()
self._is_not_asleep.set()
self.suspension_timeout = None
super(KillableThread, self).__init__(group, target, name, args, kwargs) super(KillableThread, self).__init__(group, target, name, args, kwargs)
def isCanceled(self): def should_cancel(self):
""" """
Returns True if the thread is stopped Returns True if the thread should be stopped immediately
""" """
if self._canceled or xbmc.abortRequested: return self._canceled or app.APP.stop_pkc
return True
return False
def abort(self): def cancel(self):
""" """
Call to stop this thread Call from another thread to stop this current thread
""" """
self._canceled = True self._canceled = True
# Make sure thread is running in order to exit quickly
self._is_not_suspended.set()
self._is_not_asleep.set()
def suspend(self, block=False): def should_suspend(self):
""" """
Call to suspend this thread Returns True if the current thread should be suspended immediately
""" """
return self._suspended
def suspend(self, block=False, timeout=None):
"""
Call from another thread to suspend the current thread. Provide a
timeout [float] in seconds optionally. block=True will block the caller
until the thread-to-be-suspended is indeed suspended
Will wake a thread that is asleep!
"""
self.suspension_timeout = timeout
self._suspended = True self._suspended = True
self._is_not_suspended.clear()
# Make sure thread wakes up in order to suspend
self._is_not_asleep.set()
if block: if block:
while not self.suspend_reached: self._suspension_reached.wait()
LOG.debug('Waiting for thread to suspend: %s', self)
if app.APP.monitor.waitForAbort(0.1):
return
def resume(self): def resume(self):
""" """
Call to revive a suspended thread back to life Call from another thread to revive a suspended or asleep current thread
back to life
""" """
self._suspended = False self._suspended = False
self._is_not_suspended.set()
self._is_not_asleep.set()
def wait_while_suspended(self): def wait_while_suspended(self):
""" """
Blocks until thread is not suspended anymore or the thread should Blocks until thread is not suspended anymore or the thread should
exit. exit or for a period of self.suspension_timeout (set by the caller of
Returns True only if the thread should exit (=isCanceled()) suspend())
Returns the value of should_cancel()
""" """
while self.isSuspended(): self._suspension_reached.set()
try: self._is_not_suspended.wait(self.suspension_timeout)
self.suspend_reached = True self._suspension_reached.clear()
# Set in service.py return self.should_cancel()
if self.isCanceled():
# Abort was requested while waiting. We should exit
return True
if app.APP.monitor.waitForAbort(0.1):
return True
finally:
self.suspend_reached = False
return self.isCanceled()
def isSuspended(self): def is_suspended(self):
""" """
Returns True if the thread is suspended Check from another thread whether the current thread is suspended
""" """
return self._suspended return self._suspension_reached.is_set()
def sleep(self, timeout):
"""
Only call from the current thread in order to sleep for a period of
timeout [float, seconds]. Will unblock immediately if thread should
cancel (should_cancel()) or the thread should_suspend
"""
self._is_not_asleep.clear()
self._is_not_asleep.wait(timeout)
self._is_not_asleep.set()
def is_asleep(self):
"""
Check from another thread whether the current thread is asleep
"""
return not self._is_not_asleep.is_set()
class OrderedQueue(Queue.PriorityQueue, object): class OrderedQueue(Queue.PriorityQueue, object):
@ -239,7 +203,7 @@ class Task(object):
def cancel(self): def cancel(self):
self._canceled = True self._canceled = True
def isCanceled(self): def should_cancel(self):
return self._canceled or xbmc.abortRequested return self._canceled or xbmc.abortRequested
def isValid(self): def isValid(self):

View file

@ -9,34 +9,6 @@ PLAYLIST_SYNC_ENABLED = (v.DEVICE != 'Microsoft UWP' and
utils.settings('enablePlaylistSync') == 'true') utils.settings('enablePlaylistSync') == 'true')
class fullsync_mixin(object):
def __init__(self):
self._canceled = False
def abort(self):
"""Hit method to terminate the thread"""
self._canceled = True
# Let's NOT suspend sync threads but immediately terminate them
suspend = abort
@property
def suspend_reached(self):
"""Since we're not suspending, we'll never set it to True"""
return False
@suspend_reached.setter
def suspend_reached(self):
pass
def resume(self):
"""Obsolete since we're not suspending"""
pass
def isCanceled(self):
"""Check whether we should exit this thread"""
return self._canceled
def update_kodi_library(video=True, music=True): def update_kodi_library(video=True, music=True):
""" """
Updates the Kodi library and thus refreshes the Kodi views and widgets Updates the Kodi library and thus refreshes the Kodi views and widgets

View file

@ -27,22 +27,20 @@ class FanartThread(backgroundthread.KillableThread):
self.refresh = refresh self.refresh = refresh
super(FanartThread, self).__init__() super(FanartThread, self).__init__()
def isSuspended(self): def should_suspend(self):
return self._suspended or app.APP.is_playing_video return self._suspended or app.APP.is_playing_video
def run(self): def run(self):
LOG.info('Starting FanartThread') LOG.info('Starting FanartThread')
app.APP.register_fanart_thread(self) app.APP.register_fanart_thread(self)
try: try:
self._run_internal() self._run()
except Exception: except Exception:
utils.ERROR(notify=True) utils.ERROR(notify=True)
finally: finally:
app.APP.deregister_fanart_thread(self) app.APP.deregister_fanart_thread(self)
def _run_internal(self): def _loop(self):
finished = False
try:
for typus in SUPPORTED_TYPES: for typus in SUPPORTED_TYPES:
offset = 0 offset = 0
while True: while True:
@ -58,15 +56,20 @@ class FanartThread(backgroundthread.KillableThread):
BATCH_SIZE)) BATCH_SIZE))
for plex_id in batch: for plex_id in batch:
# Do the actual, time-consuming processing # Do the actual, time-consuming processing
if self.wait_while_suspended(): if self.should_suspend() or self.should_cancel():
return return False
process_fanart(plex_id, typus, self.refresh) process_fanart(plex_id, typus, self.refresh)
if len(batch) < BATCH_SIZE: if len(batch) < BATCH_SIZE:
break break
offset += BATCH_SIZE offset += BATCH_SIZE
else: return True
finished = True
finally: def _run(self):
finished = False
while not finished:
finished = self._loop()
if self.wait_while_suspended():
break
LOG.info('FanartThread finished: %s', finished) LOG.info('FanartThread finished: %s', finished)
self.callback(finished) self.callback(finished)

View file

@ -39,7 +39,7 @@ class InitNewSection(object):
self.plex_type = plex_type self.plex_type = plex_type
class FullSync(common.fullsync_mixin): class FullSync(backgroundthread.KillableThread):
def __init__(self, repair, callback, show_dialog): def __init__(self, repair, callback, show_dialog):
""" """
repair=True: force sync EVERY item repair=True: force sync EVERY item
@ -75,6 +75,12 @@ class FullSync(common.fullsync_mixin):
worker_count=self.worker_count) worker_count=self.worker_count)
super(FullSync, self).__init__() super(FullSync, self).__init__()
def suspend(self, block=False, timeout=None):
"""
Let's NOT suspend sync threads but immediately terminate them
"""
self.cancel()
def update_progressbar(self): def update_progressbar(self):
if self.dialog: if self.dialog:
try: try:
@ -112,7 +118,7 @@ class FullSync(common.fullsync_mixin):
if not self.section: if not self.section:
_, self.section = self.queue.get() _, self.section = self.queue.get()
self.queue.task_done() self.queue.task_done()
while not self.isCanceled() and self.item_count > 0: while not self.should_cancel() and self.item_count > 0:
section = self.section section = self.section
if not section: if not section:
break break
@ -124,12 +130,12 @@ class FullSync(common.fullsync_mixin):
self.section_type_text = utils.lang( self.section_type_text = utils.lang(
v.TRANSLATION_FROM_PLEXTYPE[section.plex_type]) v.TRANSLATION_FROM_PLEXTYPE[section.plex_type])
with section.context(self.current_sync) as context: with section.context(self.current_sync) as context:
while not self.isCanceled() and self.item_count > 0: while not self.should_cancel() and self.item_count > 0:
try: try:
_, item = self.queue.get(block=False) _, item = self.queue.get(block=False)
except backgroundthread.Queue.Empty: except backgroundthread.Queue.Empty:
if self.threader.threader.working(): if self.threader.threader.working():
app.APP.monitor.waitForAbort(0.02) self.sleep(0.02)
continue continue
else: else:
# Try again, in case a thread just finished # Try again, in case a thread just finished
@ -187,7 +193,7 @@ class FullSync(common.fullsync_mixin):
# Check Plex DB to see what we need to add/update # Check Plex DB to see what we need to add/update
with PlexDB() as self.plexdb: with PlexDB() as self.plexdb:
for last, xml_item in loop: for last, xml_item in loop:
if self.isCanceled(): if self.should_cancel():
return False return False
self.process_item(xml_item) self.process_item(xml_item)
if self.item_count == BATCH_SIZE: if self.item_count == BATCH_SIZE:
@ -227,7 +233,7 @@ class FullSync(common.fullsync_mixin):
while True: while True:
with section.context(self.current_sync) as itemtype: with section.context(self.current_sync) as itemtype:
for i, (last, xml_item) in enumerate(loop): for i, (last, xml_item) in enumerate(loop):
if self.isCanceled(): if self.should_cancel():
return False return False
if not itemtype.update_userdata(xml_item, section.plex_type): if not itemtype.update_userdata(xml_item, section.plex_type):
# Somehow did not sync this item yet # Somehow did not sync this item yet
@ -256,7 +262,7 @@ class FullSync(common.fullsync_mixin):
for kind in kinds: for kind in kinds:
for section in (x for x in app.SYNC.sections for section in (x for x in app.SYNC.sections
if x.section_type == kind[1]): if x.section_type == kind[1]):
if self.isCanceled(): if self.should_cancel():
LOG.debug('Need to exit now') LOG.debug('Need to exit now')
return return
if not section.sync_to_kodi: if not section.sync_to_kodi:
@ -332,7 +338,7 @@ class FullSync(common.fullsync_mixin):
self.get_children = section.get_children self.get_children = section.get_children
self.queue = section.Queue() self.queue = section.Queue()
# Now do the heavy lifting # Now do the heavy lifting
if self.isCanceled() or not self.addupdate_section(section): if self.should_cancel() or not self.addupdate_section(section):
return False return False
if self.section_success: if self.section_success:
# Need to check because a thread might have missed to get # Need to check because a thread might have missed to get
@ -391,7 +397,7 @@ class FullSync(common.fullsync_mixin):
self.context = section.context self.context = section.context
self.get_children = section.get_children self.get_children = section.get_children
# Now do the heavy lifting # Now do the heavy lifting
if self.isCanceled() or not self.playstate_per_section(section): if self.should_cancel() or not self.playstate_per_section(section):
return False return False
# Delete movies that are not on Plex anymore # Delete movies that are not on Plex anymore
@ -416,7 +422,7 @@ class FullSync(common.fullsync_mixin):
self.current_sync, self.current_sync,
BATCH_SIZE)) BATCH_SIZE))
for plex_id in plex_ids: for plex_id in plex_ids:
if self.isCanceled(): if self.should_cancel():
return False return False
ctx.remove(plex_id, plex_type) ctx.remove(plex_id, plex_type)
if len(plex_ids) < BATCH_SIZE: if len(plex_ids) < BATCH_SIZE:
@ -436,7 +442,7 @@ class FullSync(common.fullsync_mixin):
def _run(self): def _run(self):
self.current_sync = timing.plex_now() self.current_sync = timing.plex_now()
# Get latest Plex libraries and build playlist and video node files # Get latest Plex libraries and build playlist and video node files
if self.isCanceled() or not sections.sync_from_pms(self): if self.should_cancel() or not sections.sync_from_pms(self):
return return
self.successful = True self.successful = True
try: try:
@ -447,7 +453,7 @@ class FullSync(common.fullsync_mixin):
# Actual syncing - do only new items first # Actual syncing - do only new items first
LOG.info('Running full_library_sync with repair=%s', LOG.info('Running full_library_sync with repair=%s',
self.repair) self.repair)
if self.isCanceled() or not self.full_library_sync(): if self.should_cancel() or not self.full_library_sync():
self.successful = False self.successful = False
return return
finally: finally:
@ -457,7 +463,7 @@ class FullSync(common.fullsync_mixin):
if self.threader: if self.threader:
self.threader.shutdown() self.threader.shutdown()
self.threader = None self.threader = None
if not self.successful and not self.isCanceled(): if not self.successful and not self.should_cancel():
# "ERROR in library sync" # "ERROR in library sync"
utils.dialog('notification', utils.dialog('notification',
heading='{plex}', heading='{plex}',
@ -468,4 +474,5 @@ class FullSync(common.fullsync_mixin):
def start(show_dialog, repair=False, callback=None): def start(show_dialog, repair=False, callback=None):
# Call run() and NOT start in order to not spawn another thread
FullSync(repair, callback, show_dialog).run() FullSync(repair, callback, show_dialog).run()

View file

@ -2,7 +2,6 @@
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
from . import common
from ..plex_api import API from ..plex_api import API
from .. import plex_functions as PF, backgroundthread, utils, variables as v from .. import plex_functions as PF, backgroundthread, utils, variables as v
@ -27,7 +26,7 @@ def reset_collections():
COLLECTION_XMLS = {} COLLECTION_XMLS = {}
class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task): class GetMetadataTask(backgroundthread.Task):
""" """
Threaded download of Plex XML metadata for a certain library item. Threaded download of Plex XML metadata for a certain library item.
Fills the queue with the downloaded etree XML objects Fills the queue with the downloaded etree XML objects
@ -45,6 +44,12 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
self.count = count self.count = count
super(GetMetadataTask, self).__init__() super(GetMetadataTask, self).__init__()
def suspend(self, block=False, timeout=None):
"""
Let's NOT suspend sync threads but immediately terminate them
"""
self.cancel()
def _collections(self, item): def _collections(self, item):
global COLLECTION_MATCH, COLLECTION_XMLS global COLLECTION_MATCH, COLLECTION_XMLS
api = API(item['xml'][0]) api = API(item['xml'][0])
@ -59,7 +64,7 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
utils.cast(int, x.get('ratingKey'))) for x in COLLECTION_MATCH] utils.cast(int, x.get('ratingKey'))) for x in COLLECTION_MATCH]
item['children'] = {} item['children'] = {}
for plex_set_id, set_name in api.collections(): for plex_set_id, set_name in api.collections():
if self.isCanceled(): if self.should_cancel():
return return
if plex_set_id not in COLLECTION_XMLS: if plex_set_id not in COLLECTION_XMLS:
# Get Plex metadata for collections - a pain # Get Plex metadata for collections - a pain
@ -84,7 +89,7 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
""" """
Do the work Do the work
""" """
if self.isCanceled(): if self.should_cancel():
return return
# Download Metadata # Download Metadata
item = { item = {
@ -101,7 +106,7 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
'Cancelling sync for now') 'Cancelling sync for now')
utils.window('plex_scancrashed', value='401') utils.window('plex_scancrashed', value='401')
return return
if not self.isCanceled() and self.plex_type == v.PLEX_TYPE_MOVIE: if not self.should_cancel() and self.plex_type == v.PLEX_TYPE_MOVIE:
# Check for collections/sets # Check for collections/sets
collections = False collections = False
for child in item['xml'][0]: for child in item['xml'][0]:
@ -112,7 +117,7 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
global LOCK global LOCK
with LOCK: with LOCK:
self._collections(item) self._collections(item)
if not self.isCanceled() and self.get_children: if not self.should_cancel() and self.get_children:
children_xml = PF.GetAllPlexChildren(self.plex_id) children_xml = PF.GetAllPlexChildren(self.plex_id)
try: try:
children_xml[0].attrib children_xml[0].attrib
@ -121,5 +126,5 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
self.plex_id) self.plex_id)
else: else:
item['children'] = children_xml item['children'] = children_xml
if not self.isCanceled(): if not self.should_cancel():
self.queue.put((self.count, item)) self.queue.put((self.count, item))

View file

@ -16,7 +16,7 @@ LOG = getLogger('PLEX.sync.sections')
BATCH_SIZE = 500 BATCH_SIZE = 500
# Need a way to interrupt our synching process # Need a way to interrupt our synching process
IS_CANCELED = None SHOULD_CANCEL = None
LIBRARY_PATH = path_ops.translate_path('special://profile/library/video/') LIBRARY_PATH = path_ops.translate_path('special://profile/library/video/')
# The video library might not yet exist for this user - create it # The video library might not yet exist for this user - create it
@ -490,7 +490,7 @@ def _delete_kodi_db_items(section):
with kodi_context(texture_db=True) as kodidb: with kodi_context(texture_db=True) as kodidb:
typus = context(None, plexdb=plexdb, kodidb=kodidb) typus = context(None, plexdb=plexdb, kodidb=kodidb)
for plex_id in plex_ids: for plex_id in plex_ids:
if IS_CANCELED(): if SHOULD_CANCEL():
return False return False
typus.remove(plex_id) typus.remove(plex_id)
if len(plex_ids) < BATCH_SIZE: if len(plex_ids) < BATCH_SIZE:
@ -582,13 +582,13 @@ def sync_from_pms(parent_self, pick_libraries=False):
pick_libraries=True will prompt the user the select the libraries he pick_libraries=True will prompt the user the select the libraries he
wants to sync wants to sync
""" """
global IS_CANCELED global SHOULD_CANCEL
LOG.info('Starting synching sections from the PMS') LOG.info('Starting synching sections from the PMS')
IS_CANCELED = parent_self.isCanceled SHOULD_CANCEL = parent_self.should_cancel
try: try:
return _sync_from_pms(pick_libraries) return _sync_from_pms(pick_libraries)
finally: finally:
IS_CANCELED = None SHOULD_CANCEL = None
LOG.info('Done synching sections from the PMS: %s', app.SYNC.sections) LOG.info('Done synching sections from the PMS: %s', app.SYNC.sections)

View file

@ -38,7 +38,7 @@ SUPPORTED_FILETYPES = (
############################################################################### ###############################################################################
def isCanceled(): def should_cancel():
return app.APP.stop_pkc or app.SYNC.stop_sync return app.APP.stop_pkc or app.SYNC.stop_sync
@ -179,7 +179,7 @@ def _full_sync():
# before. If yes, make sure that hashes are identical. If not, sync it. # before. If yes, make sure that hashes are identical. If not, sync it.
old_plex_ids = db.plex_playlist_ids() old_plex_ids = db.plex_playlist_ids()
for xml_playlist in xml: for xml_playlist in xml:
if isCanceled(): if should_cancel():
return False return False
api = API(xml_playlist) api = API(xml_playlist)
try: try:
@ -211,7 +211,7 @@ def _full_sync():
LOG.info('Could not recreate playlist %s', api.plex_id) LOG.info('Could not recreate playlist %s', api.plex_id)
# Get rid of old Plex playlists that were deleted on the Plex side # Get rid of old Plex playlists that were deleted on the Plex side
for plex_id in old_plex_ids: for plex_id in old_plex_ids:
if isCanceled(): if should_cancel():
return False return False
playlist = db.get_playlist(plex_id=plex_id) playlist = db.get_playlist(plex_id=plex_id)
LOG.debug('Removing outdated Plex playlist from Kodi: %s', playlist) LOG.debug('Removing outdated Plex playlist from Kodi: %s', playlist)
@ -225,7 +225,7 @@ def _full_sync():
old_kodi_paths = db.kodi_playlist_paths() old_kodi_paths = db.kodi_playlist_paths()
for root, _, files in path_ops.walk(v.PLAYLIST_PATH): for root, _, files in path_ops.walk(v.PLAYLIST_PATH):
for f in files: for f in files:
if isCanceled(): if should_cancel():
return False return False
path = path_ops.path.join(root, f) path = path_ops.path.join(root, f)
try: try:
@ -256,7 +256,7 @@ def _full_sync():
except PlaylistError: except PlaylistError:
LOG.info('Skipping Kodi playlist %s', path) LOG.info('Skipping Kodi playlist %s', path)
for kodi_path in old_kodi_paths: for kodi_path in old_kodi_paths:
if isCanceled(): if should_cancel():
return False return False
playlist = db.get_playlist(path=kodi_path) playlist = db.get_playlist(path=kodi_path)
if not playlist: if not playlist:

View file

@ -120,7 +120,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
# Ignore new media added by other addons # Ignore new media added by other addons
continue continue
for j, old_item in enumerate(old): for j, old_item in enumerate(old):
if self.isCanceled(): if self.should_suspend() or self.should_cancel():
# Chances are that we got an empty Kodi playlist due to # Chances are that we got an empty Kodi playlist due to
# Kodi exit # Kodi exit
return return
@ -189,7 +189,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
for j in range(i, len(index)): for j in range(i, len(index)):
index[j] += 1 index[j] += 1
for i in reversed(index): for i in reversed(index):
if self.isCanceled(): if self.should_suspend() or self.should_cancel():
# Chances are that we got an empty Kodi playlist due to # Chances are that we got an empty Kodi playlist due to
# Kodi exit # Kodi exit
return return
@ -212,7 +212,8 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
LOG.info("----===## PlayqueueMonitor stopped ##===----") LOG.info("----===## PlayqueueMonitor stopped ##===----")
def _run(self): def _run(self):
while not self.isCanceled(): while not self.should_cancel():
if self.should_suspend():
if self.wait_while_suspended(): if self.wait_while_suspended():
return return
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
@ -228,4 +229,4 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
# compare old and new playqueue # compare old and new playqueue
self._compare_playqueues(playqueue, kodi_pl) self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl) playqueue.old_kodi_pl = list(kodi_pl)
app.APP.monitor.waitForAbort(0.2) self.sleep(0.2)

View file

@ -312,10 +312,11 @@ class PlexCompanion(backgroundthread.KillableThread):
if httpd: if httpd:
thread = Thread(target=httpd.handle_request) thread = Thread(target=httpd.handle_request)
while not self.isCanceled(): while not self.should_cancel():
# If we are not authorized, sleep # If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a # Otherwise, we trigger a download which leads to a
# re-authorizations # re-authorizations
if self.should_suspend():
if self.wait_while_suspended(): if self.wait_while_suspended():
break break
try: try:
@ -356,6 +357,6 @@ class PlexCompanion(backgroundthread.KillableThread):
app.APP.companion_queue.task_done() app.APP.companion_queue.task_done()
# Don't sleep # Don't sleep
continue continue
app.APP.monitor.waitForAbort(0.05) self.sleep(0.05)
subscription_manager.signal_stop() subscription_manager.signal_stop()
client.stop_all() client.stop_all()

View file

@ -101,7 +101,7 @@ class Service(object):
self._init_done = True self._init_done = True
@staticmethod @staticmethod
def isCanceled(): def should_cancel():
return xbmc.abortRequested or app.APP.stop_pkc return xbmc.abortRequested or app.APP.stop_pkc
def on_connection_check(self, result): def on_connection_check(self, result):
@ -437,7 +437,7 @@ class Service(object):
self.playqueue = playqueue.PlayqueueMonitor() self.playqueue = playqueue.PlayqueueMonitor()
# Main PKC program loop # Main PKC program loop
while not self.isCanceled(): while not self.should_cancel():
# Check for PKC commands from other Python instances # Check for PKC commands from other Python instances
plex_command = utils.window('plexkodiconnect.command') plex_command = utils.window('plexkodiconnect.command')

View file

@ -38,7 +38,9 @@ class Sync(backgroundthread.KillableThread):
self.start_library_sync(show_dialog=True, self.start_library_sync(show_dialog=True,
repair=app.SYNC.run_lib_scan == 'repair', repair=app.SYNC.run_lib_scan == 'repair',
block=True) block=True)
if not self.sync_successful and not self.isSuspended() and not self.isCanceled(): if (not self.sync_successful and
not self.should_suspend() and
not self.should_cancel()):
# ERROR in library sync # ERROR in library sync
LOG.warn('Triggered full/repair sync has not been successful') LOG.warn('Triggered full/repair sync has not been successful')
elif app.SYNC.run_lib_scan == 'fanart': elif app.SYNC.run_lib_scan == 'fanart':
@ -112,7 +114,7 @@ class Sync(backgroundthread.KillableThread):
LOG.info('Not synching Plex artwork - not caching') LOG.info('Not synching Plex artwork - not caching')
return return
if self.image_cache_thread and self.image_cache_thread.is_alive(): if self.image_cache_thread and self.image_cache_thread.is_alive():
self.image_cache_thread.abort() self.image_cache_thread.cancel()
self.image_cache_thread.join() self.image_cache_thread.join()
self.image_cache_thread = artwork.ImageCachingThread() self.image_cache_thread = artwork.ImageCachingThread()
self.image_cache_thread.start() self.image_cache_thread.start()
@ -163,8 +165,9 @@ class Sync(backgroundthread.KillableThread):
utils.init_dbs() utils.init_dbs()
while not self.isCanceled(): while not self.should_cancel():
# In the event the server goes offline # In the event the server goes offline
if self.should_suspend():
if self.wait_while_suspended(): if self.wait_while_suspended():
return return
if not install_sync_done: if not install_sync_done:
@ -188,7 +191,7 @@ class Sync(backgroundthread.KillableThread):
self.start_image_cache_thread() self.start_image_cache_thread()
else: else:
LOG.error('Initial start-up full sync unsuccessful') LOG.error('Initial start-up full sync unsuccessful')
app.APP.monitor.waitForAbort(1) self.sleep(1)
xbmc.executebuiltin('InhibitIdleShutdown(false)') xbmc.executebuiltin('InhibitIdleShutdown(false)')
elif not initial_sync_done: elif not initial_sync_done:
@ -205,7 +208,7 @@ class Sync(backgroundthread.KillableThread):
self.start_image_cache_thread() self.start_image_cache_thread()
else: else:
LOG.info('Startup sync has not yet been successful') LOG.info('Startup sync has not yet been successful')
app.APP.monitor.waitForAbort(1) self.sleep(1)
# Currently no db scan, so we could start a new scan # Currently no db scan, so we could start a new scan
else: else:
@ -240,9 +243,9 @@ class Sync(backgroundthread.KillableThread):
library_sync.store_websocket_message(message) library_sync.store_websocket_message(message)
queue.task_done() queue.task_done()
# Sleep just a bit # Sleep just a bit
app.APP.monitor.waitForAbort(0.01) self.sleep(0.01)
continue continue
app.APP.monitor.waitForAbort(0.1) self.sleep(0.1)
# Shut down playlist monitoring # Shut down playlist monitoring
if playlist_monitor: if playlist_monitor:
playlist_monitor.stop() playlist_monitor.stop()

View file

@ -19,7 +19,7 @@ class WebSocket(backgroundthread.KillableThread):
def __init__(self): def __init__(self):
self.ws = None self.ws = None
self.redirect_uri = None self.redirect_uri = None
self.sleeptime = 0 self.sleeptime = 0.0
super(WebSocket, self).__init__() super(WebSocket, self).__init__()
def process(self, opcode, message): def process(self, opcode, message):
@ -46,15 +46,15 @@ class WebSocket(backgroundthread.KillableThread):
def getUri(self): def getUri(self):
raise NotImplementedError raise NotImplementedError
def __sleep(self): def _sleep_cycle(self):
""" """
Sleeps for 2^self.sleeptime where sleeping period will be doubled with Sleeps for 2^self.sleeptime where sleeping period will be doubled with
each unsuccessful connection attempt. each unsuccessful connection attempt.
Will sleep at most 64 seconds Will sleep at most 64 seconds
""" """
app.APP.monitor.waitForAbort(2**self.sleeptime) self.sleep(2 ** self.sleeptime)
if self.sleeptime < 6: if self.sleeptime < 6:
self.sleeptime += 1 self.sleeptime += 1.0
def run(self): def run(self):
LOG.info("----===## Starting %s ##===----", self.__class__.__name__) LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
@ -69,9 +69,9 @@ class WebSocket(backgroundthread.KillableThread):
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
def _run(self): def _run(self):
while not self.isCanceled(): while not self.should_cancel():
# In the event the server goes offline # In the event the server goes offline
if self.isSuspended(): if self.should_suspend():
# Set in service.py # Set in service.py
if self.ws is not None: if self.ws is not None:
self.ws.close() self.ws.close()
@ -99,11 +99,11 @@ class WebSocket(backgroundthread.KillableThread):
# Server is probably offline # Server is probably offline
LOG.debug("%s: IOError connecting", self.__class__.__name__) LOG.debug("%s: IOError connecting", self.__class__.__name__)
self.ws = None self.ws = None
self.__sleep() self._sleep_cycle()
except websocket.WebSocketTimeoutException: except websocket.WebSocketTimeoutException:
LOG.debug("%s: WebSocketTimeoutException", self.__class__.__name__) LOG.debug("%s: WebSocketTimeoutException", self.__class__.__name__)
self.ws = None self.ws = None
self.__sleep() self._sleep_cycle()
except websocket.WebsocketRedirect as e: except websocket.WebsocketRedirect as e:
LOG.debug('301 redirect detected: %s', e) LOG.debug('301 redirect detected: %s', e)
self.redirect_uri = e.headers.get('location', self.redirect_uri = e.headers.get('location',
@ -111,11 +111,11 @@ class WebSocket(backgroundthread.KillableThread):
if self.redirect_uri: if self.redirect_uri:
self.redirect_uri = self.redirect_uri.decode('utf-8') self.redirect_uri = self.redirect_uri.decode('utf-8')
self.ws = None self.ws = None
self.__sleep() self._sleep_cycle()
except websocket.WebSocketException as e: except websocket.WebSocketException as e:
LOG.debug('%s: WebSocketException: %s', self.__class__.__name__, e) LOG.debug('%s: WebSocketException: %s', self.__class__.__name__, e)
self.ws = None self.ws = None
self.__sleep() self._sleep_cycle()
except Exception as e: except Exception as e:
LOG.error('%s: Unknown exception encountered when ' LOG.error('%s: Unknown exception encountered when '
'connecting: %s', self.__class__.__name__, e) 'connecting: %s', self.__class__.__name__, e)
@ -123,9 +123,9 @@ class WebSocket(backgroundthread.KillableThread):
LOG.error("%s: Traceback:\n%s", LOG.error("%s: Traceback:\n%s",
self.__class__.__name__, traceback.format_exc()) self.__class__.__name__, traceback.format_exc())
self.ws = None self.ws = None
self.__sleep() self._sleep_cycle()
else: else:
self.sleeptime = 0 self.sleeptime = 0.0
except Exception as e: except Exception as e:
LOG.error("%s: Unknown exception encountered: %s", LOG.error("%s: Unknown exception encountered: %s",
self.__class__.__name__, e) self.__class__.__name__, e)
@ -141,7 +141,7 @@ class PMS_Websocket(WebSocket):
""" """
Websocket connection with the PMS for Plex Companion Websocket connection with the PMS for Plex Companion
""" """
def isSuspended(self): def should_suspend(self):
""" """
Returns True if the thread is suspended Returns True if the thread is suspended
""" """
@ -206,7 +206,7 @@ class Alexa_Websocket(WebSocket):
""" """
Websocket connection to talk to Amazon Alexa. Websocket connection to talk to Amazon Alexa.
""" """
def isSuspended(self): def should_suspend(self):
""" """
Overwrite method since we need to check for plex token Overwrite method since we need to check for plex token
""" """

View file

@ -24,7 +24,7 @@ class UserThumbTask(backgroundthread.Task):
def run(self): def run(self):
for user in self.users: for user in self.users:
if self.isCanceled(): if self.should_cancel():
return return
thumb, back = user.thumb, '' thumb, back = user.thumb, ''
self.callback(user, thumb, back) self.callback(user, thumb, back)