import os import re import threading import urllib try: import urllib.parse as urlparse except ImportError: # py2 import urlparse import xbmc import xbmcvfs from contextlib import closing from lib import cleaner from lib.libs import mediainfo as info, mediatypes, pykodi, quickjson, utils from lib.libs.addonsettings import settings from lib.libs.pykodi import localize as L, log from lib.libs.webhelper import Getter, GetterError CANT_CONTACT_PROVIDER = 32034 HTTP_ERROR = 32035 CANT_WRITE_TO_FILE = 32037 REMOTE_CONTROL_REQUIRED = 32039 FILEERROR_LIMIT = 3 PROVIDERERROR_LIMIT = 3 TEMP_DIR = 'special://temp/recycledartwork/' typemap = {'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif'} # REVIEW: Deleting replaced artwork. If [movie base name]-fanart.jpg exists and AB is # configured for fanart.jpg, downloading a new artwork will save to the short name but # leave the long name, and the next scan will pick up the long name. # ditto scanning 'logo.png' at first and saving new 'clearlogo.png', but clearlogo will be picked # first by the next scan so that's not such a big deal. class FileManager(object): def __init__(self, debug=False, bigcache=False): self.getter = Getter() self.getter.session.headers['User-Agent'] = settings.useragent self.size = 0 self.fileerror_count = 0 self.provider_errors = {} self.debug = debug self.alreadycached = None if not bigcache else [] self._build_imagecachebase() def _build_imagecachebase(self): result = pykodi.execute_jsonrpc({"jsonrpc": "2.0", "id": 1, "method": "Settings.GetSettings", "params": {"filter": {"category": "control", "section": "services"}}}) port = 80 username = '' password = '' secure = False server_enabled = True if result.get('result', {}).get('settings'): for setting in result['result']['settings']: if setting['id'] == 'services.webserver' and not setting['value']: server_enabled = False break if setting['id'] == 'services.webserverusername': username = setting['value'] elif setting['id'] == 'services.webserverport': port = setting['value'] elif setting['id'] == 'services.webserverpassword': password = setting['value'] elif setting['id'] == 'services.webserverssl' and setting['value']: secure = True username = '{0}:{1}@'.format(username, password) if username and password else '' else: server_enabled = False if server_enabled: protocol = 'https' if secure else 'http' self.imagecachebase = '{0}://{1}localhost:{2}/image/'.format(protocol, username, port) else: self.imagecachebase = None log(L(REMOTE_CONTROL_REQUIRED), xbmc.LOGWARNING) def downloadfor(self, mediaitem, allartwork=True): if self.fileerror_count >= FILEERROR_LIMIT: return False, '' if not info.can_saveartwork(mediaitem): return False, '' to_download = get_downloadable_art(mediaitem, allartwork) if not to_download: return False, '' services_hit = False error = '' localfiles = get_local_art(mediaitem, allartwork) for arttype, url in to_download.items(): hostname = urlparse.urlparse(url).netloc if self.provider_errors.get(hostname, 0) >= PROVIDERERROR_LIMIT: continue full_basefilepath = info.build_artwork_basepath(mediaitem, arttype) if not full_basefilepath: continue if self.debug: mediaitem.downloadedart[arttype] = full_basefilepath + '.ext' continue result, err = self.doget(url) if err: error = err self.provider_errors[hostname] = self.provider_errors.get(hostname, 0) + 1 continue if not result: # 404 URL dead, wipe it so we can add another one later mediaitem.downloadedart[arttype] = None continue self.size += int(result.headers.get('content-length', 0)) services_hit = True ext = get_file_extension(result.headers.get('content-type'), url) if not ext: log("Can't determine extension for '{0}'\nfor image type '{1}'".format(url, arttype)) continue full_basefilepath += '.' + ext if xbmcvfs.exists(full_basefilepath): if extrafanart_name_used(full_basefilepath, localfiles): # REVIEW: can this happen in any other circumstance? full_basefilepath = get_next_filename(full_basefilepath, localfiles) localfiles.append(full_basefilepath) if xbmcvfs.exists(full_basefilepath) and settings.recycle_removed: recyclefile(full_basefilepath) else: folder = os.path.dirname(full_basefilepath) if not xbmcvfs.exists(folder): xbmcvfs.mkdirs(folder) # For now this just downloads the whole thing in memory, then saves it to file. # Maybe chunking it will be better when GIFs are handled file_ = xbmcvfs.File(full_basefilepath, 'wb') with closing(file_): if not file_.write(result.content): self.fileerror_count += 1 raise FileError(L(CANT_WRITE_TO_FILE).format(full_basefilepath)) self.fileerror_count = 0 mediaitem.downloadedart[arttype] = full_basefilepath log("downloaded '{0}'\nto image file '{1}'".format(url, full_basefilepath)) return services_hit, error def doget(self, url, **kwargs): try: result = self.getter(url, **kwargs) if not result and url.startswith('http://'): # Try https, the browser "that totally shows this image" probably is, even if no redirect result, err = self.doget('https://' + url[7:]) if err or not result: result = None return result, None except GetterError as ex: message = L(CANT_CONTACT_PROVIDER) if ex.connection_error else L(HTTP_ERROR).format(ex.message) return None, message def remove_deselected_files(self, mediaitem, assignedart=False): if self.debug: return for arttype, newimage in mediaitem.selectedart.iteritems(): if newimage is not None: continue if assignedart: oldimage = mediaitem.art.get(arttype) else: oldimage = mediaitem.forcedart.get(arttype) if not oldimage: continue old_url = oldimage['url'] if isinstance(oldimage, dict) else \ oldimage if isinstance(oldimage, basestring) else oldimage[0]['url'] if not old_url or old_url.startswith(pykodi.notimagefiles) \ or old_url in mediaitem.selectedart.values() or not xbmcvfs.exists(old_url): continue if settings.recycle_removed: recyclefile(old_url) xbmcvfs.delete(old_url) def set_bigcache(self): if self.alreadycached is None: self.alreadycached = [] def cachefor(self, artmap, multiplethreads=False): if not self.imagecachebase or self.debug: return 0 urls = [url for url in artmap.values() if url and not url.startswith(('http', 'image'))] if not urls: return 0 if self.alreadycached is not None: if not self.alreadycached: self.alreadycached = [pykodi.unquoteimage(texture['url']) for texture in quickjson.get_textures() if not pykodi.unquoteimage(texture['url']).startswith(('http', 'image'))] alreadycached = self.alreadycached else: alreadycached = [pykodi.unquoteimage(texture['url']) for texture in quickjson.get_textures(urls)] count = [0] def worker(path): try: res, _ = self.doget(self.imagecachebase + urllib.quote(pykodi.quoteimage(path), ''), stream=True) if res: res.iter_content(chunk_size=1024) res.close() count[0] += 1 except GetterError: pass threads = [] for path in urls: if path in alreadycached: continue if multiplethreads: t = threading.Thread(target=worker, args=(path,)) threads.append(t) t.start() else: worker(path) for t in threads: t.join() return count[0] def extrafanart_name_used(path, localfiles): return utils.parent_dir(path) == 'extrafanart' and path in localfiles def get_file_extension(contenttype, request_url, re_search=re.compile(r'\.\w*$')): if contenttype in typemap: return typemap[contenttype] if re.search(re_search, request_url): return request_url.rsplit('.', 1)[1] def get_next_filename(full_basefilepath, localfiles): nextname = full_basefilepath char_int = 97 while nextname in localfiles: name, ext = os.path.splitext(full_basefilepath) nextname = name + chr(char_int) + ext char_int += 1 return nextname def get_downloadable_art(mediaitem, allartwork): if allartwork: downloadable = dict(mediaitem.art) downloadable.update(mediaitem.selectedart) else: downloadable = dict(mediaitem.selectedart) for arttype in list(downloadable): if not downloadable[arttype] or not downloadable[arttype].startswith('http') or \ not mediatypes.downloadartwork(mediaitem.mediatype, arttype): del downloadable[arttype] return downloadable def get_local_art(mediaitem, allartwork): local = [] if allartwork: arts = mediaitem.art if settings.clean_imageurls else \ cleaner.clean_artwork(mediaitem) # library URLs not cleaned, but can still help here for url in arts.values(): if url and not url.startswith('http'): local.append(url) for url in mediaitem.selectedart.values(): if url and not url.startswith('http'): local.append(url) return local def recyclefile(filename): firstdir = utils.parent_dir(filename) directory = TEMP_DIR pathsep = utils.get_pathsep(directory) if firstdir in ('extrafanart', 'extrathumbs'): directory += utils.parent_dir(os.path.dirname(filename)) + pathsep directory += firstdir if not xbmcvfs.exists(directory): xbmcvfs.mkdirs(directory) recycled_filename = directory + pathsep + os.path.basename(filename) if not xbmcvfs.copy(filename, recycled_filename): raise FileError(L(CANT_WRITE_TO_FILE).format(recycled_filename)) class FileError(Exception): def __init__(self, message, cause=None): super(FileError, self).__init__() self.cause = cause self.message = message