From 07c4d64a84f232d2f21a7c929ca0b8bb1fb798cc Mon Sep 17 00:00:00 2001 From: croneter Date: Mon, 5 Nov 2018 18:00:01 +0100 Subject: [PATCH] Rewire artwork caching --- resources/lib/artwork.py | 332 ++++++++---------------- resources/lib/itemtypes/common.py | 11 +- resources/lib/itemtypes/movies.py | 20 +- resources/lib/itemtypes/music.py | 42 ++- resources/lib/itemtypes/tvshows.py | 38 ++- resources/lib/kodidb_functions.py | 52 +++- resources/lib/library_sync/websocket.py | 43 ++- resources/lib/sync.py | 19 +- resources/lib/utils.py | 4 +- 9 files changed, 249 insertions(+), 312 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index ccdc1cfc..e07c4b91 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -2,11 +2,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from logging import getLogger -from Queue import Queue, Empty from urllib import quote_plus, unquote -from threading import Thread import requests - import xbmc from . import backgroundthread, path_ops, utils @@ -17,7 +14,11 @@ LOG = getLogger('PLEX.artwork') # Disable annoying requests warnings requests.packages.urllib3.disable_warnings() -ARTWORK_QUEUE = Queue() + +# Potentially issues with limited number of threads Hence let Kodi wait till +# download is successful +TIMEOUT = (35.1, 35.1) + IMAGE_CACHING_SUSPENDS = [ state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN @@ -37,20 +38,19 @@ def double_urldecode(text): class ImageCachingThread(backgroundthread.KillableThread): - # Potentially issues with limited number of threads - # Hence let Kodi wait till download is successful - timeout = (35.1, 35.1) - def __init__(self): - self.queue = ARTWORK_QUEUE - Thread.__init__(self) + self._canceled = False + super(ImageCachingThread, self).__init__() def isCanceled(self): - return state.STOP_PKC + return self._canceled or state.STOP_PKC def isSuspended(self): return any(IMAGE_CACHING_SUSPENDS) + def cancel(self): + self._canceled = True + @staticmethod def _art_url_generator(): from . import kodidb_functions as kodidb @@ -67,49 +67,6 @@ class ImageCachingThread(backgroundthread.KillableThread): 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 ###===---") # Cache already synced artwork first @@ -123,167 +80,113 @@ class ImageCachingThread(backgroundthread.KillableThread): 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 = self.queue.get(block=False) - except Empty: - xbmc.sleep(1000) - continue - if isinstance(url, ArtworkSyncMessage): - if state.IMAGE_SYNC_NOTIFICATIONS: - utils.dialog('notification', - heading=utils.lang(29999), - message=url.message, - icon='{plex}', - sound=False) - self.queue.task_done() - continue - self._cache_url(url) - self.queue.task_done() - # Sleep for a bit to reduce CPU strain - xbmc.sleep(100) + cache_url(url) LOG.info("---===### Stopped ImageCachingThread ###===---") -class Artwork(): - enableTextureCache = utils.settings('enableTextureCache') == "true" - if enableTextureCache: - queue = ARTWORK_QUEUE +def cache_url(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=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 state.STOP_PKC: + # 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 fullTextureCacheSync(self): - """ - This method will sync all Kodi artwork to textures13.db - and cache them locally. This takes diskspace! - """ - if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)): - return - LOG.info("Doing Image Cache Sync") +def modify_artwork(artworks, kodi_id, kodi_type, cursor): + """ + Pass in an artworks dict (see PlexAPI) to set an items artwork. + """ + for kodi_art, url in artworks.iteritems(): + modify_art(url, kodi_id, kodi_type, kodi_art, cursor) - # ask to rest all existing or not - if utils.yesno_dialog("Image Texture Cache", utils.lang(39251)): - LOG.info("Resetting all cache data first") - # Remove all existing textures first - path = path_ops.translate_path('special://thumbnails/') - if path_ops.exists(path): - path_ops.rmtree(path, ignore_errors=True) - self.restore_cache_directories() - # remove all existing data from texture DB - connection = utils.kodi_sql('texture') - cursor = connection.cursor() - query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' - cursor.execute(query, ('table', )) - rows = cursor.fetchall() - for row in rows: - tableName = row[0] - if tableName != "version": - cursor.execute("DELETE FROM %s" % tableName) - connection.commit() - connection.close() - - # Cache all entries in video DB - connection = utils.kodi_sql('video') - cursor = connection.cursor() - # dont include actors - query = "SELECT url FROM art WHERE media_type != ?" - cursor.execute(query, ('actor', )) - result = cursor.fetchall() - total = len(result) - LOG.info("Image cache sync about to process %s video images", total) - connection.close() - - for url in result: - self.cache_texture(url[0]) - # Cache all entries in music DB - connection = utils.kodi_sql('music') - cursor = connection.cursor() - cursor.execute("SELECT url FROM art") - result = cursor.fetchall() - total = len(result) - LOG.info("Image cache sync about to process %s music images", total) - connection.close() - for url in result: - self.cache_texture(url[0]) - - def cache_texture(self, url): - ''' - Cache a single image url to the texture cache. url: unicode - ''' - if url and self.enableTextureCache: - self.queue.put(url) - - def modify_artwork(self, artworks, kodi_id, kodi_type, cursor): - """ - Pass in an artworks dict (see PlexAPI) to set an items artwork. - """ - for kodi_art, url in artworks.iteritems(): - self.modify_art(url, kodi_id, kodi_type, kodi_art, cursor) - - def modify_art(self, url, kodi_id, kodi_type, kodi_art, cursor): - """ - Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the - Kodi art table for item kodi_id/kodi_type. Will also cache everything - except actor portraits. - """ +def modify_art(url, kodi_id, kodi_type, kodi_art, cursor): + """ + Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the + Kodi art table for item kodi_id/kodi_type. Will also cache everything + except actor portraits. + """ + query = ''' + SELECT url FROM art + WHERE media_id = ? AND media_type = ? AND type = ? + LIMIT 1 + ''' + cursor.execute(query, (kodi_id, kodi_type, kodi_art,)) + try: + # Update the artwork + old_url = cursor.fetchone()[0] + except TypeError: + # Add the artwork query = ''' - SELECT url FROM art - WHERE media_id = ? AND media_type = ? AND type = ? - LIMIT 1 + INSERT INTO art(media_id, media_type, type, url) + VALUES (?, ?, ?, ?) ''' - cursor.execute(query, (kodi_id, kodi_type, kodi_art,)) - try: - # Update the artwork - old_url = cursor.fetchone()[0] - except TypeError: - # Add the artwork - query = ''' - INSERT INTO art(media_id, media_type, type, url) - VALUES (?, ?, ?, ?) - ''' - cursor.execute(query, (kodi_id, kodi_type, kodi_art, url)) - else: - if url == old_url: - # Only cache artwork if it changed - return - self.delete_cached_artwork(old_url) - query = ''' - UPDATE art SET url = ? - WHERE media_id = ? AND media_type = ? AND type = ? - ''' - cursor.execute(query, (url, kodi_id, kodi_type, kodi_art)) - # Cache fanart and poster in Kodi texture cache - if kodi_type != 'actor': - self.cache_texture(url) + cursor.execute(query, (kodi_id, kodi_type, kodi_art, url)) + else: + if url == old_url: + # Only cache artwork if it changed + return + delete_cached_artwork(old_url) + query = ''' + UPDATE art SET url = ? + WHERE media_id = ? AND media_type = ? AND type = ? + ''' + cursor.execute(query, (url, kodi_id, kodi_type, kodi_art)) - def delete_artwork(self, kodiId, mediaType, cursor): - query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?' - cursor.execute(query, (kodiId, mediaType,)) - for row in cursor.fetchall(): - self.delete_cached_artwork(row[0]) - @staticmethod - def delete_cached_artwork(url): - """ - Deleted the cached artwork with path url (if it exists) - """ - connection = utils.kodi_sql('texture') - cursor = connection.cursor() +def delete_artwork(kodiId, mediaType, cursor): + cursor.execute('SELECT url FROM art WHERE media_id = ? AND media_type = ?', + (kodiId, mediaType, )) + for row in cursor.fetchall(): + delete_cached_artwork(row[0]) + + +def delete_cached_artwork(url): + """ + Deleted the cached artwork with path url (if it exists) + """ + from . import kodidb_functions as kodidb + with kodidb.GetKodiDB('texture') as kodi_db: try: - cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1", - (url,)) - cachedurl = cursor.fetchone()[0] + kodi_db.cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1", + (url, )) + cachedurl = kodi_db.cursor.fetchone()[0] except TypeError: # Could not find cached url pass @@ -293,25 +196,4 @@ class Artwork(): % cachedurl) if path_ops.exists(path): path_ops.rmtree(path, ignore_errors=True) - cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) - connection.commit() - finally: - connection.close() - - @staticmethod - def restore_cache_directories(): - LOG.info("Restoring cache directories...") - paths = ("", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", - "a", "b", "c", "d", "e", "f", - "Video", "plex") - for path in paths: - new_path = path_ops.translate_path("special://thumbnails/%s" % path) - path_ops.makedirs(path_ops.encode_path(new_path)) - - -class ArtworkSyncMessage(object): - """ - Put in artwork queue to display the message as a Kodi notification - """ - def __init__(self, message): - self.message = message + kodi_db.cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) diff --git a/resources/lib/itemtypes/common.py b/resources/lib/itemtypes/common.py index e1e481fc..0f574520 100644 --- a/resources/lib/itemtypes/common.py +++ b/resources/lib/itemtypes/common.py @@ -7,7 +7,7 @@ from ntpath import dirname from ..plex_api import API from ..plex_db import PlexDB from .. import kodidb_functions as kodidb -from .. import artwork, utils, variables as v +from .. import artwork, utils LOG = getLogger('PLEX.itemtypes.common') @@ -41,7 +41,6 @@ class ItemBase(object): """ def __init__(self, last_sync, plexdb=None, kodi_db=None): self.last_sync = last_sync - self.artwork = artwork.Artwork() self.plexconn = None self.plexcursor = plexdb.cursor if plexdb else None self.kodiconn = None @@ -75,10 +74,10 @@ class ItemBase(object): """ Writes artworks [dict containing only set artworks] to the Kodi art DB """ - self.artwork.modify_artwork(artworks, - kodi_id, - kodi_type, - self.kodicursor) + artwork.modify_artwork(artworks, + kodi_id, + kodi_type, + self.kodicursor) def update_userdata(self, xml_element, plex_type): """ diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index 791a4aac..f340216e 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -5,7 +5,7 @@ from logging import getLogger from .common import ItemBase from ..plex_api import API -from .. import state, variables as v, plex_functions as PF +from .. import artwork, state, variables as v, plex_functions as PF LOG = getLogger('PLEX.movies') @@ -165,10 +165,10 @@ class Movie(ItemBase): v.KODI_TYPE_MOVIE, api.people_list()) self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, genres) - self.artwork.modify_artwork(api.artwork(), - kodi_id, - v.KODI_TYPE_MOVIE, - self.kodicursor) + artwork.modify_artwork(api.artwork(), + kodi_id, + v.KODI_TYPE_MOVIE, + self.kodicursor) self.kodi_db.modify_streams(file_id, api.mediastreams(), runtime) self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, studios) tags = [section_name] @@ -190,10 +190,10 @@ class Movie(ItemBase): coll_plex_id) continue set_api = API(set_xml[0]) - self.artwork.modify_artwork(set_api.artwork(), - kodi_set_id, - v.KODI_TYPE_SET, - self.kodicursor) + artwork.modify_artwork(set_api.artwork(), + kodi_set_id, + v.KODI_TYPE_SET, + self.kodicursor) break self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_MOVIE, tags) # Process playstate @@ -230,7 +230,7 @@ class Movie(ItemBase): # Remove the plex reference self.plexdb.remove(plex_id, v.PLEX_TYPE_MOVIE) # Remove artwork - self.artwork.delete_artwork(kodi_id, kodi_type, self.self.kodicursor) + artwork.delete_artwork(kodi_id, kodi_type, self.self.kodicursor) set_id = self.kodi_db.get_set_id(kodi_id) self.kodi_db.modify_countries(kodi_id, kodi_type) self.kodi_db.modify_people(kodi_id, kodi_type) diff --git a/resources/lib/itemtypes/music.py b/resources/lib/itemtypes/music.py index a0334d1e..6b3a455d 100644 --- a/resources/lib/itemtypes/music.py +++ b/resources/lib/itemtypes/music.py @@ -6,7 +6,7 @@ from logging import getLogger from .common import ItemBase from ..plex_api import API from ..plex_db import PlexDB -from .. import kodidb_functions as kodidb +from .. import artwork, kodidb_functions as kodidb from .. import plex_functions as PF, utils, state, variables as v LOG = getLogger('PLEX.music') @@ -106,7 +106,7 @@ class MusicMixin(object): self.kodi_db.delete_song_from_song_genre(kodi_id) query = 'DELETE FROM albuminfosong WHERE idAlbumInfoSong = ?' self.kodicursor.execute(query, (kodi_id, )) - self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_SONG, self.kodicursor) + artwork.delete_artwork(kodi_id, v.KODI_TYPE_SONG, self.kodicursor) def remove_album(self, kodi_id): ''' @@ -121,7 +121,7 @@ class MusicMixin(object): (kodi_id, )) self.kodicursor.execute('DELETE FROM album WHERE idAlbum = ?', (kodi_id, )) - self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_ALBUM, self.kodicursor) + artwork.delete_artwork(kodi_id, v.KODI_TYPE_ALBUM, self.kodicursor) def remove_artist(self, kodi_id): ''' @@ -135,9 +135,7 @@ class MusicMixin(object): (kodi_id, )) self.kodicursor.execute('DELETE FROM discography WHERE idArtist = ?', (kodi_id, )) - self.artwork.delete_artwork(kodi_id, - v.KODI_TYPE_ARTIST, - self.kodicursor) + artwork.delete_artwork(kodi_id, v.KODI_TYPE_ARTIST, self.kodicursor) class Artist(MusicMixin, ItemBase): @@ -205,10 +203,10 @@ class Artist(MusicMixin, ItemBase): utils.unix_date_to_kodi(self.last_sync), kodi_id)) # Update artwork - self.artwork.modify_artwork(artworks, - kodi_id, - v.KODI_TYPE_ARTIST, - self.kodicursor) + artwork.modify_artwork(artworks, + kodi_id, + v.KODI_TYPE_ARTIST, + self.kodicursor) self.plexdb.add_artist(plex_id, api.checksum(), section_id, @@ -366,10 +364,10 @@ class Album(MusicMixin, ItemBase): genres, v.KODI_TYPE_ALBUM) # Update artwork - self.artwork.modify_artwork(artworks, - kodi_id, - v.KODI_TYPE_ALBUM, - self.kodicursor) + artwork.modify_artwork(artworks, + kodi_id, + v.KODI_TYPE_ALBUM, + self.kodicursor) self.plexdb.add_album(plex_id, api.checksum(), section_id, @@ -734,16 +732,16 @@ class Song(MusicMixin, ItemBase): if genres: self.kodi_db.add_music_genres(kodi_id, genres, v.KODI_TYPE_SONG) artworks = api.artwork() - self.artwork.modify_artwork(artworks, - kodi_id, - v.KODI_TYPE_SONG, - self.kodicursor) + artwork.modify_artwork(artworks, + kodi_id, + v.KODI_TYPE_SONG, + self.kodicursor) if xml.get('parentKey') is None: # Update album artwork - self.artwork.modify_artwork(artworks, - parent_id, - v.KODI_TYPE_ALBUM, - self.kodicursor) + artwork.modify_artwork(artworks, + parent_id, + v.KODI_TYPE_ALBUM, + self.kodicursor) # Create the reference in plex table self.plexdb.add_song(plex_id, api.checksum(), diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index 38843fef..db7ac7e1 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -5,7 +5,7 @@ from logging import getLogger from .common import ItemBase, process_path from ..plex_api import API -from .. import plex_functions as PF, state, variables as v +from .. import artwork, plex_functions as PF, state, variables as v LOG = getLogger('PLEX.tvshows') @@ -77,9 +77,7 @@ class TvShowMixin(object): self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW) - self.artwork.delete_artwork(kodi_id, - v.KODI_TYPE_SHOW, - self.kodicursor) + artwork.delete_artwork(kodi_id, v.KODI_TYPE_SHOW, self.kodicursor) self.kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,)) if v.KODIVERSION >= 17: @@ -91,9 +89,7 @@ class TvShowMixin(object): """ Remove a season, and only a season, not the show or episodes """ - self.artwork.delete_artwork(kodi_id, - v.KODI_TYPE_SEASON, - self.kodicursor) + artwork.delete_artwork(kodi_id, v.KODI_TYPE_SEASON, self.kodicursor) self.kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodi_id,)) LOG.debug("Removed season: %s", kodi_id) @@ -104,9 +100,7 @@ class TvShowMixin(object): """ self.kodi_db.modify_people(kodi_id, v.KODI_TYPE_EPISODE) self.kodi_db.remove_file(file_id, plex_type=v.PLEX_TYPE_EPISODE) - self.artwork.delete_artwork(kodi_id, - v.KODI_TYPE_EPISODE, - self.kodicursor) + artwork.delete_artwork(kodi_id, v.KODI_TYPE_EPISODE, self.kodicursor) self.kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,)) if v.KODIVERSION >= 17: @@ -252,10 +246,10 @@ class Show(ItemBase, TvShowMixin): v.KODI_TYPE_SHOW, api.people_list()) self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_SHOW, genres) - self.artwork.modify_artwork(api.artwork(), - kodi_id, - v.KODI_TYPE_SHOW, - self.kodicursor) + artwork.modify_artwork(api.artwork(), + kodi_id, + v.KODI_TYPE_SHOW, + self.kodicursor) # Process studios self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_SHOW, studios) # Process tags: view, PMS collection tags @@ -301,10 +295,10 @@ class Season(ItemBase, TvShowMixin): return parent_id = show['kodi_id'] kodi_id = self.kodi_db.add_season(parent_id, api.season_number()) - self.artwork.modify_artwork(api.artwork(), - kodi_id, - v.KODI_TYPE_SEASON, - self.kodicursor) + artwork.modify_artwork(api.artwork(), + kodi_id, + v.KODI_TYPE_SEASON, + self.kodicursor) self.plexdb.add_season(plex_id=plex_id, checksum=api.checksum(), section_id=section_id, @@ -505,10 +499,10 @@ class Episode(ItemBase, TvShowMixin): self.kodi_db.modify_people(kodi_id, v.KODI_TYPE_EPISODE, api.people_list()) - self.artwork.modify_artwork(api.artwork(), - kodi_id, - v.KODI_TYPE_EPISODE, - self.kodicursor) + artwork.modify_artwork(api.artwork(), + kodi_id, + v.KODI_TYPE_EPISODE, + self.kodicursor) streams = api.mediastreams() self.kodi_db.modify_streams(kodi_fileid, streams, api.runtime()) self.kodi_db.set_resume(kodi_fileid, diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 7dd9c7cc..1979c1bb 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -13,8 +13,6 @@ from . import artwork, utils, variables as v, state, path_ops LOG = getLogger('PLEX.kodidb_functions') -############################################################################### - class GetKodiDB(object): """ @@ -49,7 +47,6 @@ class KodiDBMethods(object): """ def __init__(self, cursor): self.cursor = cursor - self.artwork = artwork.Artwork() def setup_path_table(self): """ @@ -460,7 +457,7 @@ class KodiDBMethods(object): self.cursor.execute(query_actor_delete, (person[0],)) if kind == 'actor': # Delete any associated artwork - self.artwork.delete_artwork(person[0], 'actor', self.cursor) + artwork.delete_artwork(person[0], 'actor', self.cursor) # Save new people to Kodi DB by iterating over the remaining entries if kind == 'actor': query = 'INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)' @@ -506,11 +503,11 @@ class KodiDBMethods(object): 'VALUES (?, ?)', (actor_id, name)) if art_url: - self.artwork.modify_art(art_url, - actor_id, - 'actor', - 'thumb', - self.cursor) + artwork.modify_art(art_url, + actor_id, + 'actor', + 'thumb', + self.cursor) return actor_id def get_art(self, kodi_id, kodi_type): @@ -1199,6 +1196,11 @@ class KodiDBMethods(object): ''' self.cursor.execute(query, (kodi_id, kodi_type)) + def art_urls(self, kodi_id, kodi_type): + self.cursor.execute('SELECT url FROM art WHERE media_id = ? AND media_type = ?', + (kodi_id, kodi_type)) + return (x[0] for x in self.cursor) + def artwork_generator(self, kodi_type): """ """ @@ -1275,6 +1277,27 @@ def setup_kodi_default_entries(): utils.unix_timestamp()))) +def reset_cached_images(): + LOG.info('Resetting cached artwork') + # Remove all existing textures first + path = path_ops.translate_path('special://thumbnails/') + if path_ops.exists(path): + path_ops.rmtree(path, ignore_errors=True) + paths = ('', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', + 'Video', 'plex') + for path in paths: + new_path = path_ops.translate_path('special://thumbnails/%s' % path) + path_ops.makedirs(path_ops.encode_path(new_path)) + with GetKodiDB('texture') as kodi_db: + query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' + kodi_db.cursor.execute(query, ('table', )) + rows = kodi_db.cursor.fetchall() + for row in rows: + if row[0] != 'version': + kodi_db.cursor.execute("DELETE FROM %s" % row[0]) + + def wipe_dbs(): """ Completely resets the Kodi databases 'video', 'texture' and 'music' (if @@ -1304,3 +1327,14 @@ def wipe_dbs(): xbmc.executebuiltin('UpdateLibrary(video)') if utils.settings('enableMusic') == 'true': xbmc.executebuiltin('UpdateLibrary(music)') + + +KODIDB_FROM_PLEXTYPE = { + v.PLEX_TYPE_MOVIE: GetKodiDB('video'), + v.PLEX_TYPE_SHOW: GetKodiDB('video'), + v.PLEX_TYPE_SEASON: GetKodiDB('video'), + v.PLEX_TYPE_EPISODE: GetKodiDB('video'), + v.PLEX_TYPE_ARTIST: GetKodiDB('music'), + v.PLEX_TYPE_ALBUM: GetKodiDB('music'), + v.PLEX_TYPE_SONG: GetKodiDB('music') +} diff --git a/resources/lib/library_sync/websocket.py b/resources/lib/library_sync/websocket.py index 7e721616..283532f3 100644 --- a/resources/lib/library_sync/websocket.py +++ b/resources/lib/library_sync/websocket.py @@ -8,11 +8,14 @@ from .full_sync import PLAYLIST_SYNC_ENABLED from .fanart import SYNC_FANART, FanartTask from ..plex_api import API from ..plex_db import PlexDB +from .. import kodidb_functions as kodidb from .. import backgroundthread, playlists, plex_functions as PF, itemtypes -from .. import utils, variables as v, state +from .. import artwork, utils, variables as v, state LOG = getLogger('PLEX.sync.websocket') +CACHING_ENALBED = utils.settings('enableTextureCache') == "true" + WEBSOCKET_MESSAGES = [] # Dict to save info for Plex items currently being played somewhere PLAYSTATE_SESSIONS = {} @@ -116,24 +119,26 @@ def process_websocket_messages(): def process_new_item_message(message): - xml = PF.GetPlexMetadata(message['ratingKey']) + plex_id = message['ratingKey'] + xml = PF.GetPlexMetadata(plex_id) try: plex_type = xml[0].attrib['type'] except (IndexError, KeyError, TypeError): - LOG.error('Could not download metadata for %s', message['ratingKey']) + LOG.error('Could not download metadata for %s', plex_id) return False, False, False - LOG.debug("Processing new/updated PMS item: %s", message['ratingKey']) - typus = itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](utils.unix_timestamp()) - typus.add_update(xml[0], - section_name=xml.get('librarySectionTitle'), - section_id=xml.get('librarySectionID')) + LOG.debug("Processing new/updated PMS item: %s", plex_id) + with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](utils.unix_timestamp()) as typus: + typus.add_update(xml[0], + section_name=xml.get('librarySectionTitle'), + section_id=xml.get('librarySectionID')) + cache_artwork(plex_id, plex_type) return True, plex_type in v.PLEX_VIDEOTYPES, plex_type in v.PLEX_AUDIOTYPES def process_delete_message(message): plex_type = message['type'] - typus = itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) - typus.remove(message['ratingKey'], plex_type=plex_type) + with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as typus: + typus.remove(message['ratingKey'], plex_type=plex_type) return True, plex_type in v.PLEX_VIDEOTYPES, plex_type in v.PLEX_AUDIOTYPES @@ -340,3 +345,21 @@ def process_playing(data): session['file_id'], utils.unix_timestamp(), v.PLEX_TYPE_FROM_KODI_TYPE[session['kodi_type']]) + + +def cache_artwork(plex_id, plex_type, kodi_id=None, kodi_type=None): + """ + Triggers caching of artwork (if so enabled in the PKC settings) + """ + if not CACHING_ENALBED: + return + if not kodi_id: + with PlexDB() as plexdb: + item = plexdb.item_by_id(plex_id, plex_type) + if not item: + LOG.error('Could not retrieve Plex db info for %s', plex_id) + return + kodi_id, kodi_type = item['kodi_id'], item['kodi_type'] + with kodidb.KODIDB_FROM_PLEXTYPE[plex_type]() as kodi_db: + for url in kodi_db.art_urls(kodi_id, kodi_type): + artwork.cache_url(url) diff --git a/resources/lib/sync.py b/resources/lib/sync.py index 10e54c7f..f1d576d3 100644 --- a/resources/lib/sync.py +++ b/resources/lib/sync.py @@ -115,7 +115,13 @@ class Sync(backgroundthread.KillableThread): icon='{plex}', sound=False) elif state.RUN_LIB_SCAN == 'textures': - artwork.Artwork().fullTextureCacheSync() + LOG.info("Caching of images requested") + if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)): + return + # ask to reset all existing or not + if utils.yesno_dialog('Image Texture Cache', utils.lang(39251)): + kodidb.reset_cached_images() + self.start_image_cache_thread() def on_library_scan_finished(self, successful): """ @@ -162,11 +168,14 @@ class Sync(backgroundthread.KillableThread): 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: + if not utils.settings('enableTextureCache') == "true": LOG.info('Image caching has been deactivated') + return + if self.image_cache_thread and self.image_cache_thread.is_alive(): + self.image_cache_thread.cancel() + self.image_cache_thread.join() + self.image_cache_thread = artwork.ImageCachingThread() + self.image_cache_thread.start() def run(self): try: diff --git a/resources/lib/utils.py b/resources/lib/utils.py index d656515c..d4adb497 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -536,9 +536,7 @@ def wipe_database(): LOG.info("Resetting all cached artwork.") # Remove all cached artwork - path = path_ops.translate_path("special://thumbnails/") - if path_ops.exists(path): - path_ops.rmtree(path, ignore_errors=True) + kodidb_functions.reset_cached_images() # reset the install run flag settings('SyncInstallRunDone', value="false") LOG.info('Wiping done')