# -*- coding: utf-8 -*- ################################################################################################# import logging import os import urllib from sqlite3 import OperationalError import xbmc import xbmcgui import xbmcvfs import requests import image_cache_thread from utils import window, settings, dialog, language as lang, JSONRPC from database import DatabaseConn ################################################################################################## log = logging.getLogger("EMBY."+__name__) ################################################################################################## class Artwork(object): xbmc_host = 'localhost' xbmc_port = None xbmc_username = None xbmc_password = None image_cache_threads = [] image_cache_limit = 0 def __init__(self): self.enable_texture_cache = settings('enableTextureCache') == "true" self.image_cache_limit = int(settings('imageCacheLimit')) * 5 log.debug("image cache thread count: %s", self.image_cache_limit) if not self.xbmc_port and self.enable_texture_cache: self._set_webserver_details() self.user_id = window('emby_currUser') self.server = window('emby_server%s' % self.user_id) def _double_urlencode(self, text): text = self.single_urlencode(text) text = self.single_urlencode(text) return text @classmethod def single_urlencode(cls, text): # urlencode needs a utf- string text = urllib.urlencode({'blahblahblah': text.encode('utf-8')}) text = text[13:] return text.decode('utf-8') #return the result again as unicode def _set_webserver_details(self): # Get the Kodi webserver details - used to set the texture cache get_setting_value = JSONRPC('Settings.GetSettingValue') web_query = { "setting": "services.webserver" } result = get_setting_value.execute(web_query) 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 set_setting_value = JSONRPC('Settings.SetSettingValue') web_port = { "setting": "services.webserverport", "value": 8080 } set_setting_value.execute(web_port) self.xbmc_port = 8080 web_user = { "setting": "services.webserver", "value": True } set_setting_value.execute(web_user) self.xbmc_username = "kodi" # Webserver already enabled web_port = { "setting": "services.webserverport" } result = get_setting_value.execute(web_port) try: self.xbmc_port = result['result']['value'] except (TypeError, KeyError): pass web_user = { "setting": "services.webserverusername" } result = get_setting_value.execute(web_user) try: self.xbmc_username = result['result']['value'] except (TypeError, KeyError): pass web_pass = { "setting": "services.webserverpassword" } result = get_setting_value.execute(web_pass) try: self.xbmc_password = result['result']['value'] except (TypeError, KeyError): pass def texture_cache_sync(self): # This method will sync all Kodi artwork to textures13.db # and cache them locally. This takes diskspace! if not dialog(type_="yesno", heading="{emby}", line1=lang(33042)): return log.info("Doing Image Cache Sync") pdialog = xbmcgui.DialogProgress() pdialog.create(lang(29999), lang(33043)) # ask to rest all existing or not if dialog(type_="yesno", heading="{emby}", line1=lang(33044)): log.info("Resetting all cache data first") self.delete_cache() # Cache all entries in video DB self._cache_all_video_entries(pdialog) # Cache all entries in music DB self._cache_all_music_entries(pdialog) pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads))) log.info("Waiting for all threads to exit") while len(self.image_cache_threads): for thread in self.image_cache_threads: if thread.is_finished: self.image_cache_threads.remove(thread) pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads))) log.info("Waiting for all threads to exit: %s", len(self.image_cache_threads)) xbmc.sleep(500) pdialog.close() def _cache_all_video_entries(self, pdialog): with DatabaseConn('video') as cursor_video: cursor_video.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors result = cursor_video.fetchall() total = len(result) log.info("Image cache sync about to process %s images", total) cursor_video.close() count = 0 for url in result: if pdialog.iscanceled(): break percentage = int((float(count) / float(total))*100) message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads)) pdialog.update(percentage, "%s %s" % (lang(33045), message)) self.cache_texture(url[0]) count += 1 def _cache_all_music_entries(self, pdialog): with DatabaseConn('music') as cursor_music: cursor_music.execute("SELECT url FROM art") result = cursor_music.fetchall() total = len(result) log.info("Image cache sync about to process %s images", total) count = 0 for url in result: if pdialog.iscanceled(): break percentage = int((float(count) / float(total))*100) message = "%s of %s" % (count, total) pdialog.update(percentage, "%s %s" % (lang(33045), message)) self.cache_texture(url[0]) count += 1 @classmethod def delete_cache(cls): # Remove all existing textures first path = xbmc.translatePath('special://thumbnails/').decode('utf-8') if xbmcvfs.exists(path): dirs, ignore_files = xbmcvfs.listdir(path) for directory in dirs: ignore_dirs, files = xbmcvfs.listdir(path + directory) for file_ in files: if os.path.supports_unicode_filenames: filename = os.path.join(path + directory.decode('utf-8'), file_.decode('utf-8')) else: filename = os.path.join(path.encode('utf-8') + directory, file_) xbmcvfs.delete(filename) log.debug("deleted: %s", filename) # remove all existing data from texture DB with DatabaseConn('texture') as cursor_texture: cursor_texture.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') rows = cursor_texture.fetchall() for row in rows: table_name = row[0] if table_name != "version": cursor_texture.execute("DELETE FROM " + table_name) def _add_worker_image_thread(self, url): while True: # removed finished for thread in self.image_cache_threads: if thread.is_finished: self.image_cache_threads.remove(thread) # add a new thread or wait and retry if we hit our limit if len(self.image_cache_threads) < self.image_cache_limit: new_thread = image_cache_thread.ImageCacheThread() new_thread.set_url(self._double_urlencode(url)) new_thread.set_host(self.xbmc_host, self.xbmc_port) new_thread.set_auth(self.xbmc_username, self.xbmc_password) counter = 0 worked = False while counter < 10: try: new_thread.start() worked = True break except: counter = counter + 1 xbmc.sleep(1000) if(worked): self.image_cache_threads.append(new_thread) return True else: return False else: log.info("Waiting for empty queue spot: %s", len(self.image_cache_threads)) xbmc.sleep(100) def cache_texture(self, url): # Cache a single image url to the texture cache if url and self.enable_texture_cache: log.debug("Processing: %s", url) if not self.image_cache_limit: url = self._double_urlencode(url) try: # Add image to texture cache by simply calling it at the http endpoint requests.head(url=("http://%s:%s/image/image://%s" % (self.xbmc_host, self.xbmc_port, url)), auth=(self.xbmc_username, self.xbmc_password), timeout=(0.01, 0.01)) except Exception: # We don't need the result pass else: self._add_worker_image_thread(url) def add_artwork(self, artwork, kodi_id, media_type, cursor): # Kodi conversion table kodi_artwork = { 'Primary': ["thumb", "poster"], 'Banner': "banner", 'Logo': "clearlogo", 'Art': "clearart", 'Thumb': "landscape", 'Disc': "discart", 'Backdrop': "fanart", 'BoxRear': "poster" } # Artwork is a dictionary for artwork_type in artwork: if artwork_type == 'Backdrop': # Backdrop entry is a list # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...) backdrops = artwork[artwork_type] backdrops_number = len(backdrops) query = ' '.join(( "SELECT url", "FROM art", "WHERE media_id = ?", "AND media_type = ?", "AND type LIKE ?" )) cursor.execute(query, (kodi_id, media_type, "fanart%",)) rows = cursor.fetchall() if len(rows) > backdrops_number: # More backdrops in database. Delete extra fanart. query = ' '.join(( "DELETE FROM art", "WHERE media_id = ?", "AND media_type = ?", "AND type LIKE ?" )) cursor.execute(query, (kodi_id, media_type, "fanart_",)) # Process backdrops and extra fanart for index, backdrop in enumerate(backdrops): self.add_update_art(image_url=backdrop, kodi_id=kodi_id, media_type=media_type, image_type=("fanart" if not index else "%s%s" % ("fanart", index)), cursor=cursor) elif artwork_type == 'Primary': # Primary art is processed as thumb and poster for Kodi. for art_type in kodi_artwork[artwork_type]: self.add_update_art(image_url=artwork[artwork_type], kodi_id=kodi_id, media_type=media_type, image_type=art_type, cursor=cursor) elif artwork_type in kodi_artwork: # Process the rest artwork type that Kodi can use self.add_update_art(image_url=artwork[artwork_type], kodi_id=kodi_id, media_type=media_type, image_type=kodi_artwork[artwork_type], cursor=cursor) def add_update_art(self, image_url, kodi_id, media_type, image_type, cursor): ''' Add or update the artwork (if it changed) to the Kodi database. Update the cache if backdrop or poster image_type. Possible to an empty imageurl since the process is automated. ''' if image_type == 'poster' and media_type in ('song', 'artist', 'album'): log.info("skipping poster for songs, artists, albums.") return if not image_url: log.warn("empty url for: %s/%s", kodi_id, media_type) return cache_image = False query = ' '.join(( "SELECT url", "FROM art", "WHERE media_id = ?", "AND media_type = ?", "AND type = ?" )) cursor.execute(query, (kodi_id, media_type, image_type,)) try: url = cursor.fetchone()[0] except TypeError: cache_image = True log.debug("Adding Art Link for kodiId: %s (%s)", kodi_id, image_url) query = ( ''' INSERT INTO art(media_id, media_type, type, url) VALUES (?, ?, ?, ?) ''' ) cursor.execute(query, (kodi_id, media_type, image_type, image_url)) else: if url != image_url: cache_image = True if (window('emby_initialScan') != "true" and image_type in ("fanart", "poster")): # Delete current entry before updating with the new one self.delete_cached_artwork(url) log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)", image_type, kodi_id, url, image_url) query = ' '.join(( "UPDATE art", "SET url = ?", "WHERE media_id = ?", "AND media_type = ?", "AND type = ?" )) cursor.execute(query, (image_url, kodi_id, media_type, image_type)) if cache_image and image_type in ("fanart", "poster"): self.cache_texture(image_url) def delete_artwork(self, kodi_id, media_type, cursor): query = ' '.join(( "SELECT url, type", "FROM art", "WHERE media_id = ?", "AND media_type = ?" )) cursor.execute(query, (kodi_id, media_type,)) rows = cursor.fetchall() for row in rows: url = row[0] image_type = row[1] if image_type in ("poster", "fanart"): self.delete_cached_artwork(url) @classmethod def delete_cached_artwork(cls, url): # Only necessary to remove and apply a new backdrop or poster with DatabaseConn('texture') as cursor_texture: try: cursor_texture.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) cached_url = cursor_texture.fetchone()[0] except TypeError: log.info("Could not find cached url") except OperationalError: log.info("Database is locked. Skip deletion process.") else: # Delete thumbnail as well as the entry thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8') log.info("Deleting cached thumbnail: %s", thumbnails) xbmcvfs.delete(thumbnails) try: cursor_texture.execute("DELETE FROM texture WHERE url = ?", (url,)) except OperationalError: log.debug("Issue deleting url from cache. Skipping.") def get_people_artwork(self, people): # append imageurl if existing for person in people: image = "" person_id = person['Id'] if 'PrimaryImageTag' in person: image = ( "%s/emby/Items/%s/Images/Primary?" "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s" % (self.server, person_id, person['PrimaryImageTag'])) person['imageurl'] = image return people def get_user_artwork(self, item_id, item_type): # Load user information set by UserClient return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, item_id, item_type) def get_all_artwork(self, item, parent_info=False): item_id = item['Id'] artworks = item['ImageTags'] backdrops = item.get('BackdropImageTags', []) custom_query = "" if settings('compressArt') == "true": custom_query = "&Quality=90" if settings('enableCoverArt') == "false": custom_query += "&EnableImageEnhancers=false" all_artwork = { 'Primary': "", 'Art': "", 'Banner': "", 'Logo': "", 'Thumb': "", 'Disc': "", 'Backdrop': [] } def get_backdrops(item_id, backdrops): for index, tag in enumerate(backdrops): artwork = ("%s/emby/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (self.server, item_id, index, tag, custom_query)) all_artwork['Backdrop'].append(artwork) def get_artwork(item_id, type_, tag, override_name=None): if not tag: return artwork = ("%s/emby/Items/%s/Images/%s/0?Format=original&Tag=%s%s" % (self.server, item_id, type_, tag, custom_query)) all_artwork[override_name or type_] = artwork # Process backdrops get_backdrops(item_id, backdrops) # Process the rest of the artwork for artwork in artworks: # Filter backcover if artwork != "BoxRear": get_artwork(item_id, artwork, artworks[artwork]) # Process parent items if the main item is missing artwork if parent_info: # Process parent backdrops if not all_artwork['Backdrop']: if 'ParentBackdropItemId' in item: # If there is a parent_id, go through the parent backdrop list get_backdrops(item['ParentBackdropItemId'], item['ParentBackdropImageTags']) # Process the rest of the artwork for parent_artwork in ('Logo', 'Art', 'Thumb'): if not all_artwork[parent_artwork]: if 'Parent%sItemId' % parent_artwork in item: get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork, item['Parent%sImageTag' % parent_artwork]) if 'SeriesPrimaryImageTag' in item: get_artwork(item['SeriesId'], "Primary", item['SeriesPrimaryImageTag'], "Series.Primary" if all_artwork['Primary'] else None) # Parent album works a bit differently if not all_artwork['Primary']: if 'AlbumId' in item and 'AlbumPrimaryImageTag' in item: get_artwork(item['AlbumId'], 'Primary', item['AlbumPrimaryImageTag']) return all_artwork