def cache_artwork(librarytype='videos'): fileman = FileManager(False, True) if not fileman.imagecachebase: xbmcgui.Dialog().notification("Artwork Beef", L(M.REMOTE_CONTROL_REQUIRED), xbmcgui.NOTIFICATION_WARNING) return heading = L(M.CACHE_VIDEO_ARTWORK if librarytype == 'videos' else M.CACHE_MUSIC_ARTWORK) cached = runon_medialist(lambda mi: fileman.cachefor(mi.art, True), heading, librarytype, fg=True) xbmcgui.Dialog().ok("Artwork Beef", L(M.CACHED_COUNT).format(cached))
class ArtworkProcessor(object): def __init__(self, monitor=None): self.monitor = monitor or xbmc.Monitor() self.language = None self.autolanguages = None self.progress = xbmcgui.DialogProgressBG() self.visible = False self.freshstart = "0" self.processed = ProcessedItems() self.gatherer = None self.downloader = None self.chunkcount = 1 self.currentchunk = 0 self.debug = False self.localmode = False settings.update_settings() mediatypes.update_settings() def create_progress(self): if not self.visible and settings.progressdisplay == PROGRESS_DISPLAY_FULLPROGRESS: self.progress.create("Artwork Beef: " + L(ADDING_ARTWORK_MESSAGE), "") self.visible = True def update_progress(self, percent, message, heading=None): if self.chunkcount > 1: onechunkp = 100 / (self.chunkcount * 1.0) percent = int(onechunkp * self.currentchunk + percent / (self.chunkcount * 1.0)) if self.visible and settings.progressdisplay == PROGRESS_DISPLAY_FULLPROGRESS: self.progress.update(percent, heading, message) def finalupdate(self, header, message): if settings.final_notification: xbmcgui.Dialog().notification("Artwork Beef: " + header, message, '-', 8000) elif settings.progressdisplay == PROGRESS_DISPLAY_FULLPROGRESS: self.update_progress(100, message, header) try: self.monitor.really_waitforabort(8) except AttributeError: self.monitor.waitForAbort(8) def close_progress(self): if self.visible and settings.progressdisplay == PROGRESS_DISPLAY_FULLPROGRESS: self.progress.close() self.visible = False def notify_warning(self, message, header=None, error=False): if settings.progressdisplay != PROGRESS_DISPLAY_NONE: header = "Artwork Beef: " + header if header else "Artwork Beef" xbmcgui.Dialog().notification( header, message, xbmcgui.NOTIFICATION_ERROR if error else xbmcgui.NOTIFICATION_WARNING) def init_run(self, show_progress=False, chunkcount=1): self.setlanguages() self.gatherer = Gatherer(self.monitor, self.autolanguages) self.downloader = FileManager(self.debug, chunkcount > 1) self.freshstart = str(datetime_now() - timedelta(days=365)) self.chunkcount = chunkcount self.currentchunk = 0 if get_kodi_version() >= 18: populate_musiccentraldir() if show_progress: self.create_progress() def set_debug(self, debug): if self.downloader: self.downloader.debug = debug reporting.debug = debug self.debug = debug def finish_run(self): info.clear_cache() self.downloader = None self.set_debug(False) self.close_progress() @property def processor_busy(self): # DEPRECATED: StringCompare is deprecated in Krypton, gone in Leia return pykodi.get_conditional( '![StringCompare(Window(Home).Property(ArtworkBeef.Status),idle) | String.IsEqual(Window(Home).Property(ArtworkBeef.Status),idle)]' ) def process_item(self, mediatype, dbid, mode): if self.processor_busy: return if mode == MODE_DEBUG: mode = MODE_AUTO self.set_debug(True) if mode == MODE_GUI: busy = pykodi.get_busydialog() busy.create() if mediatype in mediatypes.artinfo and (mediatype not in mediatypes.audiotypes or get_kodi_version() >= 18): mediaitem = info.MediaItem( quickjson.get_item_details(dbid, mediatype)) log("Processing {0} '{1}' {2}.".format( mediatype, mediaitem.label, 'automatically' if mode == MODE_AUTO else 'manually')) else: if mode == MODE_GUI: busy.close() xbmcgui.Dialog().notification( "Artwork Beef", L(NOT_SUPPORTED_MESSAGE).format(mediatype), '-', 6500) return self.init_run() if mediatype == mediatypes.EPISODE: series = quickjson.get_item_details(mediaitem.tvshowid, mediatypes.TVSHOW) if not any(uniqueid in settings.autoadd_episodes for uniqueid in series['uniqueid'].itervalues()): mediaitem.skip_artwork = ['fanart'] info.add_additional_iteminfo(mediaitem, self.processed, search) if not mediaitem.uniqueids and not mediatypes.only_filesystem( mediaitem.mediatype): if mediatype in mediatypes.require_manualid: self.manual_id(mediaitem) if mode == MODE_GUI: self._manual_item_process(mediaitem, busy) else: medialist = [mediaitem] if mediatype == mediatypes.TVSHOW and not mediatypes.disabled( mediatypes.EPISODE): gen_epthumb = mediatypes.generatethumb(mediatypes.EPISODE) download_ep = mediatypes.downloadanyartwork(mediatypes.EPISODE) if mediaitem.uniqueids and any( x in mediaitem.uniqueids.values() for x in settings.autoadd_episodes): medialist.extend( info.MediaItem(ep) for ep in quickjson.get_episodes(dbid)) elif gen_epthumb or download_ep: for episode in quickjson.get_episodes(dbid): if gen_epthumb and not info.has_generated_thumbnail(episode) \ or download_ep and info.has_art_todownload(episode['art'], mediatypes.EPISODE): episode = info.MediaItem(episode) episode.skip_artwork = ['fanart'] medialist.append(episode) elif mediatype == mediatypes.ARTIST and not mediatypes.disabled( mediatypes.ALBUM): medialist.extend( info.MediaItem(album) for album in quickjson.get_albums( mediaitem.label, mediaitem.dbid)) if mediatype in (mediatypes.ALBUM, mediatypes.ARTIST) and not mediatypes.disabled(mediatypes.ALBUM) \ and not mediatypes.disabled(mediatypes.SONG): medialist.extend( info.MediaItem(song) for song in quickjson.get_songs( mediaitem.mediatype, mediaitem.dbid)) self.process_medialist(medialist, True) def _manual_item_process(self, mediaitem, busy): self._process_item(mediaitem, True, False) busy.close() if mediaitem.availableart or mediaitem.forcedart: availableart = dict(mediaitem.availableart) if mediaitem.mediatype == mediatypes.TVSHOW and 'fanart' in availableart: # add unseasoned backdrops as manual-only options for each season fanart unseasoned_backdrops = [ dict(art) for art in availableart['fanart'] if not art.get('hasseason') ] if unseasoned_backdrops: for season in mediaitem.seasons.keys(): key = 'season.{0}.fanart'.format(season) if key in availableart: availableart[key].extend(unseasoned_backdrops) else: availableart[key] = list(unseasoned_backdrops) if mediaitem.mediatype in (mediatypes.MOVIE, mediatypes.MOVIESET ) and 'poster' in availableart: # add no-language posters from TMDB as manual-only options for 'keyart' nolang_posters = [ dict(art) for art in availableart['poster'] if not art['language'] ] for art in nolang_posters: if art['provider'].sort == 'themoviedb.org': if 'keyart' not in availableart: availableart['keyart'] = [] availableart['keyart'].append(art) tag_forcedandexisting_art(availableart, mediaitem.forcedart, mediaitem.art) selectedarttype, selectedart = prompt_for_artwork( mediaitem.mediatype, mediaitem.label, availableart, self.monitor) if selectedarttype and selectedarttype not in availableart: self.manual_id(mediaitem) return if selectedarttype and selectedart: if mediatypes.get_artinfo(mediaitem.mediatype, selectedarttype)['multiselect']: selectedart = info.fill_multiart(mediaitem.art, selectedarttype, selectedart) else: selectedart = {selectedarttype: selectedart} selectedart = get_simpledict_updates(mediaitem.art, selectedart) mediaitem.selectedart = selectedart toset = dict(selectedart) if settings.remove_deselected_files: self.downloader.remove_deselected_files(mediaitem) if mediatypes.downloadanyartwork(mediaitem.mediatype): try: self.downloader.downloadfor(mediaitem, False) except FileError as ex: mediaitem.error = ex.message log(ex.message, xbmc.LOGERROR) xbmcgui.Dialog().notification( "Artwork Beef", ex.message, xbmcgui.NOTIFICATION_ERROR) toset.update(mediaitem.downloadedart) if toset: mediaitem.updatedart = toset.keys() add_art_to_library(mediaitem.mediatype, mediaitem.seasons, mediaitem.dbid, toset) self.cachelocal(mediaitem, toset) reporting.report_item(mediaitem, True, True, self.downloader.size) if not mediaitem.error: notifycount(len(toset)) else: xbmcgui.Dialog().notification( L(NOT_AVAILABLE_MESSAGE), L(SOMETHING_MISSING) + ' ' + L(FINAL_MESSAGE), '-', 8000) self.finish_run() def process_medialist(self, medialist, alwaysnotify=False): return self.process_chunkedlist([medialist], 1, alwaysnotify) def process_chunkedlist(self, chunkedlist, chunkcount, alwaysnotify=False): self.init_run(True, chunkcount) aborted = False artcount = 0 for idx, medialist in enumerate(chunkedlist): self.currentchunk = idx if not idx and chunkcount == 1 and len(medialist) > 100: self.downloader.set_bigcache() if self.monitor.abortRequested() or medialist is False: aborted = True break reporting.report_start(medialist) this_aborted, updateditemcount, this_artcount = \ self._process_chunk(medialist, len(medialist) == 1 and not idx, alwaysnotify) artcount += this_artcount reporting.report_end(medialist, updateditemcount if this_aborted else 0, self.downloader.size) self.downloader.size = 0 if this_aborted: aborted = True break if artcount or alwaysnotify: self.finalupdate(*finalmessages(artcount)) self.finish_run() return not aborted def _process_chunk(self, medialist, singleitemlist, singleitem): artcount = 0 currentitem = 0 aborted = False for mediaitem in medialist: if is_excluded(mediaitem): continue if mediaitem.mediatype in mediatypes.audiotypes and get_kodi_version( ) < 18: continue self.update_progress(currentitem * 100 // len(medialist), mediaitem.label) info.add_additional_iteminfo(mediaitem, self.processed, None if self.localmode else search) currentitem += 1 try: services_hit = self._process_item(mediaitem, singleitem) except JSONException as ex: mediaitem.error = "Kodi threw a non-descript JSON error." log("Kodi threw a non-descript JSON error.", xbmc.LOGERROR) log(ex.message, xbmc.LOGERROR) services_hit = True except FileError as ex: services_hit = True mediaitem.error = ex.message log(ex.message, xbmc.LOGERROR) self.notify_warning(ex.message, None, True) reporting.report_item(mediaitem, singleitemlist or mediaitem.error) artcount += len(mediaitem.updatedart) if not services_hit: if self.monitor.abortRequested(): aborted = True break elif self.monitor.waitForAbort(THROTTLE_TIME): aborted = True break return aborted, currentitem, artcount def _process_item(self, mediaitem, singleitem=False, auto=True): log("Processing {0} '{1}' automatically.".format( mediaitem.mediatype, mediaitem.label)) mediatype = mediaitem.mediatype onlyfs = self.localmode or mediatypes.only_filesystem( mediaitem.mediatype) if not mediaitem.uniqueids and not onlyfs: mediaitem.missingid = True if singleitem: header = L(NO_IDS_MESSAGE) message = "{0} '{1}'".format(mediatype, mediaitem.label) log(header + ": " + message, xbmc.LOGNOTICE) xbmcgui.Dialog().notification("Artwork Beef: " + header, message, xbmcgui.NOTIFICATION_INFO) if auto: cleaned = get_simpledict_updates(mediaitem.art, cleaner.clean_artwork(mediaitem)) if cleaned: if not self.debug: add_art_to_library(mediatype, mediaitem.seasons, mediaitem.dbid, cleaned) mediaitem.art.update(cleaned) mediaitem.art = dict(item for item in mediaitem.art.iteritems() if item[1]) mediaitem.missingart = list( info.iter_missing_arttypes(mediaitem, mediaitem.art)) services_hit, error = self.gatherer.getartwork(mediaitem, onlyfs, auto) if auto: existingart = dict(mediaitem.art) selectedart = dict( (key, image['url']) for key, image in mediaitem.forcedart.iteritems()) existingart.update(selectedart) # Then add the rest of the missing art selectedart.update( self.get_top_missing_art( info.iter_missing_arttypes(mediaitem, existingart), mediatype, existingart, mediaitem.availableart)) selectedart = get_simpledict_updates(mediaitem.art, selectedart) mediaitem.selectedart = selectedart toset = dict(selectedart) if not self.localmode and mediatypes.downloadanyartwork( mediaitem.mediatype): sh, er = self.downloader.downloadfor(mediaitem) services_hit = services_hit or sh error = error or er toset.update(mediaitem.downloadedart) if toset: mediaitem.updatedart = list( set(mediaitem.updatedart + toset.keys())) if not self.debug: add_art_to_library(mediatype, mediaitem.seasons, mediaitem.dbid, toset) self.cachelocal(mediaitem, toset) if error: if isinstance(error, dict): header = L(PROVIDER_ERROR_MESSAGE).format( error['providername']) error = '{0}: {1}'.format(header, error['message']) mediaitem.error = error log(error, xbmc.LOGWARNING) self.notify_warning(error) elif auto and not self.debug and not self.localmode: if not (mediatype == mediatypes.EPISODE and 'fanart' in mediaitem.skip_artwork) and \ mediatype != mediatypes.SONG: self.processed.set_nextdate( mediaitem.dbid, mediatype, mediaitem.label, datetime_now() + timedelta(days=self.get_nextcheckdelay(mediaitem))) if mediatype == mediatypes.TVSHOW: self.processed.set_data(mediaitem.dbid, mediatype, mediaitem.label, mediaitem.season) if mediaitem.borked_filename: msg = L(FILENAME_ENCODING_ERROR).format(mediaitem.file) if not mediaitem.error: mediaitem.error = msg log(msg, xbmc.LOGWARNING) if self.debug: log(mediaitem, xbmc.LOGNOTICE) return services_hit def cachelocal(self, mediaitem, toset): ismusic = mediaitem.mediatype in mediatypes.audiotypes if settings.cache_local_video_artwork and not ismusic or \ settings.cache_local_music_artwork and ismusic: artmap = dict(mediaitem.art) artmap.update(toset) self.downloader.cachefor(artmap) def get_nextcheckdelay(self, mediaitem): weeks = 4 if mediatypes.only_filesystem(mediaitem.mediatype) \ else 32 if mediaitem.missingid or not mediaitem.missingart \ or mediaitem.mediatype in (mediatypes.MOVIE, mediatypes.TVSHOW) \ and mediaitem.premiered < self.freshstart \ else 16 return plus_some(weeks * 7, weeks) def manual_id(self, mediaitem): label = ENTER_COLLECTION_NAME if mediaitem.mediatype == mediatypes.MOVIESET else ENTER_ARTIST_TRACK_NAMES result = xbmcgui.Dialog().input(L(label), mediaitem.label) if not result: return False # Cancelled options = search[mediaitem.mediatype].search(result, mediaitem.mediatype) selected = xbmcgui.Dialog().select( mediaitem.label, [option['label'] for option in options]) if selected < 0: return False # Cancelled uq = options[selected]['uniqueids'] toset = uq.get('tmdb') if not toset and 'mbtrack' in uq and 'mbgroup' in uq and 'mbartist' in uq: toset = '{0}/{1}/{2}'.format(uq['mbtrack'], uq['mbgroup'], uq['mbartist']) if toset: self.processed.set_data(mediaitem.dbid, mediaitem.mediatype, mediaitem.label, toset) return bool(toset) def setlanguages(self): languages = [] if settings.language_override: languages.append(settings.language_override) if settings.language_fallback_kodi: newlang = pykodi.get_language(xbmc.ISO_639_1) if newlang not in languages: languages.append(newlang) if settings.language_fallback_en and 'en' not in languages: languages.append('en') self.autolanguages = languages log("Working language filter: " + str(languages)) def get_top_missing_art(self, missingarts, mediatype, existingart, availableart): # TODO: refactor to `get_top_artwork` that works on a single art type if not availableart: return {} newartwork = {} for missingart in missingarts: if missingart not in availableart: continue itemtype, artkey = mediatypes.hack_mediaarttype( mediatype, missingart) artinfo = mediatypes.get_artinfo(itemtype, artkey) if artinfo['multiselect']: existingurls = [] existingartnames = [] for art, url in existingart.iteritems(): if info.arttype_matches_base(art, missingart) and url: existingurls.append(url) existingartnames.append(art) newart = [ art for art in availableart[missingart] if self._auto_filter( missingart, art, mediatype, availableart[missingart], existingurls) ] if not newart: continue newartcount = 0 for i in range(0, artinfo['autolimit']): exacttype = '%s%s' % (artkey, i if i else '') if exacttype not in existingartnames: if newartcount >= len(newart): break if exacttype not in newartwork: newartwork[exacttype] = [] newartwork[exacttype] = newart[newartcount]['url'] newartcount += 1 else: newart = next((art for art in availableart[missingart] if self._auto_filter(missingart, art, mediatype, availableart[missingart])), None) if newart: newartwork[missingart] = newart['url'] return newartwork def _auto_filter(self, basearttype, art, mediatype, availableart, ignoreurls=(), skippreferred=False): if art['rating'].sort < settings.minimum_rating: return False if not skippreferred and mediatypes.haspreferred_source(mediatype) and \ not mediatypes.ispreferred_source(mediatype, art['provider'][0]) and \ any(1 for i in availableart if mediatypes.ispreferred_source(mediatype, i['provider'][0]) and self._auto_filter(basearttype, i, mediatype, availableart, ignoreurls, True)): return False if basearttype.endswith( 'fanart') and art['size'].sort < settings.minimum_size: return False if art['provider'].sort == 'theaudiodb.com' or not art['language'] and \ (basearttype.endswith('poster') and settings.titlefree_poster or basearttype.endswith(('fanart', 'keyart', 'characterart'))): return skippreferred or art['url'] not in ignoreurls return art['language'] in self.autolanguages and ( skippreferred or art['url'] not in ignoreurls)