Rewire image caching thread

This commit is contained in:
croneter 2018-11-05 15:23:51 +01:00
parent 30d85eebc0
commit e761567592
3 changed files with 119 additions and 106 deletions

View file

@ -9,8 +9,7 @@ import requests
import xbmc
from . import path_ops
from . import utils
from . import backgroundthread, path_ops, utils
from . import state
###############################################################################
@ -19,9 +18,12 @@ LOG = getLogger('PLEX.artwork')
# Disable annoying requests warnings
requests.packages.urllib3.disable_warnings()
ARTWORK_QUEUE = Queue()
IMAGE_CACHING_SUSPENDS = ['SUSPEND_LIBRARY_THREAD', 'DB_SCAN', 'STOP_SYNC']
IMAGE_CACHING_SUSPENDS = [
state.SUSPEND_LIBRARY_THREAD,
state.DB_SCAN
]
if not utils.settings('imageSyncDuringPlayback') == 'true':
IMAGE_CACHING_SUSPENDS.append('SUSPEND_SYNC')
IMAGE_CACHING_SUSPENDS.append(state.SUSPEND_SYNC)
###############################################################################
@ -34,9 +36,7 @@ def double_urldecode(text):
return unquote(unquote(text))
@utils.thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS)
class ImageCachingThread(Thread):
sleep_between = 50
class ImageCachingThread(backgroundthread.KillableThread):
# Potentially issues with limited number of threads
# Hence let Kodi wait till download is successful
timeout = (35.1, 35.1)
@ -45,24 +45,98 @@ class ImageCachingThread(Thread):
self.queue = ARTWORK_QUEUE
Thread.__init__(self)
def isCanceled(self):
return state.STOP_PKC
def isSuspended(self):
return any(IMAGE_CACHING_SUSPENDS)
@staticmethod
def _art_url_generator():
from . import kodidb_functions as kodidb
for kind in ('video', 'music'):
with kodidb.GetKodiDB(kind) as kodi_db:
for kodi_type in ('poster', 'fanart'):
for url in kodi_db.artwork_generator(kodi_type):
yield url
def missing_art_cache_generator(self):
from . import kodidb_functions as kodidb
with kodidb.GetKodiDB('texture') as kodi_db:
for url in self._art_url_generator():
if kodi_db.url_not_yet_cached(url):
yield url
def _cache_url(self, url):
url = double_urlencode(utils.try_encode(url))
sleeptime = 0
while True:
try:
requests.head(
url="http://%s:%s/image/image://%s"
% (state.WEBSERVER_HOST,
state.WEBSERVER_PORT,
url),
auth=(state.WEBSERVER_USERNAME,
state.WEBSERVER_PASSWORD),
timeout=self.timeout)
except requests.Timeout:
# We don't need the result, only trigger Kodi to start the
# download. All is well
break
except requests.ConnectionError:
if self.isCanceled():
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')
# Wait before trying again
if sleeptime > 5:
LOG.error('Repeatedly got ConnectionError for url %s',
double_urldecode(url))
break
LOG.debug('Were trying too hard to download art, server '
'over-loaded. Sleep %s seconds before trying '
'again to download %s',
2**sleeptime, double_urldecode(url))
xbmc.sleep((2**sleeptime) * 1000)
sleeptime += 1
continue
except Exception as err:
LOG.error('Unknown exception for url %s: %s'.
double_urldecode(url), err)
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
break
# We did not even get a timeout
break
def run(self):
LOG.info("---===### Starting ImageCachingThread ###===---")
stopped = self.stopped
suspended = self.suspended
queue = self.queue
sleep_between = self.sleep_between
while not stopped():
# In the event the server goes offline
while suspended():
# Cache already synced artwork first
for url in self.missing_art_cache_generator():
if self.isCanceled():
return
while self.isSuspended():
# Set in service.py
if stopped():
if self.isCanceled():
# Abort was requested while waiting. We should exit
LOG.info("---===### Stopped ImageCachingThread ###===---")
return
xbmc.sleep(1000)
self._cache_url(url)
# Now wait for more stuff to cache - via the queue
while not self.isCanceled():
# In the event the server goes offline
while self.isSuspended():
# Set in service.py
if self.isCanceled():
# Abort was requested while waiting. We should exit
LOG.info("---===### Stopped ImageCachingThread ###===---")
return
xbmc.sleep(1000)
try:
url = queue.get(block=False)
url = self.queue.get(block=False)
except Empty:
xbmc.sleep(1000)
continue
@ -73,52 +147,12 @@ class ImageCachingThread(Thread):
message=url.message,
icon='{plex}',
sound=False)
queue.task_done()
self.queue.task_done()
continue
url = double_urlencode(utils.try_encode(url))
sleeptime = 0
while True:
try:
requests.head(
url="http://%s:%s/image/image://%s"
% (state.WEBSERVER_HOST,
state.WEBSERVER_PORT,
url),
auth=(state.WEBSERVER_USERNAME,
state.WEBSERVER_PASSWORD),
timeout=self.timeout)
except requests.Timeout:
# We don't need the result, only trigger Kodi to start the
# download. All is well
break
except requests.ConnectionError:
if stopped():
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')
# Wait before trying again
if sleeptime > 5:
LOG.error('Repeatedly got ConnectionError for url %s',
double_urldecode(url))
break
LOG.debug('Were trying too hard to download art, server '
'over-loaded. Sleep %s seconds before trying '
'again to download %s',
2**sleeptime, double_urldecode(url))
xbmc.sleep((2**sleeptime) * 1000)
sleeptime += 1
continue
except Exception as err:
LOG.error('Unknown exception for url %s: %s'.
double_urldecode(url), err)
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
break
# We did not even get a timeout
break
queue.task_done()
self._cache_url(url)
self.queue.task_done()
# Sleep for a bit to reduce CPU strain
xbmc.sleep(sleep_between)
xbmc.sleep(100)
LOG.info("---===### Stopped ImageCachingThread ###===---")
@ -127,47 +161,6 @@ class Artwork():
if enableTextureCache:
queue = ARTWORK_QUEUE
def cache_major_artwork(self):
"""
Takes the existing Kodi library and caches posters and fanart.
Necessary because otherwise PKC caches artwork e.g. from fanart.tv
which basically blocks Kodi from getting needed artwork fast (e.g.
while browsing the library)
"""
if not self.enableTextureCache:
return
artworks = list()
# Get all posters and fanart/background for video and music
for kind in ('video', 'music'):
connection = utils.kodi_sql(kind)
cursor = connection.cursor()
for typus in ('poster', 'fanart'):
cursor.execute('SELECT url FROM art WHERE type == ?',
(typus, ))
artworks.extend(cursor.fetchall())
connection.close()
artworks_to_cache = list()
connection = utils.kodi_sql('texture')
cursor = connection.cursor()
for url in artworks:
query = 'SELECT url FROM texture WHERE url == ? LIMIT 1'
cursor.execute(query, (url[0], ))
if not cursor.fetchone():
artworks_to_cache.append(url)
connection.close()
if not artworks_to_cache:
LOG.info('Caching of major images to Kodi texture cache done')
return
length = len(artworks_to_cache)
LOG.info('Caching has not been completed - caching %s major images',
length)
# Caching %s Plex images
self.queue.put(ArtworkSyncMessage(utils.lang(30006) % length))
for url in artworks_to_cache:
self.queue.put(url[0])
# Plex image caching done
self.queue.put(ArtworkSyncMessage(utils.lang(30007)))
def fullTextureCacheSync(self):
"""
This method will sync all Kodi artwork to textures13.db

View file

@ -1199,6 +1199,21 @@ class KodiDBMethods(object):
'''
self.cursor.execute(query, (kodi_id, kodi_type))
def artwork_generator(self, kodi_type):
"""
"""
self.cursor.execute('SELECT url FROM art WHERE type == ?',
(kodi_type, ))
return (x[0] for x in self.cursor)
def url_not_yet_cached(self, url):
"""
Returns True if url has not yet been cached to the Kodi texture cache
"""
self.cursor.execute('SELECT url FROM texture WHERE url == ? LIMIT 1',
(url, ))
return self.cursor.fetchone() is None
def kodiid_from_filename(path, kodi_type=None, db_type=None):
"""

View file

@ -35,8 +35,7 @@ class Sync(backgroundthread.KillableThread):
self.fanart = None
# Show sync dialog even if user deactivated?
self.force_dialog = False
if utils.settings('enableTextureCache') == "true":
self.image_cache_thread = artwork.ImageCachingThread()
self.image_cache_thread = None
# Lock used to wait on a full sync, e.g. on initial sync
# self.lock = backgroundthread.threading.Lock()
super(Sync, self).__init__()
@ -162,6 +161,13 @@ class Sync(backgroundthread.KillableThread):
icon='{plex}',
sound=False)
def start_image_cache_thread(self):
if utils.settings('enableTextureCache') == "true":
self.image_cache_thread = artwork.ImageCachingThread()
self.image_cache_thread.start()
else:
LOG.info('Image caching has been deactivated')
def run(self):
try:
self._run_internal()
@ -250,7 +256,7 @@ class Sync(backgroundthread.KillableThread):
from . import playlists
playlist_monitor = playlists.kodi_playlist_monitor()
self.start_fanart_download(refresh=False)
self.image_cache_thread.start()
self.start_image_cache_thread()
else:
LOG.error('Initial start-up full sync unsuccessful')
self.force_dialog = False
@ -271,9 +277,8 @@ class Sync(backgroundthread.KillableThread):
if library_sync.PLAYLIST_SYNC_ENABLED:
from . import playlists
playlist_monitor = playlists.kodi_playlist_monitor()
artwork.Artwork().cache_major_artwork()
self.start_fanart_download(refresh=False)
self.image_cache_thread.start()
self.start_image_cache_thread()
else:
LOG.info('Startup sync has not yet been successful')