# -*- coding: utf-8 -*- ############################################################################### import logging from json import dumps, loads import requests from os import path as os_path from urllib import quote_plus, unquote from threading import Thread from Queue import Queue, Empty from xbmc import executeJSONRPC, sleep, translatePath from xbmcvfs import listdir, delete from utils import window, settings, language as lang, kodiSQL, tryEncode, \ tryDecode, IfExists, ThreadMethods, ThreadMethodsAdditionalStop, dialog # Disable annoying requests warnings import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() ############################################################################### log = logging.getLogger("PLEX."+__name__) ############################################################################### ARTWORK_QUEUE = Queue() def setKodiWebServerDetails(): """ Get the Kodi webserver details - used to set the texture cache """ xbmc_port = None xbmc_username = None xbmc_password = None web_query = { "jsonrpc": "2.0", "id": 1, "method": "Settings.GetSettingValue", "params": { "setting": "services.webserver" } } result = executeJSONRPC(dumps(web_query)) result = loads(result) try: xbmc_webserver_enabled = result['result']['value'] except (KeyError, TypeError): xbmc_webserver_enabled = False if not xbmc_webserver_enabled: # Enable the webserver, it is disabled web_port = { "jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": { "setting": "services.webserverport", "value": 8080 } } result = executeJSONRPC(dumps(web_port)) xbmc_port = 8080 web_user = { "jsonrpc": "2.0", "id": 1, "method": "Settings.SetSettingValue", "params": { "setting": "services.webserver", "value": True } } result = executeJSONRPC(dumps(web_user)) xbmc_username = "kodi" # Webserver already enabled web_port = { "jsonrpc": "2.0", "id": 1, "method": "Settings.GetSettingValue", "params": { "setting": "services.webserverport" } } result = executeJSONRPC(dumps(web_port)) result = loads(result) try: xbmc_port = result['result']['value'] except (TypeError, KeyError): pass web_user = { "jsonrpc": "2.0", "id": 1, "method": "Settings.GetSettingValue", "params": { "setting": "services.webserverusername" } } result = executeJSONRPC(dumps(web_user)) result = loads(result) try: xbmc_username = result['result']['value'] except TypeError: pass web_pass = { "jsonrpc": "2.0", "id": 1, "method": "Settings.GetSettingValue", "params": { "setting": "services.webserverpassword" } } result = executeJSONRPC(dumps(web_pass)) result = loads(result) try: xbmc_password = result['result']['value'] except TypeError: pass return (xbmc_port, xbmc_username, xbmc_password) def double_urlencode(text): return quote_plus(quote_plus(text)) def double_urldecode(text): return unquote(unquote(text)) def get_uncached_artwork(): """ Returns a list of URLs that haven't been cached yet """ all_urls = [] cached_urls = [] result = [] connection = kodiSQL('video') cursor = connection.cursor() # Get all artwork urls cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") for url in cursor.fetchall(): all_urls.append(url[0]) connection.close() connection = kodiSQL('music') cursor = connection.cursor() cursor.execute("SELECT url FROM art") for url in cursor.fetchall(): all_urls.append(url[0]) connection.close() # Get the cached urls connection = kodiSQL('texture') cursor = connection.cursor() cursor.execute("SELECT url FROM texture") for url in cursor.fetchall(): cached_urls.append(url[0]) connection.close() for url in all_urls: if url not in cached_urls: result.append(url) log.info('%s artwork urls have not been cached yet' % len(result)) return result @ThreadMethodsAdditionalStop('plex_shouldStop') @ThreadMethods class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() sleep_between = 50 # 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) def threadSuspended(self): # Overwrite method to add TWO additional suspends return (self._threadSuspended or window('suspend_LibraryThread') or window('plex_dbScan')) def run(self): threadStopped = self.threadStopped threadSuspended = self.threadSuspended queue = self.queue sleep_between = self.sleep_between while not threadStopped(): # In the event the server goes offline while threadSuspended(): # Set in service.py if threadStopped(): # Abort was requested while waiting. We should exit log.info("---===### Stopped Image_Cache_Thread ###===---") return sleep(1000) try: url = queue.get(block=False) except Empty: sleep(1000) continue sleeptime = 0 while True: try: requests.head( url="http://%s:%s/image/image://%s" % (self.xbmc_host, self.xbmc_port, url), auth=(self.xbmc_username, self.xbmc_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 threadStopped(): # 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))) sleep((2**sleeptime)*1000) sleeptime += 1 continue except Exception as e: log.error('Unknown exception for url %s: %s' % (double_urldecode(url), e)) import traceback log.error("Traceback:\n%s" % traceback.format_exc()) break # We did not even get a timeout break queue.task_done() log.debug('Cached art: %s' % double_urldecode(url)) # Sleep for a bit to reduce CPU strain sleep(sleep_between) log.info("---===### Stopped Image_Cache_Thread ###===---") class Artwork(): enableTextureCache = settings('enableTextureCache') == "true" if enableTextureCache: queue = ARTWORK_QUEUE def fullTextureCacheSync(self): """ This method will sync all Kodi artwork to textures13.db and cache them locally. This takes diskspace! """ if not dialog('yesno', "Image Texture Cache", lang(39250)): return log.info("Doing Image Cache Sync") # ask to rest all existing or not if dialog('yesno', "Image Texture Cache", lang(39251)): log.info("Resetting all cache data first") # Remove all existing textures first path = tryDecode(translatePath("special://thumbnails/")) if IfExists(path): allDirs, allFiles = listdir(path) for dir in allDirs: allDirs, allFiles = listdir(path+dir) for file in allFiles: if os_path.supports_unicode_filenames: delete(os_path.join( path + tryDecode(dir), tryDecode(file))) else: delete(os_path.join( tryEncode(path) + dir, file)) # remove all existing data from texture DB connection = kodiSQL('texture') cursor = connection.cursor() cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') rows = cursor.fetchall() for row in rows: tableName = row[0] if tableName != "version": cursor.execute("DELETE FROM " + tableName) connection.commit() connection.close() # Cache all entries in video DB connection = kodiSQL('video') cursor = connection.cursor() # dont include actors cursor.execute("SELECT url FROM art WHERE media_type != '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.cacheTexture(url[0]) # Cache all entries in music DB connection = kodiSQL('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.cacheTexture(url[0]) def cacheTexture(self, url): # Cache a single image url to the texture cache if url and self.enableTextureCache: self.queue.put(double_urlencode(tryEncode(url))) def addArtwork(self, artwork, kodiId, mediaType, cursor): # Kodi conversion table kodiart = { 'Primary': ["thumb", "poster"], 'Banner': "banner", 'Logo': "clearlogo", 'Art': "clearart", 'Thumb': "landscape", 'Disc': "discart", 'Backdrop': "fanart", 'BoxRear': "poster" } # Artwork is a dictionary for art in artwork: if art == "Backdrop": # Backdrop entry is a list # Process extra fanart for artwork downloader (fanart, fanart1, # fanart2...) backdrops = artwork[art] backdropsNumber = len(backdrops) query = ' '.join(( "SELECT url", "FROM art", "WHERE media_id = ?", "AND media_type = ?", "AND type LIKE ?" )) cursor.execute(query, (kodiId, mediaType, "fanart%",)) rows = cursor.fetchall() if len(rows) > backdropsNumber: # More backdrops in database. Delete extra fanart. query = ' '.join(( "DELETE FROM art", "WHERE media_id = ?", "AND media_type = ?", "AND type LIKE ?" )) cursor.execute(query, (kodiId, mediaType, "fanart_",)) # Process backdrops and extra fanart index = "" for backdrop in backdrops: self.addOrUpdateArt( imageUrl=backdrop, kodiId=kodiId, mediaType=mediaType, imageType="%s%s" % ("fanart", index), cursor=cursor) if backdropsNumber > 1: try: # Will only fail on the first try, str to int. index += 1 except TypeError: index = 1 elif art == "Primary": # Primary art is processed as thumb and poster for Kodi. for artType in kodiart[art]: self.addOrUpdateArt( imageUrl=artwork[art], kodiId=kodiId, mediaType=mediaType, imageType=artType, cursor=cursor) elif kodiart.get(art): # Process the rest artwork type that Kodi can use self.addOrUpdateArt( imageUrl=artwork[art], kodiId=kodiId, mediaType=mediaType, imageType=kodiart[art], cursor=cursor) def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor): if not imageUrl: # Possible that the imageurl is an empty string return query = ' '.join(( "SELECT url", "FROM art", "WHERE media_id = ?", "AND media_type = ?", "AND type = ?" )) cursor.execute(query, (kodiId, mediaType, imageType,)) try: # Update the artwork url = cursor.fetchone()[0] except TypeError: # Add the artwork log.debug("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl)) query = ( ''' INSERT INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?) ''' ) cursor.execute(query, (kodiId, mediaType, imageType, imageUrl)) else: if url == imageUrl: # Only cache artwork if it changed return # Only for the main backdrop, poster if (window('plex_initialScan') != "true" and imageType in ("fanart", "poster")): # Delete current entry before updating with the new one self.deleteCachedArtwork(url) log.debug("Updating Art url for %s kodiId %s %s -> (%s)" % (imageType, kodiId, url, imageUrl)) query = ' '.join(( "UPDATE art", "SET url = ?", "WHERE media_id = ?", "AND media_type = ?", "AND type = ?" )) cursor.execute(query, (imageUrl, kodiId, mediaType, imageType)) # Cache fanart and poster in Kodi texture cache if mediaType != 'actor': self.cacheTexture(imageUrl) def deleteArtwork(self, kodiId, mediaType, cursor): query = ' '.join(( "SELECT url", "FROM art", "WHERE media_id = ?", "AND media_type = ?" )) cursor.execute(query, (kodiId, mediaType,)) rows = cursor.fetchall() for row in rows: self.deleteCachedArtwork(row[0]) def deleteCachedArtwork(self, url): # Only necessary to remove and apply a new backdrop or poster connection = kodiSQL('texture') cursor = connection.cursor() try: cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) cachedurl = cursor.fetchone()[0] except TypeError: log.info("Could not find cached url.") else: # Delete thumbnail as well as the entry thumbnails = tryDecode( translatePath("special://thumbnails/%s" % cachedurl)) log.debug("Deleting cached thumbnail: %s" % thumbnails) try: delete(thumbnails) except Exception as e: log.error('Could not delete cached artwork %s. Error: %s' % (thumbnails, e)) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: connection.close()