def get_repo_resourceaddons(filterstr=""): '''helper to retrieve all available resource addons on the kodi repo''' result = [] simplecache = SimpleCache() for item in xbmcvfs.listdir("addons://all/kodi.resource.images/")[1]: if not filterstr or item.lower().startswith(filterstr.lower()): addoninfo = get_repo_addoninfo(item, simplecache) if not addoninfo.get("name"): addoninfo = {"addonid": item, "name": item, "author": ""} addoninfo["thumbnail"] = "http://mirrors.kodi.tv/addons/krypton/%s/icon.png" % item addoninfo["path"] = "resource://%s/" % item result.append(addoninfo) simplecache.close() return result
class PluginContent: """Hidden plugin entry point providing some helper features""" params = {} win = None def __init__(self): self.cache = SimpleCache() self.kodi_db = KodiDb() self.win = xbmcgui.Window(10000) try: self.params = dict(urlparse.parse_qsl(sys.argv[2].replace("?", "").lower().decode("utf-8"))) log_msg("plugin called with parameters: %s" % self.params) self.main() except Exception as exc: log_exception(__name__, exc) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) # cleanup when done processing self.close() def close(self): """Cleanup Kodi Cpython instances""" self.cache.close() del self.win def main(self): """main action, load correct function""" action = self.params.get("action", "") if self.win.getProperty("SkinHelperShutdownRequested"): # do not proceed if kodi wants to exit log_msg("%s --> Not forfilling request: Kodi is exiting" % __name__, xbmc.LOGWARNING) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) else: try: if hasattr(self.__class__, action): # launch module for action provided by this plugin getattr(self, action)() else: # legacy (widget) path called !!! self.load_widget() except Exception as exc: log_exception(__name__, exc) def load_widget(self): """legacy entrypoint called (widgets are moved to seperate addon), start redirect...""" action = self.params.get("action", "") newaddon = "script.skin.helper.widgets" log_msg( "Deprecated method: %s. Please reassign your widgets to get rid of this message. -" "This automatic redirect will be removed in the future" % (action), xbmc.LOGWARNING, ) paramstring = "" for key, value in self.params.iteritems(): paramstring += ",%s=%s" % (key, value) if xbmc.getCondVisibility("System.HasAddon(%s)" % newaddon): # TEMP !!! for backwards compatability reasons only - to be removed in the near future!! import imp addon = xbmcaddon.Addon(newaddon) addon_path = addon.getAddonInfo("path").decode("utf-8") imp.load_source("plugin", os.path.join(addon_path, "plugin.py")) from plugin import main main.Main() del addon else: # trigger install of the addon if KODI_VERSION >= 17: xbmc.executebuiltin("InstallAddon(%s)" % newaddon) else: xbmc.executebuiltin("RunPlugin(plugin://%s)" % newaddon) def playchannel(self): """play channel from widget helper""" params = {"item": {"channelid": int(self.params["channelid"])}} self.kodi_db.set_json("Player.Open", params) def playrecording(self): """retrieve the recording and play to get resume working""" recording = self.kodi_db.recording(self.params["recordingid"]) params = {"item": {"recordingid": recording["recordingid"]}} self.kodi_db.set_json("Player.Open", params) # manually seek because passing resume to the player json cmd doesn't seem to work if recording["resume"].get("position"): for i in range(50): if xbmc.getCondVisibility("Player.HasVideo"): break xbmc.sleep(50) xbmc.Player().seekTime(recording["resume"].get("position")) def launch(self): """launch any builtin action using a plugin listitem""" if "runscript" in self.params["path"]: self.params["path"] = self.params["path"].replace("?", ",") xbmc.executebuiltin(self.params["path"]) def playalbum(self): """helper to play an entire album""" xbmc.executeJSONRPC( '{ "jsonrpc": "2.0", "method": "Player.Open", "params": { "item": { "albumid": %d } }, "id": 1 }' % int(self.params["albumid"]) ) def smartshortcuts(self): """called from skinshortcuts to retrieve listing of all smart shortcuts""" import skinshortcuts skinshortcuts.get_smartshortcuts(self.params.get("path", "")) @staticmethod def backgrounds(): """called from skinshortcuts to retrieve listing of all backgrounds""" import skinshortcuts skinshortcuts.get_backgrounds() def widgets(self): """called from skinshortcuts to retrieve listing of all widgetss""" import skinshortcuts skinshortcuts.get_widgets(self.params.get("path", ""), self.params.get("sublevel", "")) def resourceimages(self): """retrieve listing of specific resource addon images""" from resourceaddons import get_resourceimages addontype = self.params.get("addontype", "") for item in get_resourceimages(addontype, True): listitem = xbmcgui.ListItem(item[0], label2=item[2], path=item[1], iconImage=item[3]) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item[1], listitem=listitem, isFolder=False) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def extrafanart(self): """helper to display extrafanart in multiimage control in the skin""" fanarts = eval(self.params["fanarts"]) # process extrafanarts for item in fanarts: listitem = xbmcgui.ListItem(item, path=item) listitem.setProperty("mimetype", "image/jpeg") xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item, listitem=listitem) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def genrebackground(self): """helper to display images for a specific genre in multiimage control in the skin""" genre = self.params.get("genre").split(".")[0] arttype = self.params.get("arttype", "fanart") randomize = self.params.get("random", "false") == "true" mediatype = self.params.get("mediatype", "movies") if genre and genre != "..": filters = [{"operator": "is", "field": "genre", "value": genre}] if randomize: sort = {"method": "random", "order": "descending"} else: sort = None items = getattr(self.kodi_db, mediatype)(sort=sort, filters=filters, limits=(0, 50)) for item in items: image = get_clean_image(item["art"].get(arttype, "")) if image: image = get_clean_image(item["art"][arttype]) listitem = xbmcgui.ListItem(image, path=image) listitem.setProperty("mimetype", "image/jpeg") xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=image, listitem=listitem) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def getcastmedia(self): """helper to display get all media for a specific actor""" name = self.params.get("name") if name: all_items = self.kodi_db.castmedia(name) all_items = process_method_on_list(self.kodi_db.prepare_listitem, all_items) all_items = process_method_on_list(self.kodi_db.create_listitem, all_items) xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items)) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def getcast(self): """helper to get all cast for a given media item""" db_id = None all_cast = [] all_cast_names = list() cache_str = "" download_thumbs = self.params.get("downloadthumbs", "") == "true" extended_cast_action = self.params.get("castaction", "") == "extendedinfo" tmdb = Tmdb() movie = self.params.get("movie") tvshow = self.params.get("tvshow") episode = self.params.get("episode") movieset = self.params.get("movieset") try: # try to parse db_id if movieset: cache_str = "movieset.castcache-%s-%s" % (self.params["movieset"], download_thumbs) db_id = int(movieset) elif tvshow: cache_str = "tvshow.castcache-%s-%s" % (self.params["tvshow"], download_thumbs) db_id = int(tvshow) elif movie: cache_str = "movie.castcache-%s-%s" % (self.params["movie"], download_thumbs) db_id = int(movie) elif episode: cache_str = "episode.castcache-%s-%s" % (self.params["episode"], download_thumbs) db_id = int(episode) except Exception: pass cachedata = self.cache.get(cache_str) if cachedata: # get data from cache all_cast = cachedata else: # retrieve data from json api... if movie and db_id: all_cast = self.kodi_db.movie(db_id)["cast"] elif movie and not db_id: filters = [{"operator": "is", "field": "title", "value": movie}] result = self.kodi_db.movies(filters=filters) all_cast = result[0]["cast"] if result else [] elif tvshow and db_id: all_cast = self.kodi_db.tvshow(db_id)["cast"] elif tvshow and not db_id: filters = [{"operator": "is", "field": "title", "value": tvshow}] result = self.kodi_db.tvshows(filters=filters) all_cast = result[0]["cast"] if result else [] elif episode and db_id: all_cast = self.kodi_db.episode(db_id)["cast"] elif episode and not db_id: filters = [{"operator": "is", "field": "title", "value": episode}] result = self.kodi_db.episodes(filters=filters) all_cast = result[0]["cast"] if result else [] elif movieset: if not db_id: for item in self.kodi_db.moviesets(): if item["title"].lower() == movieset.lower(): db_id = item["setid"] if db_id: json_result = self.kodi_db.movieset(db_id, include_set_movies_fields=["cast"]) if "movies" in json_result: for movie in json_result["movies"]: all_cast += movie["cast"] # optional: download missing actor thumbs if all_cast and download_thumbs: for cast in all_cast: if cast.get("thumbnail"): cast["thumbnail"] = get_clean_image(cast.get("thumbnail")) if not cast.get("thumbnail"): artwork = tmdb.get_actor(cast["name"]) cast["thumbnail"] = artwork.get("thumb", "") # lookup tmdb if item is requested that is not in local db if not all_cast: tmdbdetails = {} if movie and not db_id: tmdbdetails = tmdb.search_movie(movie) elif tvshow and not db_id: tmdbdetails = tmdb.search_tvshow(tvshow) if tmdbdetails.get("cast"): all_cast = tmdbdetails["cast"] # save to cache self.cache.set(cache_str, all_cast) # process listing with the results... for cast in all_cast: if cast.get("name") not in all_cast_names: liz = xbmcgui.ListItem(label=cast.get("name"), label2=cast.get("role"), iconImage=cast.get("thumbnail")) if extended_cast_action: url = "RunScript(script.extendedinfo,info=extendedactorinfo,name=%s)" % cast.get("name") url = "plugin://script.skin.helper.service/?action=launch&path=%s" % url is_folder = False else: url = "RunScript(script.skin.helper.service,action=getcastmedia,name=%s)" % cast.get("name") url = "plugin://script.skin.helper.service/?action=launch&path=%s" % urlencode(url) is_folder = False all_cast_names.append(cast.get("name")) liz.setThumbnailImage(cast.get("thumbnail")) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=liz, isFolder=is_folder) xbmcplugin.endOfDirectory(int(sys.argv[1])) @staticmethod def alphabet(): """display an alphabet scrollbar in listings""" all_letters = [] if xbmc.getInfoLabel("Container.NumItems"): for i in range(int(xbmc.getInfoLabel("Container.NumItems"))): all_letters.append(xbmc.getInfoLabel("Listitem(%s).SortLetter" % i).upper()) start_number = "" for number in ["2", "3", "4", "5", "6", "7", "8", "9"]: if number in all_letters: start_number = number break for letter in [ start_number, "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ]: if letter == start_number: label = "#" else: label = letter listitem = xbmcgui.ListItem(label=label) if letter not in all_letters: lipath = "noop" listitem.setProperty("NotAvailable", "true") else: lipath = "plugin://script.skin.helper.service/?action=alphabetletter&letter=%s" % letter xbmcplugin.addDirectoryItem(int(sys.argv[1]), lipath, listitem, isFolder=False) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def alphabetletter(self): """used with the alphabet scrollbar to jump to a letter""" if KODI_VERSION > 16: xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=False, listitem=xbmcgui.ListItem()) letter = self.params.get("letter", "").upper() jumpcmd = "" if letter in ["A", "B", "C", "2"]: jumpcmd = "2" elif letter in ["D", "E", "F", "3"]: jumpcmd = "3" elif letter in ["G", "H", "I", "4"]: jumpcmd = "4" elif letter in ["J", "K", "L", "5"]: jumpcmd = "5" elif letter in ["M", "N", "O", "6"]: jumpcmd = "6" elif letter in ["P", "Q", "R", "S", "7"]: jumpcmd = "7" elif letter in ["T", "U", "V", "8"]: jumpcmd = "8" elif letter in ["W", "X", "Y", "Z", "9"]: jumpcmd = "9" if jumpcmd: xbmc.executebuiltin("SetFocus(50)") for i in range(40): xbmc.executeJSONRPC( '{ "jsonrpc": "2.0", "method": "Input.ExecuteAction",\ "params": { "action": "jumpsms%s" }, "id": 1 }' % (jumpcmd) ) xbmc.sleep(50) if xbmc.getInfoLabel("ListItem.Sortletter").upper() == letter: break
class MetadataUtils(object): ''' Provides all kind of mediainfo for kodi media, returned as dict with details ''' _audiodb, _addon, _close_called, _omdb, _kodidb, _tmdb, _fanarttv, _channellogos = [ None ] * 8 _imdb, _google, _studiologos, _animatedart, _thetvdb, _musicart, _pvrart, _lastfm = [ None ] * 8 _studiologos_path, _process_method_on_list, _detect_plugin_content, _get_streamdetails = [ None ] * 4 _extend_dict, _get_clean_image, _get_duration, _get_extrafanart, _get_extraposter, _get_moviesetdetails = [ None ] * 6 cache = None def __init__(self): '''Initialize and load all our helpers''' self.cache = SimpleCache() log_msg("Initialized") @use_cache(14) def get_extrafanart(self, file_path): '''helper to retrieve the extrafanart path for a kodi media item''' log_msg("metadatautils get_extrafanart called for %s" % file_path) if not self._get_extrafanart: from helpers.extrafanart import get_extrafanart self._get_extrafanart = get_extrafanart return self._get_extrafanart(file_path) @use_cache(14) def get_extraposter(self, file_path): '''helper to retrieve the extraposter path for a kodi media item''' if not self._get_extraposter: from helpers.extraposter import get_extraposter self._get_extraposter = get_extraposter return self._get_extraposter(file_path) def get_music_artwork(self, artist, album="", track="", disc="", ignore_cache=False, flush_cache=False): '''method to get music artwork for the goven artist/album/song''' return self.musicart.get_music_artwork(artist, album, track, disc, ignore_cache=ignore_cache, flush_cache=flush_cache) def music_artwork_options(self, artist, album="", track="", disc=""): '''options for music metadata for specific item''' return self.musicart.music_artwork_options(artist, album, track, disc) @use_cache(7) def get_extended_artwork(self, imdb_id="", tvdb_id="", tmdb_id="", media_type=""): '''get extended artwork for the given imdbid or tvdbid''' result = None if "movie" in media_type and tmdb_id: result = self.fanarttv.movie(tmdb_id) elif "movie" in media_type and imdb_id: # prefer local artwork local_details = self.kodidb.movie_by_imdbid(imdb_id) if local_details: result = local_details["art"] result = self.extend_dict(result, self.fanarttv.movie(imdb_id)) elif media_type in ["tvshow", "tvshows", "seasons", "episodes"]: if not tvdb_id: if imdb_id and not imdb_id.startswith("tt"): tvdb_id = imdb_id elif imdb_id: tvdb_id = self.thetvdb.get_series_by_imdb_id(imdb_id).get( "tvdb_id") if tvdb_id: # prefer local artwork local_details = self.kodidb.tvshow_by_imdbid(tvdb_id) if local_details: result = local_details["art"] elif imdb_id and imdb_id != tvdb_id: local_details = self.kodidb.tvshow_by_imdbid(imdb_id) if local_details: result = local_details["art"] result = self.extend_dict(result, self.fanarttv.tvshow(tvdb_id)) # add additional art with special path if result: result = {"art": result} for arttype in [ "fanarts", "posters", "clearlogos", "banners", "discarts", "cleararts", "characterarts" ]: if result["art"].get(arttype): result["art"][arttype] = "plugin://script.skin.helper.service/"\ "?action=extrafanart&fanarts=%s" % quote_plus(repr(result["art"][arttype])) return result @use_cache(90) def get_tmdb_details(self, imdb_id="", tvdb_id="", title="", year="", media_type="", preftype="", manual_select=False, ignore_cache=False): '''returns details from tmdb''' result = {} if imdb_id: result = self.tmdb.get_videodetails_by_externalid( imdb_id, "imdb_id") elif tvdb_id: result = self.tmdb.get_videodetails_by_externalid( tvdb_id, "tvdb_id") elif title and media_type in ["movies", "setmovies", "movie"]: result = self.tmdb.search_movie(title, year, manual_select=manual_select, ignore_cache=ignore_cache) elif title and media_type in ["tvshows", "tvshow"]: result = self.tmdb.search_tvshow(title, year, manual_select=manual_select, ignore_cache=ignore_cache) elif title: result = self.tmdb.search_video(title, year, preftype=preftype, manual_select=manual_select, ignore_cache=ignore_cache) if result and result.get("status"): result["status"] = self.translate_string(result["status"]) if result and result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result @use_cache(90) def get_moviesetdetails(self, title, set_id): '''get a nicely formatted dict of the movieset details which we can for example set as window props''' # get details from tmdb if not self._get_moviesetdetails: from helpers.moviesetdetails import get_moviesetdetails self._get_moviesetdetails = get_moviesetdetails return self._get_moviesetdetails(self, title, set_id) @use_cache(14) def get_streamdetails(self, db_id, media_type, ignore_cache=False): '''get a nicely formatted dict of the streamdetails''' if not self._get_streamdetails: from helpers.streamdetails import get_streamdetails self._get_streamdetails = get_streamdetails return self._get_streamdetails(self.kodidb, db_id, media_type) def get_pvr_artwork(self, title, channel="", genre="", manual_select=False, ignore_cache=False): '''get artwork and mediadetails for PVR entries''' return self.pvrart.get_pvr_artwork(title, channel, genre, manual_select=manual_select, ignore_cache=ignore_cache) def pvr_artwork_options(self, title, channel="", genre=""): '''options for pvr metadata for specific item''' return self.pvrart.pvr_artwork_options(title, channel, genre) def get_channellogo(self, channelname): '''get channellogo for the given channel name''' return self.channellogos.get_channellogo(channelname) def get_studio_logo(self, studio): '''get studio logo for the given studio''' # dont use cache at this level because of changing logospath return self.studiologos.get_studio_logo(studio, self.studiologos_path) @property def studiologos_path(self): '''path to use to lookup studio logos, must be set by the calling addon''' return self._studiologos_path @studiologos_path.setter def studiologos_path(self, value): '''path to use to lookup studio logos, must be set by the calling addon''' self._studiologos_path = value def get_animated_artwork(self, imdb_id, manual_select=False, ignore_cache=False): '''get animated artwork, perform extra check if local version still exists''' artwork = self.animatedart.get_animated_artwork( imdb_id, manual_select=manual_select, ignore_cache=ignore_cache) if not (manual_select or ignore_cache): refresh_needed = False if artwork.get("animatedposter") and not xbmcvfs.exists( artwork["animatedposter"]): refresh_needed = True if artwork.get("animatedfanart") and not xbmcvfs.exists( artwork["animatedfanart"]): refresh_needed = True return {"art": artwork} @use_cache(90) def get_omdb_info(self, imdb_id="", title="", year="", content_type=""): '''Get (kodi compatible formatted) metadata from OMDB, including Rotten tomatoes details''' title = title.split(" (")[0] # strip year appended to title result = {} if imdb_id: result = self.omdb.get_details_by_imdbid(imdb_id) elif title and content_type in [ "seasons", "season", "episodes", "episode", "tvshows", "tvshow" ]: result = self.omdb.get_details_by_title(title, "", "tvshows") elif title and year: result = self.omdb.get_details_by_title(title, year, content_type) if result and result.get("status"): result["status"] = self.translate_string(result["status"]) if result and result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result def get_top250_rating(self, imdb_id): '''get the position in the IMDB top250 for the given IMDB ID''' return self.imdb.get_top250_rating(imdb_id) @use_cache(14) def get_duration(self, duration): '''helper to get a formatted duration''' if not self._get_duration: from helpers.utils import get_duration self._get_duration = get_duration if sys.version_info.major == 3: if isinstance(duration, str) and ":" in duration: dur_lst = duration.split(":") return { "Duration": "%s:%s" % (dur_lst[0], dur_lst[1]), "Duration.Hours": dur_lst[0], "Duration.Minutes": dur_lst[1], "Runtime": int(dur_lst[0]) * 60 + int(dur_lst[1]), } else: return self._get_duration(duration) else: if isinstance(duration, (str)) and ":" in duration: dur_lst = duration.split(":") return { "Duration": "%s:%s" % (dur_lst[0], dur_lst[1]), "Duration.Hours": dur_lst[0], "Duration.Minutes": dur_lst[1], "Runtime": str((int(dur_lst[0]) * 60) + int(dur_lst[1])), } else: return self._get_duration(duration) @use_cache(2) def get_tvdb_details(self, imdbid="", tvdbid=""): '''get metadata from tvdb by providing a tvdbid or tmdbid''' result = {} self.thetvdb.days_ahead = 365 if not tvdbid and imdbid and not imdbid.startswith("tt"): # assume imdbid is actually a tvdbid... tvdbid = imdbid if tvdbid: result = self.thetvdb.get_series(tvdbid) elif imdbid: result = self.thetvdb.get_series_by_imdb_id(imdbid) if result: if result["status"] == "Continuing": # include next episode info result["nextepisode"] = self.thetvdb.get_nextaired_episode( result["tvdb_id"]) # include last episode info result["lastepisode"] = self.thetvdb.get_last_episode_for_series( result["tvdb_id"]) result["status"] = self.translate_string(result["status"]) if result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result @use_cache(90) def get_imdbtvdb_id(self, title, content_type, year="", imdbid="", tvshowtitle=""): '''try to figure out the imdbnumber and/or tvdbid''' tvdbid = "" if content_type in ["seasons", "episodes"] or tvshowtitle: title = tvshowtitle content_type = "tvshows" if imdbid and not imdbid.startswith("tt"): if content_type in ["tvshows", "seasons", "episodes"]: tvdbid = imdbid imdbid = "" if not imdbid and year: omdb_info = self.get_omdb_info("", title, year, content_type) if omdb_info: imdbid = omdb_info.get("imdbnumber", "") if not imdbid: # repeat without year omdb_info = self.get_omdb_info("", title, "", content_type) if omdb_info: imdbid = omdb_info.get("imdbnumber", "") # return results return (imdbid, tvdbid) def translate_string(self, _str): '''translate the received english string from the various sources like tvdb, tmbd etc''' translation = _str _str = _str.lower() if "continuing" in _str: translation = self.addon.getLocalizedString(32037) elif "ended" in _str: translation = self.addon.getLocalizedString(32038) elif "released" in _str: translation = self.addon.getLocalizedString(32040) return translation def process_method_on_list(self, *args, **kwargs): '''expose our process_method_on_list method to public''' if not self._process_method_on_list: from helpers.utils import process_method_on_list self._process_method_on_list = process_method_on_list return self._process_method_on_list(*args, **kwargs) def detect_plugin_content(self, *args, **kwargs): '''expose our detect_plugin_content method to public''' if not self._detect_plugin_content: from helpers.utils import detect_plugin_content self._detect_plugin_content = detect_plugin_content return self._detect_plugin_content(*args, **kwargs) def extend_dict(self, *args, **kwargs): '''expose our extend_dict method to public''' if not self._extend_dict: from helpers.utils import extend_dict self._extend_dict = extend_dict return self._extend_dict(*args, **kwargs) def get_clean_image(self, *args, **kwargs): '''expose our get_clean_image method to public''' if not self._get_clean_image: from helpers.utils import get_clean_image self._get_clean_image = get_clean_image return self._get_clean_image(*args, **kwargs) @property def omdb(self): '''public omdb object - for lazy loading''' if not self._omdb: from helpers.omdb import Omdb self._omdb = Omdb(self.cache) return self._omdb @property def kodidb(self): '''public kodidb object - for lazy loading''' if not self._kodidb: from helpers.kodidb import KodiDb self._kodidb = KodiDb(self.cache) return self._kodidb @property def tmdb(self): '''public Tmdb object - for lazy loading''' if not self._tmdb: from helpers.tmdb import Tmdb self._tmdb = Tmdb(self.cache) return self._tmdb @property def fanarttv(self): '''public FanartTv object - for lazy loading''' if not self._fanarttv: from helpers.fanarttv import FanartTv self._fanarttv = FanartTv(self.cache) return self._fanarttv @property def channellogos(self): '''public ChannelLogos object - for lazy loading''' if not self._channellogos: from helpers.channellogos import ChannelLogos self._channellogos = ChannelLogos(self.kodidb) return self._channellogos @property def imdb(self): '''public Imdb object - for lazy loading''' if not self._imdb: from helpers.imdb import Imdb self._imdb = Imdb(self.cache) return self._imdb @property def google(self): '''public GoogleImages object - for lazy loading''' if not self._google: from helpers.google import GoogleImages self._google = GoogleImages(self.cache) return self._google @property def studiologos(self): '''public StudioLogos object - for lazy loading''' if not self._studiologos: from helpers.studiologos import StudioLogos self._studiologos = StudioLogos(self.cache) return self._studiologos @property def animatedart(self): '''public AnimatedArt object - for lazy loading''' if not self._animatedart: from helpers.animatedart import AnimatedArt self._animatedart = AnimatedArt(self.cache, self.kodidb) return self._animatedart @property def thetvdb(self): '''public TheTvDb object - for lazy loading''' if not self._thetvdb: from thetvdb import TheTvDb self._thetvdb = TheTvDb() return self._thetvdb @property def musicart(self): '''public MusicArtwork object - for lazy loading''' if not self._musicart: from helpers.musicartwork import MusicArtwork self._musicart = MusicArtwork(self) return self._musicart @property def pvrart(self): '''public PvrArtwork object - for lazy loading''' if not self._pvrart: from helpers.pvrartwork import PvrArtwork self._pvrart = PvrArtwork(self) return self._pvrart @property def addon(self): '''public Addon object - for lazy loading''' if not self._addon: import xbmcaddon self._addon = xbmcaddon.Addon(ADDON_ID) return self._addon @property def lastfm(self): '''public LastFM object - for lazy loading''' if not self._lastfm: from helpers.lastfm import LastFM self._lastfm = LastFM() return self._lastfm @property def audiodb(self): '''public TheAudioDb object - for lazy loading''' if not self._audiodb: from helpers.theaudiodb import TheAudioDb self._audiodb = TheAudioDb() return self._audiodb def close(self): '''Cleanup instances''' self._close_called = True if self.cache: self.cache.close() del self.cache if self._addon: del self._addon if self._thetvdb: del self._thetvdb log_msg("Exited") def __del__(self): '''make sure close is called''' if not self._close_called: self.close()
class ListItemMonitor(threading.Thread): '''Our main class monitoring the kodi listitems and providing additional information''' event = None exit = False delayed_task_interval = 1795 listitem_details = {} all_window_props = {} cur_listitem = "" last_folder = "" last_listitem = "" foldercontent = {} screensaver_setting = None screensaver_disabled = False lookup_busy = {} enable_extendedart = False enable_musicart = False enable_animatedart = False enable_extrafanart = False enable_extraposter = False enable_pvrart = False enable_forcedviews = False def __init__(self, *args, **kwargs): self.cache = SimpleCache() self.metadatautils = kwargs.get("metadatautils") self.win = kwargs.get("win") self.kodimonitor = kwargs.get("monitor") self.event = threading.Event() threading.Thread.__init__(self, *args) def stop(self): '''called when the thread has to stop working''' log_msg("ListItemMonitor - stop called") self.exit = True self.cache.close() self.event.set() self.event.clear() self.join(1) def run(self): '''our main loop monitoring the listitem and folderpath changes''' log_msg("ListItemMonitor - started") self.get_settings() while not self.exit: # check screensaver and OSD self.check_screensaver() self.check_osd() # do some background stuff every 30 minutes if (self.delayed_task_interval >= 1800) and not self.exit: thread.start_new_thread(self.do_background_work, ()) self.delayed_task_interval = 0 # skip if any of the artwork context menus is opened if self.win.getProperty("SkinHelper.Artwork.ManualLookup"): self.reset_win_props() self.last_listitem = "" self.listitem_details = {} self.kodimonitor.waitForAbort(3) self.delayed_task_interval += 3 # skip when modal dialogs are opened (e.g. textviewer in musicinfo dialog) elif getCondVisibility( "Window.IsActive(DialogSelect.xml) | Window.IsActive(progressdialog) | " "Window.IsActive(contextmenu) | Window.IsActive(busydialog)" ): self.kodimonitor.waitForAbort(2) self.delayed_task_interval += 2 self.last_listitem = "" # skip when container scrolling elif getCondVisibility( "Container.OnScrollNext | Container.OnScrollPrevious | Container.Scrolling" ): self.kodimonitor.waitForAbort(1) self.delayed_task_interval += 1 self.last_listitem = "" # media window is opened or widgetcontainer set - start listitem monitoring! elif getCondVisibility( "Window.IsMedia | " "!IsEmpty(Window(Home).Property(SkinHelper.WidgetContainer))" ): self.monitor_listitem() self.kodimonitor.waitForAbort(0.15) self.delayed_task_interval += 0.15 # flush any remaining window properties elif self.all_window_props: self.reset_win_props() self.win.clearProperty("SkinHelper.ContentHeader") self.win.clearProperty("contenttype") self.win.clearProperty("curlistitem") self.last_listitem = "" # other window active - do nothing else: self.kodimonitor.waitForAbort(1) self.delayed_task_interval += 1 def get_settings(self): '''collect our skin settings that control the monitoring''' self.enable_extendedart = getCondVisibility( "Skin.HasSetting(SkinHelper.EnableExtendedArt)") == 1 self.enable_musicart = getCondVisibility( "Skin.HasSetting(SkinHelper.EnableMusicArt)") == 1 self.enable_animatedart = getCondVisibility( "Skin.HasSetting(SkinHelper.EnableAnimatedPosters)") == 1 self.enable_extrafanart = getCondVisibility( "Skin.HasSetting(SkinHelper.EnableExtraFanart)") == 1 self.enable_extraposter = getCondVisibility( "Skin.HasSetting(SkinHelper.EnableExtraPoster)") == 1 self.enable_pvrart = getCondVisibility( "Skin.HasSetting(SkinHelper.EnablePVRThumbs) + PVR.HasTVChannels" ) == 1 self.enable_forcedviews = getCondVisibility( "Skin.HasSetting(SkinHelper.ForcedViews.Enabled)") == 1 studiologos_path = xbmc.getInfoLabel( "Skin.String(SkinHelper.StudioLogos.Path)").decode("utf-8") if studiologos_path != self.metadatautils.studiologos_path: self.listitem_details = {} self.metadatautils.studiologos_path = studiologos_path # set additional window props to control contextmenus as using the skinsetting gives unreliable results for skinsetting in [ "EnableAnimatedPosters", "EnableMusicArt", "EnablePVRThumbs" ]: if getCondVisibility("Skin.HasSetting(SkinHelper.%s)" % skinsetting): self.win.setProperty("SkinHelper.%s" % skinsetting, "enabled") else: self.win.clearProperty("SkinHelper.%s" % skinsetting) def monitor_listitem(self): '''Monitor listitem details''' cur_folder, cont_prefix = self.get_folderandprefix() # identify current listitem - prefer parent folder (tvshows, music) cur_listitem = xbmc.getInfoLabel( "$INFO[%sListItem.TvshowTitle]$INFO[%sListItem.Artist]$INFO[%sListItem.Album]" % (cont_prefix, cont_prefix, cont_prefix)).decode('utf-8') if not cur_listitem: # fallback to generic approach cur_listitem = xbmc.getInfoLabel( "$INFO[%sListItem.Label]$INFO[%sListItem.DBID]$INFO[%sListItem.Title]" % (cont_prefix, cont_prefix, cont_prefix)).decode('utf-8') if self.exit: return # perform actions if the container path has changed if cur_folder != self.last_folder and not ( cur_listitem and cur_listitem == self.last_listitem): self.reset_win_props() self.get_settings() self.last_folder = cur_folder self.last_listitem = None content_type = self.get_content_type(cur_folder, cur_listitem, cont_prefix) # additional actions to perform when we have a valid contenttype and no widget container if not cont_prefix and content_type: self.set_forcedview(content_type) self.set_content_header(content_type) else: content_type = self.get_content_type(cur_folder, cur_listitem, cont_prefix) if self.exit: return # only perform actions when the listitem has actually changed if cur_listitem != self.last_listitem: self.last_listitem = cur_listitem self.win.setProperty("curlistitem", cur_listitem) if cur_listitem and cur_listitem != "..": # set listitem details in background thread thread.start_new_thread( self.set_listitem_details, (cur_listitem, content_type, cont_prefix)) def get_folderandprefix(self): '''get the current folder and prefix''' cur_folder = "" cont_prefix = "" try: widget_container = self.win.getProperty( "SkinHelper.WidgetContainer").decode('utf-8') if getCondVisibility("Window.IsActive(movieinformation)"): cont_prefix = "" cur_folder = xbmc.getInfoLabel( "$INFO[Window.Property(xmlfile)]$INFO[Container.FolderPath]" "$INFO[Container.NumItems]$INFO[Container.Content]" ).decode('utf-8') elif widget_container: cont_prefix = "Container(%s)." % widget_container cur_folder = xbmc.getInfoLabel( "widget-%s-$INFO[Container(%s).NumItems]-$INFO[Container(%s).ListItemAbsolute(1).Label]" % (widget_container, widget_container, widget_container)).decode('utf-8') else: cont_prefix = "" cur_folder = xbmc.getInfoLabel( "$INFO[Window.Property(xmlfile)]$INFO[Container.FolderPath]$INFO[Container.NumItems]$INFO[Container.Content]" ).decode('utf-8') except Exception as exc: log_exception(__name__, exc) cur_folder = "" cont_prefix = "" return (cur_folder, cont_prefix) def get_content_type(self, cur_folder, cur_listitem, cont_prefix): '''get contenttype for current folder''' content_type = "" if cur_folder in self.foldercontent: content_type = self.foldercontent[cur_folder] elif cur_folder and cur_listitem: # always wait for the content_type because some listings can be slow for i in range(20): content_type = get_current_content_type(cont_prefix) if self.exit: return "" if content_type: break else: xbmc.sleep(250) self.foldercontent[cur_folder] = content_type self.win.setProperty("contenttype", content_type) return content_type def check_screensaver(self): '''Allow user to disable screensaver on fullscreen music playback''' if getCondVisibility( "Window.IsActive(visualisation) + Skin.HasSetting(SkinHelper.DisableScreenSaverOnFullScreenMusic)" ): if not self.screensaver_disabled: # disable screensaver when fullscreen music active self.screensaver_disabled = True screensaver_setting = kodi_json( 'Settings.GetSettingValue', '{"setting":"screensaver.mode"}') if screensaver_setting: self.screensaver_setting = screensaver_setting kodi_json('Settings.SetSettingValue', { "setting": "screensaver.mode", "value": None }) log_msg( "Disabled screensaver while fullscreen music playback - previous setting: %s" % self.screensaver_setting, xbmc.LOGNOTICE) elif self.screensaver_disabled and self.screensaver_setting: # enable screensaver again after fullscreen music playback was ended kodi_json('Settings.SetSettingValue', { "setting": "screensaver.mode", "value": self.screensaver_setting }) self.screensaver_disabled = False self.screensaver_setting = None log_msg( "fullscreen music playback ended - restoring screensaver: %s" % self.screensaver_setting, xbmc.LOGNOTICE) @staticmethod def check_osd(): '''Allow user to set a default close timeout for the OSD panels''' if getCondVisibility( "[Window.IsActive(videoosd) + Skin.String(SkinHelper.AutoCloseVideoOSD)] | " "[Window.IsActive(musicosd) + Skin.String(SkinHelper.AutoCloseMusicOSD)]" ): if getCondVisibility("Window.IsActive(videoosd)"): seconds = xbmc.getInfoLabel( "Skin.String(SkinHelper.AutoCloseVideoOSD)") window = "videoosd" elif getCondVisibility("Window.IsActive(musicosd)"): seconds = xbmc.getInfoLabel( "Skin.String(SkinHelper.AutoCloseMusicOSD)") window = "musicosd" else: seconds = "" if seconds and seconds != "0": while getCondVisibility("Window.IsActive(%s)" % window): if getCondVisibility("System.IdleTime(%s)" % seconds): if getCondVisibility("Window.IsActive(%s)" % window): xbmc.executebuiltin("Dialog.Close(%s)" % window) else: xbmc.sleep(500) def set_listitem_details(self, cur_listitem, content_type, prefix): '''set the window properties based on the current listitem''' try: if cur_listitem in self.listitem_details: # data already in memory all_props = self.listitem_details[cur_listitem] else: # skip if another lookup for the same listitem is already in progress... if self.lookup_busy.get(cur_listitem) or self.exit: return self.lookup_busy[cur_listitem] = True # clear all window props, do this delayed to prevent flickering of the screen thread.start_new_thread(self.delayed_flush, (cur_listitem, )) # wait if we already have more than 5 items in the queue while len(self.lookup_busy) > 5: xbmc.sleep(100) if self.exit or cur_listitem != self.last_listitem: self.lookup_busy.pop(cur_listitem, None) return # prefer listitem's contenttype over container's contenttype dbtype = xbmc.getInfoLabel("%sListItem.DBTYPE" % prefix) if not dbtype: dbtype = xbmc.getInfoLabel("%sListItem.Property(DBTYPE)" % prefix) if dbtype: content_type = dbtype + "s" # collect details from listitem details = self.get_listitem_details(content_type, prefix) if self.exit: return # music content if content_type in ["albums", "artists", "songs" ] and self.enable_musicart: details = self.metadatautils.extend_dict( details, self.metadatautils.get_music_artwork( details["artist"], details["album"], details["title"], details["discnumber"])) # moviesets elif details["path"].startswith( "videodb://movies/sets/") and details["dbid"]: details = self.metadatautils.extend_dict( details, self.metadatautils.get_moviesetdetails( details["title"], details["dbid"]), ["year"]) content_type = "sets" # video content elif content_type in [ "movies", "setmovies", "tvshows", "seasons", "episodes", "musicvideos" ]: # get imdb and tvdbid details[ "imdbnumber"], tvdbid = self.metadatautils.get_imdbtvdb_id( details["title"], content_type, details["year"], details["imdbnumber"], details["tvshowtitle"]) if self.exit: return # generic video properties (studio, streamdetails, omdb, top250) details = merge_dict( details, self.get_directors_writers(details["director"], details["writer"])) if self.enable_extrafanart: if not details["filenameandpath"]: details["filenameandpath"] = details["path"] if "videodb://" not in details["filenameandpath"]: efa = self.metadatautils.get_extrafanart( details["filenameandpath"]) if efa: details["art"] = merge_dict( details["art"], efa["art"]) if self.enable_extraposter: if not details["filenameandpath"]: details["filenameandpath"] = details["path"] if "videodb://" not in details["filenameandpath"]: efa = self.metadatautils.get_extraposter( details["filenameandpath"]) if efa: details["art"] = merge_dict( details["art"], efa["art"]) if self.exit: return details = merge_dict( details, self.metadatautils.get_duration(details["duration"])) details = merge_dict(details, self.get_genres(details["genre"])) details = merge_dict( details, self.metadatautils.get_studio_logo(details["studio"])) details = merge_dict( details, self.metadatautils.get_omdb_info( details["imdbnumber"])) details = merge_dict( details, self.get_streamdetails(details["dbid"], details["path"], content_type)) details = merge_dict( details, self.metadatautils.get_top250_rating( details["imdbnumber"])) if self.exit: return # tvshows-only properties (tvdb) if content_type in ["tvshows", "seasons", "episodes"]: details = merge_dict( details, self.metadatautils.get_tvdb_details( details["imdbnumber"], tvdbid)) # movies-only properties (tmdb, animated art) if content_type in ["movies", "setmovies"]: details = merge_dict( details, self.metadatautils.get_tmdb_details( details["imdbnumber"])) if details["imdbnumber"] and self.enable_animatedart: details = self.metadatautils.extend_dict( details, self.metadatautils.get_animated_artwork( details["imdbnumber"])) if self.exit: return # extended art if self.enable_extendedart: tmdbid = details.get("tmdb_id", "") details = self.metadatautils.extend_dict( details, self.metadatautils.get_extended_artwork( details["imdbnumber"], tvdbid, tmdbid, content_type), [ "posters", "clearlogos", "banners", "discarts", "cleararts", "characterarts" ]) # monitor listitem props when PVR is active elif content_type in [ "tvchannels", "tvrecordings", "channels", "recordings", "timers", "tvtimers" ]: details = self.get_pvr_artwork(details, prefix) # process all properties all_props = prepare_win_props(details) if "sets" not in content_type: self.listitem_details[cur_listitem] = all_props self.lookup_busy.pop(cur_listitem, None) if cur_listitem == self.last_listitem: self.set_win_props(all_props) except Exception as exc: log_exception(__name__, exc) self.lookup_busy.pop(cur_listitem, None) def delayed_flush(self, cur_listitem): '''flushes existing properties when it takes too long to grab the new ones''' xbmc.sleep(500) if cur_listitem == self.last_listitem and cur_listitem in self.lookup_busy: self.reset_win_props() def do_background_work(self): '''stuff that's processed in the background''' try: if self.exit: return log_msg("Started Background worker...") self.set_generic_props() self.listitem_details = {} if self.exit: return self.cache.check_cleanup() log_msg("Ended Background worker...") except Exception as exc: log_exception(__name__, exc) def set_generic_props(self): '''set some generic window props with item counts''' # GET TOTAL ADDONS COUNT addons_count = len(kodi_json('Addons.GetAddons')) self.win.setProperty("SkinHelper.TotalAddons", "%s" % addons_count) addontypes = [] addontypes.append(("executable", "SkinHelper.TotalProgramAddons")) addontypes.append(("video", "SkinHelper.TotalVideoAddons")) addontypes.append(("audio", "SkinHelper.TotalAudioAddons")) addontypes.append(("image", "SkinHelper.TotalPicturesAddons")) for addontype in addontypes: media_array = kodi_json('Addons.GetAddons', {"content": addontype[0]}) self.win.setProperty(addontype[1], str(len(media_array))) # GET FAVOURITES COUNT favs = kodi_json('Favourites.GetFavourites') if favs: self.win.setProperty("SkinHelper.TotalFavourites", "%s" % len(favs)) if self.exit: return # GET TV CHANNELS COUNT if getCondVisibility("Pvr.HasTVChannels"): tv_channels = kodi_json('PVR.GetChannels', {"channelgroupid": "alltv"}) self.win.setProperty("SkinHelper.TotalTVChannels", "%s" % len(tv_channels)) if self.exit: return # GET MOVIE SETS COUNT movieset_movies_count = 0 moviesets = kodi_json('VideoLibrary.GetMovieSets') for item in moviesets: for item in kodi_json('VideoLibrary.GetMovieSetDetails', {"setid": item["setid"]}): movieset_movies_count += 1 self.win.setProperty("SkinHelper.TotalMovieSets", "%s" % len(moviesets)) self.win.setProperty("SkinHelper.TotalMoviesInSets", "%s" % movieset_movies_count) if self.exit: return # GET RADIO CHANNELS COUNT if getCondVisibility("Pvr.HasRadioChannels"): radio_channels = kodi_json('PVR.GetChannels', {"channelgroupid": "allradio"}) self.win.setProperty("SkinHelper.TotalRadioChannels", "%s" % len(radio_channels)) def reset_win_props(self): '''reset all window props set by the script...''' self.metadatautils.process_method_on_list( self.win.clearProperty, self.all_window_props.iterkeys()) self.all_window_props = {} def set_win_prop(self, prop_tuple): '''sets a window property based on the given key-value''' key = prop_tuple[0] value = prop_tuple[1] if (key not in self.all_window_props) or ( key in self.all_window_props and self.all_window_props[key] != value): self.all_window_props[key] = value self.win.setProperty(key, value) def set_win_props(self, prop_tuples): '''set multiple window properties from list of tuples''' self.metadatautils.process_method_on_list(self.set_win_prop, prop_tuples) # cleanup remaining properties new_keys = [item[0] for item in prop_tuples] for key, value in self.all_window_props.iteritems(): if value and key not in new_keys: self.all_window_props[key] = "" self.win.clearProperty(key) def set_content_header(self, content_type): '''sets a window propery which can be used as headertitle''' self.win.clearProperty("SkinHelper.ContentHeader") itemscount = xbmc.getInfoLabel("Container.NumItems") if itemscount: if xbmc.getInfoLabel( "Container.ListItemNoWrap(0).Label" ).startswith("*") or xbmc.getInfoLabel( "Container.ListItemNoWrap(1).Label").startswith("*"): itemscount = int(itemscount) - 1 headerprefix = "" if content_type == "movies": headerprefix = xbmc.getLocalizedString(36901) elif content_type == "tvshows": headerprefix = xbmc.getLocalizedString(36903) elif content_type == "seasons": headerprefix = xbmc.getLocalizedString(36905) elif content_type == "episodes": headerprefix = xbmc.getLocalizedString(36907) elif content_type == "sets": headerprefix = xbmc.getLocalizedString(36911) elif content_type == "albums": headerprefix = xbmc.getLocalizedString(36919) elif content_type == "songs": headerprefix = xbmc.getLocalizedString(36921) elif content_type == "artists": headerprefix = xbmc.getLocalizedString(36917) if headerprefix: self.win.setProperty("SkinHelper.ContentHeader", "%s %s" % (itemscount, headerprefix)) @staticmethod def get_genres(genres): '''get formatted genre string from actual genre''' details = {} if not isinstance(genres, list): genres = genres.split(" / ") details['genres'] = "[CR]".join(genres) for count, genre in enumerate(genres): details["genre.%s" % count] = genre return details @staticmethod def get_directors_writers(director, writer): '''get a formatted string with directors/writers from the actual string''' directors = director.split(" / ") writers = writer.split(" / ") return { 'Directors': "[CR]".join(directors), 'Writers': "[CR]".join(writers) } def get_listitem_details(self, content_type, prefix): '''collect all listitem properties/values we need''' listitem_details = {"art": {}} # basic properties for prop in ["dbtype", "dbid", "imdbnumber"]: propvalue = xbmc.getInfoLabel('$INFO[%sListItem.%s]' % (prefix, prop)).decode('utf-8') if not propvalue or propvalue == "-1": propvalue = xbmc.getInfoLabel( '$INFO[%sListItem.Property(%s)]' % (prefix, prop)).decode('utf-8') listitem_details[prop] = propvalue # generic properties props = [ "label", "title", "filenameandpath", "year", "genre", "path", "folderpath", "duration", "plot", "plotoutline", "label2", "icon", "thumb" ] # properties for media items if content_type in [ "movies", "tvshows", "seasons", "episodes", "musicvideos", "setmovies" ]: props += [ "studio", "tvshowtitle", "premiered", "director", "writer", "firstaired", "tagline", "rating", "season", "episode" ] # properties for music items elif content_type in ["musicvideos", "artists", "albums", "songs"]: props += ["artist", "album", "rating", "albumartist", "discnumber"] # properties for pvr items elif content_type in [ "tvchannels", "tvrecordings", "channels", "recordings", "timers", "tvtimers" ]: props += ["channel", "channelname"] for prop in props: if self.exit: break propvalue = xbmc.getInfoLabel('$INFO[%sListItem.%s]' % (prefix, prop)).decode('utf-8') listitem_details[prop] = propvalue # artwork properties artprops = [ "fanart", "poster", "clearlogo", "clearart", "landscape", "thumb", "banner", "discart", "characterart" ] for prop in artprops: if self.exit: break propvalue = xbmc.getInfoLabel('$INFO[%sListItem.Art(%s)]' % (prefix, prop)).decode('utf-8') if not propvalue: propvalue = xbmc.getInfoLabel( '$INFO[%sListItem.Art(tvshow.%s)]' % (prefix, prop)).decode('utf-8') if propvalue: listitem_details["art"][prop] = propvalue # fix for folderpath if not listitem_details.get( "path") and "folderpath" in listitem_details: listitem_details["path"] = listitem_details["folderpath"] # fix for thumb if "thumb" not in listitem_details[ "art"] and "thumb" in listitem_details: listitem_details["art"]["thumb"] = listitem_details["thumb"] if "fanart" not in listitem_details[ "art"] and "fanart" in listitem_details: listitem_details["art"]["fanart"] = listitem_details["fanart"] return listitem_details def get_streamdetails(self, li_dbid, li_path, content_type): '''get the streamdetails for the current video''' details = {} if li_dbid and content_type in [ "movies", "episodes", "musicvideos" ] and not li_path.startswith("videodb://movies/sets/"): details = self.metadatautils.get_streamdetails( li_dbid, content_type) return details def set_forcedview(self, content_type): '''helper to force the view in certain conditions''' if self.enable_forcedviews: cur_forced_view = xbmc.getInfoLabel( "Skin.String(SkinHelper.ForcedViews.%s)" % content_type) if getCondVisibility( "Control.IsVisible(%s) | IsEmpty(Container.Viewmode) | System.HasModalDialog | System.HasVisibleModalDialog" % cur_forced_view): # skip if the view is already visible or if we're not in an actual media window return if (content_type and cur_forced_view and cur_forced_view != "None" and not getCondVisibility("Window.IsActive(MyPvrGuide.xml)")): self.win.setProperty("SkinHelper.ForcedView", cur_forced_view) count = 0 while not getCondVisibility( "Control.HasFocus(%s)" % cur_forced_view): xbmc.sleep(100) xbmc.executebuiltin("Container.SetViewMode(%s)" % cur_forced_view) xbmc.executebuiltin("SetFocus(%s)" % cur_forced_view) count += 1 if count == 50 or self.exit: break else: self.win.clearProperty("SkinHelper.ForcedView") else: self.win.clearProperty("SkinHelper.ForcedView") def get_pvr_artwork(self, listitem, prefix): '''get pvr artwork from artwork module''' if self.enable_pvrart: if getCondVisibility( "%sListItem.IsFolder" % prefix ) and not listitem["channelname"] and not listitem["title"]: listitem["title"] = listitem["label"] listitem = self.metadatautils.extend_dict( listitem, self.metadatautils.get_pvr_artwork(listitem["title"], listitem["channelname"], listitem["genre"]), ["title", "genre", "genres", "thumb"]) # pvr channellogo if listitem["channelname"]: listitem["art"][ "ChannelLogo"] = self.metadatautils.get_channellogo( listitem["channelname"]) elif listitem.get("pvrchannel"): listitem["art"][ "ChannelLogo"] = self.metadatautils.get_channellogo( listitem["pvrchannel"]) return listitem
class MainModule: '''mainmodule provides the script methods for the skinhelper addon''' def __init__(self): '''Initialization and main code run''' self.win = xbmcgui.Window(10000) self.addon = xbmcaddon.Addon(ADDON_ID) self.kodidb = KodiDb() self.cache = SimpleCache() self.params = self.get_params() log_msg("MainModule called with parameters: %s" % self.params) action = self.params.get("action", "") # launch module for action provided by this script try: getattr(self, action)() except AttributeError: log_exception(__name__, "No such action: %s" % action) except Exception as exc: log_exception(__name__, exc) finally: xbmc.executebuiltin("dialog.Close(busydialog)") # do cleanup self.close() def close(self): '''Cleanup Kodi Cpython instances on exit''' self.cache.close() del self.win del self.addon del self.kodidb log_msg("MainModule exited") @classmethod def get_params(self): '''extract the params from the called script path''' params = {} for arg in sys.argv[1:]: paramname = arg.split('=')[0] paramvalue = arg.replace(paramname + "=", "") paramname = paramname.lower() if paramname == "action": paramvalue = paramvalue.lower() params[paramname] = paramvalue return params def deprecated_method(self, newaddon): ''' used when one of the deprecated methods is called print warning in log and call the external script with the same parameters ''' action = self.params.get("action") log_msg( "Deprecated method: %s. Please call %s directly" % (action, newaddon), xbmc.LOGWARNING) paramstring = "" for key, value in self.params.iteritems(): paramstring += ",%s=%s" % (key, value) if xbmc.getCondVisibility("System.HasAddon(%s)" % newaddon): xbmc.executebuiltin("RunAddon(%s%s)" % (newaddon, paramstring)) else: # trigger install of the addon if KODI_VERSION > 16: xbmc.executebuiltin("InstallAddon(%s)" % newaddon) else: xbmc.executebuiltin("RunPlugin(plugin://%s)" % newaddon) @staticmethod def musicsearch(): '''helper to go directly to music search dialog''' xbmc.executebuiltin("ActivateWindow(Music)") xbmc.executebuiltin("SendClick(8)") def setview(self): '''sets the selected viewmode for the container''' xbmc.executebuiltin("ActivateWindow(busydialog)") content_type = get_current_content_type() if not content_type: content_type = "files" current_view = xbmc.getInfoLabel("Container.Viewmode").decode("utf-8") view_id, view_label = self.selectview(content_type, current_view) current_forced_view = xbmc.getInfoLabel( "Skin.String(SkinHelper.ForcedViews.%s)" % content_type) if view_id is not None: # also store forced view if (content_type and current_forced_view and current_forced_view != "None" and xbmc.getCondVisibility( "Skin.HasSetting(SkinHelper.ForcedViews.Enabled)")): xbmc.executebuiltin( "Skin.SetString(SkinHelper.ForcedViews.%s,%s)" % (content_type, view_id)) xbmc.executebuiltin( "Skin.SetString(SkinHelper.ForcedViews.%s.label,%s)" % (content_type, view_label)) self.win.setProperty("SkinHelper.ForcedView", view_id) if not xbmc.getCondVisibility( "Control.HasFocus(%s)" % current_forced_view): xbmc.sleep(100) xbmc.executebuiltin("Container.SetViewMode(%s)" % view_id) xbmc.executebuiltin("SetFocus(%s)" % view_id) else: self.win.clearProperty("SkinHelper.ForcedView") # set view xbmc.executebuiltin("Container.SetViewMode(%s)" % view_id) def selectview(self, content_type="other", current_view=None, display_none=False): '''reads skinfile with all views to present a dialog to choose from''' cur_view_select_id = None label = "" all_views = [] if display_none: listitem = xbmcgui.ListItem(label="None") listitem.setProperty("id", "None") all_views.append(listitem) # read the special skin views file views_file = xbmc.translatePath( 'special://skin/extras/views.xml').decode("utf-8") if xbmcvfs.exists(views_file): doc = parse(views_file) listing = doc.documentElement.getElementsByTagName('view') itemcount = 0 for view in listing: label = xbmc.getLocalizedString( int(view.attributes['languageid'].nodeValue)) viewid = view.attributes['value'].nodeValue mediatypes = view.attributes['type'].nodeValue.lower().split( ",") if label.lower() == current_view.lower( ) or viewid == current_view: cur_view_select_id = itemcount if display_none: cur_view_select_id += 1 if (("all" in mediatypes or content_type.lower() in mediatypes) and (not "!" + content_type.lower() in mediatypes) and not xbmc.getCondVisibility( "Skin.HasSetting(SkinHelper.view.Disabled.%s)" % viewid)): image = "special://skin/extras/viewthumbs/%s.jpg" % viewid listitem = xbmcgui.ListItem(label=label, iconImage=image) listitem.setProperty("viewid", viewid) listitem.setProperty("icon", image) all_views.append(listitem) itemcount += 1 dialog = DialogSelect("DialogSelect.xml", "", listing=all_views, windowtitle=self.addon.getLocalizedString(32012), richlayout=True) dialog.autofocus_id = cur_view_select_id dialog.doModal() result = dialog.result del dialog if result: viewid = result.getProperty("viewid") label = result.getLabel().decode("utf-8") return (viewid, label) else: return (None, None) # pylint: disable-msg=too-many-local-variables def enableviews(self): '''show select dialog to enable/disable views''' all_views = [] views_file = xbmc.translatePath( 'special://skin/extras/views.xml').decode("utf-8") richlayout = self.params.get("richlayout", "") == "true" if xbmcvfs.exists(views_file): doc = parse(views_file) listing = doc.documentElement.getElementsByTagName('view') for view in listing: view_id = view.attributes['value'].nodeValue label = xbmc.getLocalizedString( int(view.attributes['languageid'].nodeValue)) desc = label + " (" + str(view_id) + ")" image = "special://skin/extras/viewthumbs/%s.jpg" % view_id listitem = xbmcgui.ListItem(label=label, label2=desc, iconImage=image) listitem.setProperty("viewid", view_id) if not xbmc.getCondVisibility( "Skin.HasSetting(SkinHelper.view.Disabled.%s)" % view_id): listitem.select(selected=True) excludefromdisable = False try: excludefromdisable = view.attributes[ 'excludefromdisable'].nodeValue == "true" except Exception: pass if not excludefromdisable: all_views.append(listitem) dialog = DialogSelect("DialogSelect.xml", "", listing=all_views, windowtitle=self.addon.getLocalizedString(32013), multiselect=True, richlayout=richlayout) dialog.doModal() result = dialog.result del dialog if result: for item in result: view_id = item.getProperty("viewid") if item.isSelected(): # view is enabled xbmc.executebuiltin( "Skin.Reset(SkinHelper.view.Disabled.%s)" % view_id) else: # view is disabled xbmc.executebuiltin( "Skin.SetBool(SkinHelper.view.Disabled.%s)" % view_id) # pylint: enable-msg=too-many-local-variables def setforcedview(self): '''helper that sets a forced view for a specific content type''' content_type = self.params.get("contenttype") if content_type: current_view = xbmc.getInfoLabel( "Skin.String(SkinHelper.ForcedViews.%s)" % content_type) if not current_view: current_view = "0" view_id, view_label = self.selectview(content_type, current_view, True) if view_id or view_label: xbmc.executebuiltin( "Skin.SetString(SkinHelper.ForcedViews.%s,%s)" % (content_type, view_id)) xbmc.executebuiltin( "Skin.SetString(SkinHelper.ForcedViews.%s.label,%s)" % (content_type, view_label)) @staticmethod def get_youtube_listing(searchquery): '''get items from youtube plugin by query''' lib_path = u"plugin://plugin.video.youtube/kodion/search/query/?q=%s" % searchquery return KodiDb().files(lib_path) def searchyoutube(self): '''helper to search youtube for the given title''' xbmc.executebuiltin("ActivateWindow(busydialog)") title = self.params.get("title", "") window_header = self.params.get("header", "") results = [] for media in self.get_youtube_listing(title): if not media["filetype"] == "directory": label = media["label"] label2 = media["plot"] image = "" if media.get('art'): if media['art'].get('thumb'): image = (media['art']['thumb']) listitem = xbmcgui.ListItem(label=label, label2=label2, iconImage=image) listitem.setProperty("path", media["file"]) results.append(listitem) # finished lookup - display listing with results xbmc.executebuiltin("dialog.Close(busydialog)") dialog = DialogSelect("DialogSelect.xml", "", listing=results, windowtitle=window_header, multiselect=False, richlayout=True) dialog.doModal() result = dialog.result del dialog if result: if xbmc.getCondVisibility( "Window.IsActive(script-skin_helper_service-CustomInfo.xml) | " "Window.IsActive(movieinformation)"): xbmc.executebuiltin("Dialog.Close(movieinformation)") xbmc.executebuiltin( "Dialog.Close(script-skin_helper_service-CustomInfo.xml)") xbmc.sleep(1000) xbmc.executebuiltin('PlayMedia("%s")' % result.getProperty("path")) del result def getcastmedia(self): '''helper to show a dialog with all media for a specific actor''' xbmc.executebuiltin("ActivateWindow(busydialog)") name = self.params.get("name", "") window_header = self.params.get("name", "") results = [] items = self.kodidb.castmedia(name) items = process_method_on_list(self.kodidb.prepare_listitem, items) for item in items: if item["file"].startswith("videodb://"): item[ "file"] = "ActivateWindow(Videos,%s,return)" % item["file"] else: item["file"] = 'PlayMedia("%s")' % item["file"] results.append(self.kodidb.create_listitem(item, False)) # finished lookup - display listing with results xbmc.executebuiltin("dialog.Close(busydialog)") dialog = DialogSelect("DialogSelect.xml", "", listing=results, windowtitle=window_header, richlayout=True) dialog.doModal() result = dialog.result del dialog if result: while xbmc.getCondVisibility("System.HasModalDialog"): xbmc.executebuiltin("Action(Back)") xbmc.sleep(300) xbmc.executebuiltin(result.getfilename()) del result def setfocus(self): '''helper to set focus on a list or control''' control = self.params.get("control") fallback = self.params.get("fallback") position = self.params.get("position", "0") relativeposition = self.params.get("relativeposition") if relativeposition: position = int(relativeposition) - 1 count = 0 if control: while not xbmc.getCondVisibility("Control.HasFocus(%s)" % control): if xbmc.getCondVisibility("Window.IsActive(busydialog)"): xbmc.sleep(150) continue elif count == 20 or (xbmc.getCondVisibility( "!Control.IsVisible(%s) | " "!IntegerGreaterThan(Container(%s).NumItems,0)" % (control, control))): if fallback: xbmc.executebuiltin("Control.SetFocus(%s)" % fallback) break else: xbmc.executebuiltin("Control.SetFocus(%s,%s)" % (control, position)) xbmc.sleep(50) count += 1 def setwidgetcontainer(self): '''helper that reports the current selected widget container/control''' controls = self.params.get("controls", "").split("-") if controls: xbmc.sleep(50) for i in range(10): for control in controls: if xbmc.getCondVisibility( "Control.IsVisible(%s) + IntegerGreaterThan(Container(%s).NumItems,0)" % (control, control)): self.win.setProperty("SkinHelper.WidgetContainer", control) return xbmc.sleep(50) self.win.clearProperty("SkinHelper.WidgetContainer") def saveskinimage(self): '''let the user select an image and save it to addon_data for easy backup''' skinstring = self.params.get("skinstring", "") allow_multi = self.params.get("multi", "") == "true" header = self.params.get("header", "") value = SkinSettings().save_skin_image(skinstring, allow_multi, header) if value: xbmc.executebuiltin( "Skin.SetString(%s,%s)" % (skinstring.encode("utf-8"), value.encode("utf-8"))) @staticmethod def checkskinsettings(): '''performs check of all default skin settings and labels''' SkinSettings().correct_skin_settings() def setskinsetting(self): '''allows the user to set a skin setting with a select dialog''' setting = self.params.get("setting", "") org_id = self.params.get("id", "") if "$" in org_id: org_id = xbmc.getInfoLabel(org_id).decode("utf-8") header = self.params.get("header", "") SkinSettings().set_skin_setting(setting=setting, window_header=header, original_id=org_id) def setskinconstant(self): '''allows the user to set a skin constant with a select dialog''' setting = self.params.get("setting", "") value = self.params.get("value", "") header = self.params.get("header", "") SkinSettings().set_skin_constant(setting, header, value) def setskinconstants(self): '''allows the skinner to set multiple skin constants''' settings = self.params.get("settings", "").split("|") values = self.params.get("values", "").split("|") SkinSettings().set_skin_constants(settings, values) def setskinshortcutsproperty(self): '''allows the user to make a setting for skinshortcuts using the special skinsettings dialogs''' setting = self.params.get("setting", "") prop = self.params.get("property", "") header = self.params.get("header", "") SkinSettings().set_skinshortcuts_property(setting, header, prop) def togglekodisetting(self): '''toggle kodi setting''' settingname = self.params.get("setting", "") cur_value = xbmc.getCondVisibility("system.getbool(%s)" % settingname) if cur_value: new_value = "false" else: new_value = "true" xbmc.executeJSONRPC( '{"jsonrpc":"2.0", "id":1, "method":"Settings.SetSettingValue","params":{"setting":"%s","value":%s}}' % (settingname, new_value)) def setkodisetting(self): '''set kodi setting''' settingname = self.params.get("setting", "") value = self.params.get("value", "") is_int = False try: valueint = int(value) is_int = True del valueint except Exception: pass if value.lower() == "true": value = 'true' elif value.lower() == "false": value = 'false' elif is_int: value = '"%s"' % value xbmc.executeJSONRPC( '{"jsonrpc":"2.0", "id":1, "method":"Settings.SetSettingValue",\ "params":{"setting":"%s","value":%s}}' % (settingname, value)) def playtrailer(self): '''auto play windowed trailer inside video listing''' if not xbmc.getCondVisibility( "Player.HasMedia | Container.Scrolling | Container.OnNext | " "Container.OnPrevious | !IsEmpty(Window(Home).Property(traileractionbusy))" ): self.win.setProperty("traileractionbusy", "traileractionbusy") widget_container = self.params.get("widgetcontainer", "") trailer_mode = self.params.get("mode", "").replace("auto_", "") allow_youtube = self.params.get("youtube", "") == "true" if not trailer_mode: trailer_mode = "windowed" if widget_container: widget_container_prefix = "Container(%s)." % widget_container else: widget_container_prefix = "" li_title = xbmc.getInfoLabel( "%sListItem.Title" % widget_container_prefix).decode('utf-8') li_trailer = xbmc.getInfoLabel( "%sListItem.Trailer" % widget_container_prefix).decode('utf-8') if not li_trailer and allow_youtube: youtube_result = self.get_youtube_listing("%s Trailer" % li_title) if youtube_result: li_trailer = youtube_result[0].get("file") # always wait a bit to prevent trailer start playing when we're scrolling the list xbmc.Monitor().waitForAbort(3) if li_trailer and (li_title == xbmc.getInfoLabel( "%sListItem.Title" % widget_container_prefix).decode('utf-8')): if trailer_mode == "fullscreen" and li_trailer: xbmc.executebuiltin('PlayMedia("%s")' % li_trailer) else: xbmc.executebuiltin('PlayMedia("%s",1)' % li_trailer) self.win.setProperty("TrailerPlaying", trailer_mode) self.win.clearProperty("traileractionbusy") def colorpicker(self): '''legacy''' self.deprecated_method("script.skin.helper.colorpicker") def backup(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def restore(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def reset(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def colorthemes(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def createcolortheme(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def restorecolortheme(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def conditionalbackgrounds(self): '''legacy''' self.deprecated_method("script.skin.helper.backgrounds") def splashscreen(self): '''helper to show a user defined splashscreen in the skin''' import time splashfile = self.params.get("file", "") duration = int(self.params.get("duration", 5)) if (splashfile.lower().endswith("jpg") or splashfile.lower().endswith("gif") or splashfile.lower().endswith("png") or splashfile.lower().endswith("tiff")): # this is an image file self.win.setProperty("SkinHelper.SplashScreen", splashfile) # for images we just wait for X seconds to close the splash again start_time = time.time() while (time.time() - start_time) <= duration: xbmc.sleep(500) else: # for video or audio we have to wait for the player to finish... xbmc.Player().play(splashfile, windowed=True) xbmc.sleep(500) while xbmc.getCondVisibility("Player.HasMedia"): xbmc.sleep(150) # replace startup window with home startupwindow = xbmc.getInfoLabel("System.StartupWindow") xbmc.executebuiltin("ReplaceWindow(%s)" % startupwindow) autostart_playlist = xbmc.getInfoLabel( "$ESCINFO[Skin.String(autostart_playlist)]") if autostart_playlist: xbmc.executebuiltin("PlayMedia(%s)" % autostart_playlist) def videosearch(self): '''show the special search dialog''' xbmc.executebuiltin("ActivateWindow(busydialog)") from resources.lib.searchdialog import SearchDialog search_dialog = SearchDialog( "script-skin_helper_service-CustomSearch.xml", self.addon.getAddonInfo('path').decode("utf-8"), "Default", "1080i") search_dialog.doModal() del search_dialog def showinfo(self): '''shows our special videoinfo dialog''' dbid = self.params.get("dbid", "") dbtype = self.params.get("dbtype", "") from infodialog import show_infodialog show_infodialog(dbid, dbtype) def deletedir(self): '''helper to delete a directory, input can be normal filesystem path or vfs''' del_path = self.params.get("path") if del_path: ret = xbmcgui.Dialog().yesno( heading=xbmc.getLocalizedString(122), line1=u"%s[CR]%s" % (xbmc.getLocalizedString(125), del_path)) if ret: success = recursive_delete_dir(del_path) if success: xbmcgui.Dialog().ok( heading=xbmc.getLocalizedString(19179), line1=self.addon.getLocalizedString(32014)) else: xbmcgui.Dialog().ok(heading=xbmc.getLocalizedString(16205), line1=xbmc.getLocalizedString(32015)) def overlaytexture(self): '''legacy: helper to let the user choose a background overlay from a skin defined folder''' skinstring = self.params.get("skinstring", "BackgroundOverlayTexture") self.params["skinstring"] = skinstring self.params["resourceaddon"] = "resource.images.backgroundoverlays" self.params["customfolder"] = "special://skin/extras/bgoverlays/" self.params["allowmulti"] = "false" self.params["header"] = self.addon.getLocalizedString(32002) self.selectimage() def busytexture(self): '''legacy: helper which lets the user select a busy spinner from predefined spinners in the skin''' skinstring = self.params.get("skinstring", "SkinHelper.SpinnerTexture") self.params["skinstring"] = skinstring self.params["resourceaddon"] = "resource.images.busyspinners" self.params["customfolder"] = "special://skin/extras/busy_spinners/" self.params["allowmulti"] = "true" self.params["header"] = self.addon.getLocalizedString(32006) self.selectimage() def selectimage(self): '''helper which lets the user select an image or imagepath from resourceaddons or custom path''' skinsettings = SkinSettings() skinstring = self.params.get("skinstring", "") skinshortcutsprop = self.params.get("skinshortcutsproperty", "") current_value = self.params.get("currentvalue", "") resource_addon = self.params.get("resourceaddon", "") allow_multi = self.params.get("allowmulti", "false") == "true" windowheader = self.params.get("header", "") skinhelper_backgrounds = self.params.get("skinhelperbackgrounds", "false") == "true" label, value = skinsettings.select_image( skinstring, allow_multi=allow_multi, windowheader=windowheader, resource_addon=resource_addon, skinhelper_backgrounds=skinhelper_backgrounds, current_value=current_value) if label: if skinshortcutsprop: # write value to skinshortcuts prop from skinshortcuts import set_skinshortcuts_property set_skinshortcuts_property(skinshortcutsprop, value, label) else: # write the values to skin strings if value.startswith("$INFO"): # we got an dynamic image from window property skinsettings.set_skin_variable(skinstring, value) value = "$VAR[%s]" % skinstring skinstring = skinstring.encode("utf-8") label = label.encode("utf-8") xbmc.executebuiltin("Skin.SetString(%s.label,%s)" % (skinstring, label)) xbmc.executebuiltin("Skin.SetString(%s.name,%s)" % (skinstring, label)) xbmc.executebuiltin("Skin.SetString(%s,%s)" % (skinstring, value)) xbmc.executebuiltin("Skin.SetString(%s.path,%s)" % (skinstring, value)) del skinsettings def dialogok(self): '''helper to show an OK dialog with a message''' headertxt = self.params.get("header") bodytxt = self.params.get("message") if bodytxt.startswith(" "): bodytxt = bodytxt[1:] if headertxt.startswith(" "): headertxt = headertxt[1:] dialog = xbmcgui.Dialog() dialog.ok(heading=headertxt, line1=bodytxt) del dialog def dialogyesno(self): '''helper to show a YES/NO dialog with a message''' headertxt = self.params.get("header") bodytxt = self.params.get("message") yesactions = self.params.get("yesaction", "").split("|") noactions = self.params.get("noaction", "").split("|") if bodytxt.startswith(" "): bodytxt = bodytxt[1:] if headertxt.startswith(" "): headertxt = headertxt[1:] if xbmcgui.Dialog().yesno(heading=headertxt, line1=bodytxt): for action in yesactions: xbmc.executebuiltin(action.encode("utf-8")) else: for action in noactions: xbmc.executebuiltin(action.encode("utf-8")) def textviewer(self): '''helper to show a textviewer dialog with a message''' headertxt = self.params.get("header", "") bodytxt = self.params.get("message", "") if bodytxt.startswith(" "): bodytxt = bodytxt[1:] if headertxt.startswith(" "): headertxt = headertxt[1:] xbmcgui.Dialog().textviewer(headertxt, bodytxt) def fileexists(self): '''helper to let the skinner check if a file exists and write the outcome to a window prop or skinstring''' filename = self.params.get("file") skinstring = self.params.get("skinstring") windowprop = self.params.get("winprop") if xbmcvfs.exists(filename): if windowprop: self.win.setProperty(windowprop, "exists") if skinstring: xbmc.executebuiltin("Skin.SetString(%s,exists)" % skinstring) else: if windowprop: self.win.clearProperty(windowprop) if skinstring: xbmc.executebuiltin("Skin.Reset(%s)" % skinstring) def stripstring(self): '''helper to allow the skinner to strip a string and write results to a skin string''' splitchar = self.params.get("splitchar") if splitchar.upper() == "[SPACE]": splitchar = " " skinstring = self.params.get("string") if not skinstring: skinstring = self.params.get("skinstring") output = self.params.get("output") index = self.params.get("index", 0) skinstring = skinstring.split(splitchar)[int(index)] self.win.setProperty(output, skinstring) def getfilename(self, filename=""): '''helper to display a sanitized filename in the vidoeinfo dialog''' output = self.params.get("output") if not filename: filename = xbmc.getInfoLabel("ListItem.FileNameAndPath") if not filename: filename = xbmc.getInfoLabel("ListItem.FileName") if "filename=" in filename: url_params = dict(urlparse.parse_qsl(filename)) filename = url_params.get("filename") self.win.setProperty(output, filename) def getplayerfilename(self): '''helper to parse the filename from a plugin (e.g. emby) filename''' filename = xbmc.getInfoLabel("Player.FileNameAndPath") if not filename: filename = xbmc.getInfoLabel("Player.FileName") self.getfilename(filename) def getpercentage(self): '''helper to calculate the percentage of 2 numbers and write results to a skinstring''' total = int(params.get("total")) count = int(params.get("count")) roundsteps = self.params.get("roundsteps") skinstring = self.params.get("skinstring") percentage = int(round((1.0 * count / total) * 100)) if roundsteps: roundsteps = int(roundsteps) percentage = percentage + (roundsteps - percentage) % roundsteps xbmc.executebuiltin("Skin.SetString(%s,%s)" % (skinstring, percentage)) def setresourceaddon(self): '''helper to let the user choose a resource addon and set that as skin string''' from resourceaddons import setresourceaddon addontype = self.params.get("addontype", "") skinstring = self.params.get("skinstring", "") setresourceaddon(addontype, skinstring) def checkresourceaddons(self): '''allow the skinner to perform a basic check if some required resource addons are available''' from resourceaddons import checkresourceaddons addonslist = self.params.get("addonslist", []) if addonslist: addonslist = addonslist.split("|") checkresourceaddons(addonslist)
class MetadataUtils(object): ''' Provides all kind of mediainfo for kodi media, returned as dict with details ''' close_called = False def __init__(self): '''Initialize and load all our helpers''' self._studiologos_path = "" self.cache = SimpleCache() self.addon = xbmcaddon.Addon(ADDON_ID) self.kodidb = KodiDb() self.omdb = Omdb(self.cache) self.tmdb = Tmdb(self.cache) self.channellogos = ChannelLogos(self.kodidb) self.fanarttv = FanartTv(self.cache) self.imdb = Imdb(self.cache) self.google = GoogleImages(self.cache) self.studiologos = StudioLogos(self.cache) self.animatedart = AnimatedArt(self.cache, self.kodidb) self.thetvdb = TheTvDb() self.musicart = MusicArtwork(self) self.pvrart = PvrArtwork(self) log_msg("Initialized") def close(self): '''Cleanup instances''' self.close_called = True self.cache.close() self.addon = None del self.addon del self.kodidb del self.omdb del self.tmdb del self.channellogos del self.fanarttv del self.imdb del self.google del self.studiologos del self.animatedart del self.thetvdb del self.musicart del self.pvrart log_msg("Exited") def __del__(self): '''make sure close is called''' if not self.close_called: self.close() @use_cache(14) def get_extrafanart(self, file_path): '''helper to retrieve the extrafanart path for a kodi media item''' from helpers.extrafanart import get_extrafanart return get_extrafanart(file_path) def get_music_artwork(self, artist, album="", track="", disc="", ignore_cache=False, flush_cache=False): '''method to get music artwork for the goven artist/album/song''' return self.musicart.get_music_artwork(artist, album, track, disc, ignore_cache=ignore_cache, flush_cache=flush_cache) def music_artwork_options(self, artist, album="", track="", disc=""): '''options for music metadata for specific item''' return self.musicart.music_artwork_options(artist, album, track, disc) @use_cache(14) def get_extended_artwork(self, imdb_id="", tvdb_id="", tmdb_id="", media_type=""): '''get extended artwork for the given imdbid or tvdbid''' from urllib import quote_plus result = {"art": {}} if "movie" in media_type and tmdb_id: result["art"] = self.fanarttv.movie(tmdb_id) elif "movie" in media_type and imdb_id: result["art"] = self.fanarttv.movie(imdb_id) elif media_type in ["tvshow", "tvshows", "seasons", "episodes"]: if not tvdb_id: if imdb_id and not imdb_id.startswith("tt"): tvdb_id = imdb_id elif imdb_id: tvdb_id = self.thetvdb.get_series_by_imdb_id(imdb_id).get( "tvdb_id") if tvdb_id: result["art"] = self.fanarttv.tvshow(tvdb_id) # add additional art with special path for arttype in ["fanarts", "posters", "clearlogos", "banners"]: if result["art"].get(arttype): result["art"][arttype] = "plugin://script.skin.helper.service/"\ "?action=extrafanart&fanarts=%s" % quote_plus(repr(result["art"][arttype])) return result @use_cache(14) def get_tmdb_details(self, imdb_id="", tvdb_id="", title="", year="", media_type="", preftype="", manual_select=False, ignore_cache=False): '''returns details from tmdb''' result = {} if imdb_id: result = self.tmdb.get_videodetails_by_externalid( imdb_id, "imdb_id") elif tvdb_id: result = self.tmdb.get_videodetails_by_externalid( tvdb_id, "tvdb_id") elif title and media_type in ["movies", "setmovies", "movie"]: result = self.tmdb.search_movie(title, year, manual_select=manual_select) elif title and media_type in ["tvshows", "tvshow"]: result = self.tmdb.search_tvshow(title, year, manual_select=manual_select) elif title: result = self.tmdb.search_video(title, year, preftype=preftype, manual_select=manual_select) if result.get("status"): result["status"] = self.translate_string(result["status"]) if result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result def get_moviesetdetails(self, title, set_id): '''get a nicely formatted dict of the movieset details which we can for example set as window props''' # get details from tmdb from helpers.moviesetdetails import get_moviesetdetails return get_moviesetdetails(self, title, set_id) @use_cache(14) def get_streamdetails(self, db_id, media_type, ignore_cache=False): '''get a nicely formatted dict of the streamdetails ''' from helpers.streamdetails import get_streamdetails return get_streamdetails(self.kodidb, db_id, media_type) def get_pvr_artwork(self, title, channel="", genre="", manual_select=False, ignore_cache=False): '''get artwork and mediadetails for PVR entries''' import requests, urllib, random print title.encode('utf8') r = requests.get( 'http://192.168.0.2:8081/api/teleguide/get_arts?title=' + urllib.quote_plus(title.encode('utf8'))) j = r.json() if j['arts']: data = { 'art': { 'fanarts': j['arts'], 'fanart': random.choice(j['arts']), 'thumb': random.choice(j['arts']) }, 'cachestr': "pvr_artwork.%s.%s" % (title.lower(), channel.lower()), 'pvrtitle': title, 'pvrchannel': channel, 'pvrgenre': genre, 'genre': [genre], 'thumbnail': random.choice(j['arts']) } else: data = self.pvrart.get_pvr_artwork(title, channel, genre, manual_select=manual_select, ignore_cache=ignore_cache) log_msg(data) return data def pvr_artwork_options(self, title, channel="", genre=""): '''options for pvr metadata for specific item''' return self.pvrart.pvr_artwork_options(title, channel, genre) @use_cache(14) def get_channellogo(self, channelname): '''get channellogo for the given channel name''' return self.channellogos.get_channellogo(channelname) def get_studio_logo(self, studio): '''get studio logo for the given studio''' # dont use cache at this level because of changing logospath return self.studiologos.get_studio_logo(studio, self.studiologos_path) @property def studiologos_path(self): '''path to use to lookup studio logos, must be set by the calling addon''' return self._studiologos_path @studiologos_path.setter def studiologos_path(self, value): '''path to use to lookup studio logos, must be set by the calling addon''' self._studiologos_path = value def get_animated_artwork(self, imdb_id, manual_select=False, ignore_cache=False): '''get animated artwork, perform extra check if local version still exists''' artwork = self.animatedart.get_animated_artwork( imdb_id, manual_select, ignore_cache=ignore_cache) if not (manual_select or ignore_cache): refresh_needed = False if artwork.get("animatedposter") and not xbmcvfs.exists( artwork["animatedposter"]): refresh_needed = True if artwork.get("animatedfanart") and not xbmcvfs.exists( artwork["animatedfanart"]): refresh_needed = True if refresh_needed: artwork = self.animatedart.get_animated_artwork( imdb_id, manual_select, ignore_cache=True) return {"art": artwork} @use_cache(14) def get_omdb_info(self, imdb_id="", title="", year="", content_type=""): '''Get (kodi compatible formatted) metadata from OMDB, including Rotten tomatoes details''' title = title.split(" (")[0] # strip year appended to title result = {} if imdb_id: result = self.omdb.get_details_by_imdbid(imdb_id) elif title and content_type in [ "seasons", "season", "episodes", "episode", "tvshows", "tvshow" ]: result = self.omdb.get_details_by_title(title, "", "tvshows") elif title and year: result = self.omdb.get_details_by_title(title, year, content_type) if result.get("status"): result["status"] = self.translate_string(result["status"]) if result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result @use_cache(7) def get_top250_rating(self, imdb_id): '''get the position in the IMDB top250 for the given IMDB ID''' return self.imdb.get_top250_rating(imdb_id) @use_cache(14) def get_duration(self, duration): '''helper to get a formatted duration''' if isinstance(duration, (str, unicode)) and ":" in duration: dur_lst = duration.split(":") return { "Duration": "%s:%s" % (dur_lst[0], dur_lst[1]), "Duration.Hours": dur_lst[0], "Duration.Minutes": dur_lst[1], "Runtime": str((int(dur_lst[0]) * 60) + int(dur_lst[1])), } else: return _get_duration(duration) @use_cache(2) def get_tvdb_details(self, imdbid="", tvdbid=""): '''get metadata from tvdb by providing a tvdbid or tmdbid''' result = {} self.thetvdb.days_ahead = 365 if not tvdbid and imdbid and not imdbid.startswith("tt"): # assume imdbid is actually a tvdbid... tvdbid = imdbid if tvdbid: result = self.thetvdb.get_series(tvdbid) elif imdbid: result = self.thetvdb.get_series_by_imdb_id(imdbid) if result: if result["status"] == "Continuing": # include next episode info result["nextepisode"] = self.thetvdb.get_nextaired_episode( result["tvdb_id"]) # include last episode info result["lastepisode"] = self.thetvdb.get_last_episode_for_series( result["tvdb_id"]) result["status"] = self.translate_string(result["status"]) if result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(_get_duration(result["runtime"])) return result @use_cache(14) def get_imdbtvdb_id(self, title, content_type, year="", imdbid="", tvshowtitle=""): '''try to figure out the imdbnumber and/or tvdbid''' tvdbid = "" if content_type in ["seasons", "episodes"] or tvshowtitle: title = tvshowtitle content_type = "tvshows" if imdbid and not imdbid.startswith("tt"): if content_type in ["tvshows", "seasons", "episodes"]: tvdbid = imdbid imdbid = "" if not imdbid and year: imdbid = self.get_omdb_info("", title, year, content_type).get("imdbnumber", "") if not imdbid: # repeat without year imdbid = self.get_omdb_info("", title, "", content_type).get("imdbnumber", "") # return results return (imdbid, tvdbid) def translate_string(self, _str): '''translate the received english string from the various sources like tvdb, tmbd etc''' translation = _str _str = _str.lower() if "continuing" in _str: translation = self.addon.getLocalizedString(32037) elif "ended" in _str: translation = self.addon.getLocalizedString(32038) elif "released" in _str: translation = self.addon.getLocalizedString(32040) return translation
class MainModule: '''mainmodule provides the script methods for the skinhelper addon''' def __init__(self): '''Initialization and main code run''' self.win = xbmcgui.Window(10000) self.addon = xbmcaddon.Addon(ADDON_ID) self.kodidb = KodiDb() self.cache = SimpleCache() self.params = self.get_params() log_msg("MainModule called with parameters: %s" % self.params) action = self.params.get("action", "") # launch module for action provided by this script try: getattr(self, action)() except AttributeError: log_exception(__name__, "No such action: %s" % action) except Exception as exc: log_exception(__name__, exc) finally: xbmc.executebuiltin("dialog.Close(busydialog)") # do cleanup self.close() def close(self): '''Cleanup Kodi Cpython instances on exit''' self.cache.close() del self.win del self.addon del self.kodidb log_msg("MainModule exited") @classmethod def get_params(self): '''extract the params from the called script path''' params = {} for arg in sys.argv[1:]: paramname = arg.split('=')[0] paramvalue = arg.replace(paramname + "=", "") paramname = paramname.lower() if paramname == "action": paramvalue = paramvalue.lower() params[paramname] = paramvalue return params def deprecated_method(self, newaddon): ''' used when one of the deprecated methods is called print warning in log and call the external script with the same parameters ''' action = self.params.get("action") log_msg("Deprecated method: %s. Please call %s directly" % (action, newaddon), xbmc.LOGWARNING) paramstring = "" for key, value in self.params.iteritems(): paramstring += ",%s=%s" % (key, value) if xbmc.getCondVisibility("System.HasAddon(%s)" % newaddon): xbmc.executebuiltin("RunAddon(%s%s)" % (newaddon, paramstring)) else: # trigger install of the addon if KODI_VERSION > 16: xbmc.executebuiltin("InstallAddon(%s)" % newaddon) else: xbmc.executebuiltin("RunPlugin(plugin://%s)" % newaddon) @staticmethod def musicsearch(): '''helper to go directly to music search dialog''' xbmc.executebuiltin("ActivateWindow(Music)") xbmc.executebuiltin("SendClick(8)") def setview(self): '''sets the selected viewmode for the container''' xbmc.executebuiltin("ActivateWindow(busydialog)") content_type = get_current_content_type() if not content_type: content_type = "files" current_view = xbmc.getInfoLabel("Container.Viewmode").decode("utf-8") view_id, view_label = self.selectview(content_type, current_view) current_forced_view = xbmc.getInfoLabel("Skin.String(SkinHelper.ForcedViews.%s)" % content_type) if view_id is not None: # also store forced view if (content_type and current_forced_view and current_forced_view != "None" and xbmc.getCondVisibility("Skin.HasSetting(SkinHelper.ForcedViews.Enabled)")): xbmc.executebuiltin("Skin.SetString(SkinHelper.ForcedViews.%s,%s)" % (content_type, view_id)) xbmc.executebuiltin("Skin.SetString(SkinHelper.ForcedViews.%s.label,%s)" % (content_type, view_label)) self.win.setProperty("SkinHelper.ForcedView", view_id) if not xbmc.getCondVisibility("Control.HasFocus(%s)" % current_forced_view): xbmc.sleep(100) xbmc.executebuiltin("Container.SetViewMode(%s)" % view_id) xbmc.executebuiltin("SetFocus(%s)" % view_id) else: self.win.clearProperty("SkinHelper.ForcedView") # set view xbmc.executebuiltin("Container.SetViewMode(%s)" % view_id) def selectview(self, content_type="other", current_view=None, display_none=False): '''reads skinfile with all views to present a dialog to choose from''' cur_view_select_id = None label = "" all_views = [] if display_none: listitem = xbmcgui.ListItem(label="None") listitem.setProperty("id", "None") all_views.append(listitem) # read the special skin views file views_file = xbmc.translatePath('special://skin/extras/views.xml').decode("utf-8") if xbmcvfs.exists(views_file): doc = parse(views_file) listing = doc.documentElement.getElementsByTagName('view') itemcount = 0 for view in listing: label = xbmc.getLocalizedString(int(view.attributes['languageid'].nodeValue)) viewid = view.attributes['value'].nodeValue mediatypes = view.attributes['type'].nodeValue.lower().split(",") if label.lower() == current_view.lower() or viewid == current_view: cur_view_select_id = itemcount if display_none: cur_view_select_id += 1 if (("all" in mediatypes or content_type.lower() in mediatypes) and (not "!" + content_type.lower() in mediatypes) and not xbmc.getCondVisibility("Skin.HasSetting(SkinHelper.view.Disabled.%s)" % viewid)): image = "special://skin/extras/viewthumbs/%s.jpg" % viewid listitem = xbmcgui.ListItem(label=label, iconImage=image) listitem.setProperty("viewid", viewid) listitem.setProperty("icon", image) all_views.append(listitem) itemcount += 1 dialog = DialogSelect("DialogSelect.xml", "", listing=all_views, windowtitle=self.addon.getLocalizedString(32012), richlayout=True) dialog.autofocus_id = cur_view_select_id dialog.doModal() result = dialog.result del dialog if result: viewid = result.getProperty("viewid") label = result.getLabel().decode("utf-8") return (viewid, label) else: return (None, None) # pylint: disable-msg=too-many-local-variables def enableviews(self): '''show select dialog to enable/disable views''' all_views = [] views_file = xbmc.translatePath('special://skin/extras/views.xml').decode("utf-8") richlayout = self.params.get("richlayout", "") == "true" if xbmcvfs.exists(views_file): doc = parse(views_file) listing = doc.documentElement.getElementsByTagName('view') for view in listing: view_id = view.attributes['value'].nodeValue label = xbmc.getLocalizedString(int(view.attributes['languageid'].nodeValue)) desc = label + " (" + str(view_id) + ")" image = "special://skin/extras/viewthumbs/%s.jpg" % view_id listitem = xbmcgui.ListItem(label=label, label2=desc, iconImage=image) listitem.setProperty("viewid", view_id) if not xbmc.getCondVisibility("Skin.HasSetting(SkinHelper.view.Disabled.%s)" % view_id): listitem.select(selected=True) excludefromdisable = False try: excludefromdisable = view.attributes['excludefromdisable'].nodeValue == "true" except Exception: pass if not excludefromdisable: all_views.append(listitem) dialog = DialogSelect( "DialogSelect.xml", "", listing=all_views, windowtitle=self.addon.getLocalizedString(32013), multiselect=True, richlayout=richlayout) dialog.doModal() result = dialog.result del dialog if result: for item in result: view_id = item.getProperty("viewid") if item.isSelected(): # view is enabled xbmc.executebuiltin("Skin.Reset(SkinHelper.view.Disabled.%s)" % view_id) else: # view is disabled xbmc.executebuiltin("Skin.SetBool(SkinHelper.view.Disabled.%s)" % view_id) # pylint: enable-msg=too-many-local-variables def setforcedview(self): '''helper that sets a forced view for a specific content type''' content_type = self.params.get("contenttype") if content_type: current_view = xbmc.getInfoLabel("Skin.String(SkinHelper.ForcedViews.%s)" % content_type) if not current_view: current_view = "0" view_id, view_label = self.selectview(content_type, current_view, True) if view_id or view_label: xbmc.executebuiltin("Skin.SetString(SkinHelper.ForcedViews.%s,%s)" % (content_type, view_id)) xbmc.executebuiltin("Skin.SetString(SkinHelper.ForcedViews.%s.label,%s)" % (content_type, view_label)) @staticmethod def get_youtube_listing(searchquery): '''get items from youtube plugin by query''' lib_path = u"plugin://plugin.video.youtube/kodion/search/query/?q=%s" % searchquery return KodiDb().files(lib_path) def searchyoutube(self): '''helper to search youtube for the given title''' xbmc.executebuiltin("ActivateWindow(busydialog)") title = self.params.get("title", "") window_header = self.params.get("header", "") results = [] for media in self.get_youtube_listing(title): if not media["filetype"] == "directory": label = media["label"] label2 = media["plot"] image = "" if media.get('art'): if media['art'].get('thumb'): image = (media['art']['thumb']) listitem = xbmcgui.ListItem(label=label, label2=label2, iconImage=image) listitem.setProperty("path", media["file"]) results.append(listitem) # finished lookup - display listing with results xbmc.executebuiltin("dialog.Close(busydialog)") dialog = DialogSelect("DialogSelect.xml", "", listing=results, windowtitle=window_header, multiselect=False, richlayout=True) dialog.doModal() result = dialog.result del dialog if result: if xbmc.getCondVisibility( "Window.IsActive(script-skin_helper_service-CustomInfo.xml) | " "Window.IsActive(movieinformation)"): xbmc.executebuiltin("Dialog.Close(movieinformation)") xbmc.executebuiltin("Dialog.Close(script-skin_helper_service-CustomInfo.xml)") xbmc.sleep(1000) xbmc.executebuiltin('PlayMedia("%s")' % result.getProperty("path")) del result def getcastmedia(self): '''helper to show a dialog with all media for a specific actor''' xbmc.executebuiltin("ActivateWindow(busydialog)") name = self.params.get("name", "") window_header = self.params.get("name", "") results = [] items = self.kodidb.castmedia(name) items = process_method_on_list(self.kodidb.prepare_listitem, items) for item in items: if item["file"].startswith("videodb://"): item["file"] = "ActivateWindow(Videos,%s,return)" % item["file"] else: item["file"] = 'PlayMedia("%s")' % item["file"] results.append(self.kodidb.create_listitem(item, False)) # finished lookup - display listing with results xbmc.executebuiltin("dialog.Close(busydialog)") dialog = DialogSelect("DialogSelect.xml", "", listing=results, windowtitle=window_header, richlayout=True) dialog.doModal() result = dialog.result del dialog if result: while xbmc.getCondVisibility("System.HasModalDialog"): xbmc.executebuiltin("Action(Back)") xbmc.sleep(300) xbmc.executebuiltin(result.getfilename()) del result def setfocus(self): '''helper to set focus on a list or control''' control = self.params.get("control") fallback = self.params.get("fallback") position = self.params.get("position", "0") relativeposition = self.params.get("relativeposition") if relativeposition: position = int(relativeposition) - 1 count = 0 if control: while not xbmc.getCondVisibility("Control.HasFocus(%s)" % control): if xbmc.getCondVisibility("Window.IsActive(busydialog)"): xbmc.sleep(150) continue elif count == 20 or (xbmc.getCondVisibility( "!Control.IsVisible(%s) | " "!IntegerGreaterThan(Container(%s).NumItems,0)" % (control, control))): if fallback: xbmc.executebuiltin("Control.SetFocus(%s)" % fallback) break else: xbmc.executebuiltin("Control.SetFocus(%s,%s)" % (control, position)) xbmc.sleep(50) count += 1 def setwidgetcontainer(self): '''helper that reports the current selected widget container/control''' controls = self.params.get("controls", "").split("-") if controls: xbmc.sleep(50) for i in range(10): for control in controls: if xbmc.getCondVisibility("Control.IsVisible(%s) + IntegerGreaterThan(Container(%s).NumItems,0)" % (control, control)): self.win.setProperty("SkinHelper.WidgetContainer", control) return xbmc.sleep(50) self.win.clearProperty("SkinHelper.WidgetContainer") def saveskinimage(self): '''let the user select an image and save it to addon_data for easy backup''' skinstring = self.params.get("skinstring", "") allow_multi = self.params.get("multi", "") == "true" header = self.params.get("header", "") value = SkinSettings().save_skin_image(skinstring, allow_multi, header) if value: xbmc.executebuiltin("Skin.SetString(%s,%s)" % (skinstring.encode("utf-8"), value.encode("utf-8"))) @staticmethod def checkskinsettings(): '''performs check of all default skin settings and labels''' SkinSettings().correct_skin_settings() def setskinsetting(self): '''allows the user to set a skin setting with a select dialog''' setting = self.params.get("setting", "") org_id = self.params.get("id", "") if "$" in org_id: org_id = xbmc.getInfoLabel(org_id).decode("utf-8") header = self.params.get("header", "") SkinSettings().set_skin_setting(setting=setting, window_header=header, original_id=org_id) def setskinconstant(self): '''allows the user to set a skin constant with a select dialog''' setting = self.params.get("setting", "") value = self.params.get("value", "") header = self.params.get("header", "") SkinSettings().set_skin_constant(setting, header, value) def setskinconstants(self): '''allows the skinner to set multiple skin constants''' settings = self.params.get("settings", "").split("|") values = self.params.get("values", "").split("|") SkinSettings().set_skin_constants(settings, values) def setskinshortcutsproperty(self): '''allows the user to make a setting for skinshortcuts using the special skinsettings dialogs''' setting = self.params.get("setting", "") prop = self.params.get("property", "") header = self.params.get("header", "") SkinSettings().set_skinshortcuts_property(setting, header, prop) def togglekodisetting(self): '''toggle kodi setting''' settingname = self.params.get("setting", "") cur_value = xbmc.getCondVisibility("system.getbool(%s)" % settingname) if cur_value: new_value = "false" else: new_value = "true" xbmc.executeJSONRPC( '{"jsonrpc":"2.0", "id":1, "method":"Settings.SetSettingValue","params":{"setting":"%s","value":%s}}' % (settingname, new_value)) def setkodisetting(self): '''set kodi setting''' settingname = self.params.get("setting", "") value = self.params.get("value", "") is_int = False try: valueint = int(value) is_int = True del valueint except Exception: pass if value.lower() == "true": value = 'true' elif value.lower() == "false": value = 'false' elif is_int: value = '"%s"' % value xbmc.executeJSONRPC('{"jsonrpc":"2.0", "id":1, "method":"Settings.SetSettingValue",\ "params":{"setting":"%s","value":%s}}' % (settingname, value)) def playtrailer(self): '''auto play windowed trailer inside video listing''' if not xbmc.getCondVisibility("Player.HasMedia | Container.Scrolling | Container.OnNext | " "Container.OnPrevious | !IsEmpty(Window(Home).Property(traileractionbusy))"): self.win.setProperty("traileractionbusy", "traileractionbusy") widget_container = self.params.get("widgetcontainer", "") trailer_mode = self.params.get("mode", "").replace("auto_", "") allow_youtube = self.params.get("youtube", "") == "true" if not trailer_mode: trailer_mode = "windowed" if widget_container: widget_container_prefix = "Container(%s)." % widget_container else: widget_container_prefix = "" li_title = xbmc.getInfoLabel("%sListItem.Title" % widget_container_prefix).decode('utf-8') li_trailer = xbmc.getInfoLabel("%sListItem.Trailer" % widget_container_prefix).decode('utf-8') if not li_trailer and allow_youtube: youtube_result = self.get_youtube_listing("%s Trailer" % li_title) if youtube_result: li_trailer = youtube_result[0].get("file") # always wait a bit to prevent trailer start playing when we're scrolling the list xbmc.Monitor().waitForAbort(3) if li_trailer and (li_title == xbmc.getInfoLabel("%sListItem.Title" % widget_container_prefix).decode('utf-8')): if trailer_mode == "fullscreen" and li_trailer: xbmc.executebuiltin('PlayMedia("%s")' % li_trailer) else: xbmc.executebuiltin('PlayMedia("%s",1)' % li_trailer) self.win.setProperty("TrailerPlaying", trailer_mode) self.win.clearProperty("traileractionbusy") def colorpicker(self): '''legacy''' self.deprecated_method("script.skin.helper.colorpicker") def backup(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def restore(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def reset(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def colorthemes(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def createcolortheme(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def restorecolortheme(self): '''legacy''' self.deprecated_method("script.skin.helper.skinbackup") def conditionalbackgrounds(self): '''legacy''' self.deprecated_method("script.skin.helper.backgrounds") def splashscreen(self): '''helper to show a user defined splashscreen in the skin''' import time splashfile = self.params.get("file", "") duration = int(self.params.get("duration", 5)) if (splashfile.lower().endswith("jpg") or splashfile.lower().endswith("gif") or splashfile.lower().endswith("png") or splashfile.lower().endswith("tiff")): # this is an image file self.win.setProperty("SkinHelper.SplashScreen", splashfile) # for images we just wait for X seconds to close the splash again start_time = time.time() while (time.time() - start_time) <= duration: xbmc.sleep(500) else: # for video or audio we have to wait for the player to finish... xbmc.Player().play(splashfile, windowed=True) xbmc.sleep(500) while xbmc.getCondVisibility("Player.HasMedia"): xbmc.sleep(150) # replace startup window with home startupwindow = xbmc.getInfoLabel("System.StartupWindow") xbmc.executebuiltin("ReplaceWindow(%s)" % startupwindow) autostart_playlist = xbmc.getInfoLabel("$ESCINFO[Skin.String(autostart_playlist)]") if autostart_playlist: xbmc.executebuiltin("PlayMedia(%s)" % autostart_playlist) def videosearch(self): '''show the special search dialog''' xbmc.executebuiltin("ActivateWindow(busydialog)") from resources.lib.searchdialog import SearchDialog search_dialog = SearchDialog("script-skin_helper_service-CustomSearch.xml", self.addon.getAddonInfo('path').decode("utf-8"), "Default", "1080i") search_dialog.doModal() del search_dialog def showinfo(self): '''shows our special videoinfo dialog''' dbid = self.params.get("dbid", "") dbtype = self.params.get("dbtype", "") from infodialog import show_infodialog show_infodialog(dbid, dbtype) def deletedir(self): '''helper to delete a directory, input can be normal filesystem path or vfs''' del_path = self.params.get("path") if del_path: ret = xbmcgui.Dialog().yesno(heading=xbmc.getLocalizedString(122), line1=u"%s[CR]%s" % (xbmc.getLocalizedString(125), del_path)) if ret: success = recursive_delete_dir(del_path) if success: xbmcgui.Dialog().ok(heading=xbmc.getLocalizedString(19179), line1=self.addon.getLocalizedString(32014)) else: xbmcgui.Dialog().ok(heading=xbmc.getLocalizedString(16205), line1=xbmc.getLocalizedString(32015)) def overlaytexture(self): '''legacy: helper to let the user choose a background overlay from a skin defined folder''' skinstring = self.params.get("skinstring", "BackgroundOverlayTexture") self.params["skinstring"] = skinstring self.params["resourceaddon"] = "resource.images.backgroundoverlays" self.params["customfolder"] = "special://skin/extras/bgoverlays/" self.params["allowmulti"] = "false" self.params["header"] = self.addon.getLocalizedString(32002) self.selectimage() def busytexture(self): '''legacy: helper which lets the user select a busy spinner from predefined spinners in the skin''' skinstring = self.params.get("skinstring", "SkinHelper.SpinnerTexture") self.params["skinstring"] = skinstring self.params["resourceaddon"] = "resource.images.busyspinners" self.params["customfolder"] = "special://skin/extras/busy_spinners/" self.params["allowmulti"] = "true" self.params["header"] = self.addon.getLocalizedString(32006) self.selectimage() def selectimage(self): '''helper which lets the user select an image or imagepath from resourceaddons or custom path''' skinsettings = SkinSettings() skinstring = self.params.get("skinstring", "") skinshortcutsprop = self.params.get("skinshortcutsproperty", "") current_value = self.params.get("currentvalue", "") resource_addon = self.params.get("resourceaddon", "") allow_multi = self.params.get("allowmulti", "false") == "true" windowheader = self.params.get("header", "") skinhelper_backgrounds = self.params.get("skinhelperbackgrounds", "false") == "true" label, value = skinsettings.select_image( skinstring, allow_multi=allow_multi, windowheader=windowheader, resource_addon=resource_addon, skinhelper_backgrounds=skinhelper_backgrounds, current_value=current_value) if label: if skinshortcutsprop: # write value to skinshortcuts prop from skinshortcuts import set_skinshortcuts_property set_skinshortcuts_property(skinshortcutsprop, value, label) else: # write the values to skin strings if value.startswith("$INFO"): # we got an dynamic image from window property skinsettings.set_skin_variable(skinstring, value) value = "$VAR[%s]" % skinstring skinstring = skinstring.encode("utf-8") label = label.encode("utf-8") xbmc.executebuiltin("Skin.SetString(%s.label,%s)" % (skinstring, label)) xbmc.executebuiltin("Skin.SetString(%s.name,%s)" % (skinstring, label)) xbmc.executebuiltin("Skin.SetString(%s,%s)" % (skinstring, value)) xbmc.executebuiltin("Skin.SetString(%s.path,%s)" % (skinstring, value)) del skinsettings def dialogok(self): '''helper to show an OK dialog with a message''' headertxt = self.params.get("header") bodytxt = self.params.get("message") if bodytxt.startswith(" "): bodytxt = bodytxt[1:] if headertxt.startswith(" "): headertxt = headertxt[1:] dialog = xbmcgui.Dialog() dialog.ok(heading=headertxt, line1=bodytxt) del dialog def dialogyesno(self): '''helper to show a YES/NO dialog with a message''' headertxt = self.params.get("header") bodytxt = self.params.get("message") yesactions = self.params.get("yesaction", "").split("|") noactions = self.params.get("noaction", "").split("|") if bodytxt.startswith(" "): bodytxt = bodytxt[1:] if headertxt.startswith(" "): headertxt = headertxt[1:] if xbmcgui.Dialog().yesno(heading=headertxt, line1=bodytxt): for action in yesactions: xbmc.executebuiltin(action.encode("utf-8")) else: for action in noactions: xbmc.executebuiltin(action.encode("utf-8")) def textviewer(self): '''helper to show a textviewer dialog with a message''' headertxt = self.params.get("header", "") bodytxt = self.params.get("message", "") if bodytxt.startswith(" "): bodytxt = bodytxt[1:] if headertxt.startswith(" "): headertxt = headertxt[1:] xbmcgui.Dialog().textviewer(headertxt, bodytxt) def fileexists(self): '''helper to let the skinner check if a file exists and write the outcome to a window prop or skinstring''' filename = self.params.get("file") skinstring = self.params.get("skinstring") windowprop = self.params.get("winprop") if xbmcvfs.exists(filename): if windowprop: self.win.setProperty(windowprop, "exists") if skinstring: xbmc.executebuiltin("Skin.SetString(%s,exists)" % skinstring) else: if windowprop: self.win.clearProperty(windowprop) if skinstring: xbmc.executebuiltin("Skin.Reset(%s)" % skinstring) def stripstring(self): '''helper to allow the skinner to strip a string and write results to a skin string''' splitchar = self.params.get("splitchar") if splitchar.upper() == "[SPACE]": splitchar = " " skinstring = self.params.get("string") if not skinstring: skinstring = self.params.get("skinstring") output = self.params.get("output") index = self.params.get("index", 0) skinstring = skinstring.split(splitchar)[int(index)] self.win.setProperty(output, skinstring) def getfilename(self, filename=""): '''helper to display a sanitized filename in the vidoeinfo dialog''' output = self.params.get("output") if not filename: filename = xbmc.getInfoLabel("ListItem.FileNameAndPath") if not filename: filename = xbmc.getInfoLabel("ListItem.FileName") if "filename=" in filename: url_params = dict(urlparse.parse_qsl(filename)) filename = url_params.get("filename") self.win.setProperty(output, filename) def getplayerfilename(self): '''helper to parse the filename from a plugin (e.g. emby) filename''' filename = xbmc.getInfoLabel("Player.FileNameAndPath") if not filename: filename = xbmc.getInfoLabel("Player.FileName") self.getfilename(filename) def getpercentage(self): '''helper to calculate the percentage of 2 numbers and write results to a skinstring''' total = int(params.get("total")) count = int(params.get("count")) roundsteps = self.params.get("roundsteps") skinstring = self.params.get("skinstring") percentage = int(round((1.0 * count / total) * 100)) if roundsteps: roundsteps = int(roundsteps) percentage = percentage + (roundsteps - percentage) % roundsteps xbmc.executebuiltin("Skin.SetString(%s,%s)" % (skinstring, percentage)) def setresourceaddon(self): '''helper to let the user choose a resource addon and set that as skin string''' from resourceaddons import setresourceaddon addontype = self.params.get("addontype", "") skinstring = self.params.get("skinstring", "") setresourceaddon(addontype, skinstring) def checkresourceaddons(self): '''allow the skinner to perform a basic check if some required resource addons are available''' from resourceaddons import checkresourceaddons addonslist = self.params.get("addonslist", []) if addonslist: addonslist = addonslist.split("|") checkresourceaddons(addonslist)
class PluginContent: '''Hidden plugin entry point providing some helper features''' params = {} win = None def __init__(self): self.cache = SimpleCache() self.mutils = MetadataUtils() self.win = xbmcgui.Window(10000) try: self.params = dict( urlparse.parse_qsl(sys.argv[2].replace( '?', '').lower().decode("utf-8"))) log_msg("plugin called with parameters: %s" % self.params) self.main() except Exception as exc: log_exception(__name__, exc) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) # cleanup when done processing self.close() def close(self): '''Cleanup Kodi Cpython instances''' self.cache.close() self.mutils.close() del self.mutils del self.win def main(self): '''main action, load correct function''' action = self.params.get("action", "") if self.win.getProperty("SkinHelperShutdownRequested"): # do not proceed if kodi wants to exit log_msg( "%s --> Not forfilling request: Kodi is exiting" % __name__, xbmc.LOGWARNING) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) else: try: if hasattr(self.__class__, action): # launch module for action provided by this plugin getattr(self, action)() else: # legacy (widget) path called !!! self.load_widget() except Exception as exc: log_exception(__name__, exc) def load_widget(self): '''legacy entrypoint called (widgets are moved to seperate addon), start redirect...''' action = self.params.get("action", "") newaddon = "script.skin.helper.widgets" log_msg( "Deprecated method: %s. Please reassign your widgets to get rid of this message. -" "This automatic redirect will be removed in the future" % (action), xbmc.LOGWARNING) paramstring = "" for key, value in self.params.iteritems(): paramstring += ",%s=%s" % (key, value) if getCondVisibility("System.HasAddon(%s)" % newaddon): # TEMP !!! for backwards compatability reasons only - to be removed in the near future!! import imp addon = xbmcaddon.Addon(newaddon) addon_path = addon.getAddonInfo('path').decode("utf-8") imp.load_source('plugin', os.path.join(addon_path, "plugin.py")) from plugin import main main.Main() del addon else: # trigger install of the addon if KODI_VERSION > 16: xbmc.executebuiltin("InstallAddon(%s)" % newaddon) else: xbmc.executebuiltin("RunPlugin(plugin://%s)" % newaddon) def playchannel(self): '''play channel from widget helper''' params = {"item": {"channelid": int(self.params["channelid"])}} self.mutils.kodidb.set_json("Player.Open", params) def playrecording(self): '''retrieve the recording and play to get resume working''' recording = self.mutils.kodidb.recording(self.params["recordingid"]) params = {"item": {"recordingid": recording["recordingid"]}} self.mutils.kodidb.set_json("Player.Open", params) # manually seek because passing resume to the player json cmd doesn't seem to work if recording["resume"].get("position"): for i in range(50): if getCondVisibility("Player.HasVideo"): break xbmc.sleep(50) xbmc.Player().seekTime(recording["resume"].get("position")) def launch(self): '''launch any builtin action using a plugin listitem''' if "runscript" in self.params["path"]: self.params["path"] = self.params["path"].replace("?", ",") xbmc.executebuiltin(self.params["path"]) def playalbum(self): '''helper to play an entire album''' xbmc.executeJSONRPC( '{ "jsonrpc": "2.0", "method": "Player.Open", "params": { "item": { "albumid": %d } }, "id": 1 }' % int(self.params["albumid"])) def smartshortcuts(self): '''called from skinshortcuts to retrieve listing of all smart shortcuts''' import skinshortcuts skinshortcuts.get_smartshortcuts(self.params.get("path", "")) @staticmethod def backgrounds(): '''called from skinshortcuts to retrieve listing of all backgrounds''' import skinshortcuts skinshortcuts.get_backgrounds() def widgets(self): '''called from skinshortcuts to retrieve listing of all widgetss''' import skinshortcuts skinshortcuts.get_widgets(self.params.get("path", ""), self.params.get("sublevel", "")) def resourceimages(self): '''retrieve listing of specific resource addon images''' from resourceaddons import get_resourceimages addontype = self.params.get("addontype", "") for item in get_resourceimages(addontype, True): listitem = xbmcgui.ListItem(item[0], label2=item[2], path=item[1], iconImage=item[3]) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item[1], listitem=listitem, isFolder=False) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def extrafanart(self): '''helper to display extrafanart in multiimage control in the skin''' fanarts = eval(self.params["fanarts"]) # process extrafanarts for count, item in enumerate(fanarts): listitem = xbmcgui.ListItem("fanart%s" % count, path=item) listitem.setProperty('mimetype', 'image/jpeg') xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item, listitem=listitem) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def extraposter(self): '''helper to display extraposter in multiimage control in the skin''' posters = eval(self.params["posters"]) # process extraposters for count, item in enumerate(posters): listitem = xbmcgui.ListItem("poster%s" % count, path=item) listitem.setProperty('mimetype', 'image/jpeg') xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=item, listitem=listitem) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def genrebackground(self): '''helper to display images for a specific genre in multiimage control in the skin''' genre = self.params.get("genre").split(".")[0] arttype = self.params.get("arttype", "fanart") randomize = self.params.get("random", "false") == "true" mediatype = self.params.get("mediatype", "movies") if genre and genre != "..": filters = [{"operator": "is", "field": "genre", "value": genre}] if randomize: sort = {"method": "random", "order": "descending"} else: sort = {"method": "sorttitle", "order": "ascending"} items = getattr(self.mutils.kodidb, mediatype)(sort=sort, filters=filters, limits=(0, 50)) for item in items: image = self.mutils.get_clean_image(item["art"].get( arttype, "")) if image: image = self.mutils.get_clean_image(item["art"][arttype]) listitem = xbmcgui.ListItem(image, path=image) listitem.setProperty('mimetype', 'image/jpeg') xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=image, listitem=listitem) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def getcastmedia(self): '''helper to display get all media for a specific actor''' name = self.params.get("name") if name: all_items = self.mutils.kodidb.castmedia(name) all_items = self.mutils.process_method_on_list( self.mutils.kodidb.prepare_listitem, all_items) all_items = self.mutils.process_method_on_list( self.mutils.kodidb.create_listitem, all_items) xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items)) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def getcast(self): '''helper to get all cast for a given media item''' db_id = None all_cast = [] all_cast_names = list() cache_str = "" download_thumbs = self.params.get("downloadthumbs", "") == "true" extended_cast_action = self.params.get("castaction", "") == "extendedinfo" movie = self.params.get("movie") tvshow = self.params.get("tvshow") episode = self.params.get("episode") movieset = self.params.get("movieset") try: # try to parse db_id if movieset: cache_str = "movieset.castcache-%s-%s" % ( self.params["movieset"], download_thumbs) db_id = int(movieset) elif tvshow: cache_str = "tvshow.castcache-%s-%s" % (self.params["tvshow"], download_thumbs) db_id = int(tvshow) elif movie: cache_str = "movie.castcache-%s-%s" % (self.params["movie"], download_thumbs) db_id = int(movie) elif episode: cache_str = "episode.castcache-%s-%s" % ( self.params["episode"], download_thumbs) db_id = int(episode) except Exception: pass cachedata = self.cache.get(cache_str) if cachedata: # get data from cache all_cast = cachedata else: # retrieve data from json api... if movie and db_id: all_cast = self.mutils.kodidb.movie(db_id)["cast"] elif movie and not db_id: filters = [{ "operator": "is", "field": "title", "value": movie }] result = self.mutils.kodidb.movies(filters=filters) all_cast = result[0]["cast"] if result else [] elif tvshow and db_id: all_cast = self.mutils.kodidb.tvshow(db_id)["cast"] elif tvshow and not db_id: filters = [{ "operator": "is", "field": "title", "value": tvshow }] result = self.mutils.kodidb.tvshows(filters=filters) all_cast = result[0]["cast"] if result else [] elif episode and db_id: all_cast = self.mutils.kodidb.episode(db_id)["cast"] elif episode and not db_id: filters = [{ "operator": "is", "field": "title", "value": episode }] result = self.mutils.kodidb.episodes(filters=filters) all_cast = result[0]["cast"] if result else [] elif movieset: if not db_id: for item in self.mutils.kodidb.moviesets(): if item["title"].lower() == movieset.lower(): db_id = item["setid"] if db_id: json_result = self.mutils.kodidb.movieset( db_id, include_set_movies_fields=["cast"]) if "movies" in json_result: for movie in json_result['movies']: all_cast += movie['cast'] # optional: download missing actor thumbs if all_cast and download_thumbs: for cast in all_cast: if cast.get("thumbnail"): cast["thumbnail"] = self.mutils.get_clean_image( cast.get("thumbnail")) if not cast.get("thumbnail"): artwork = self.mutils.tmdb.get_actor(cast["name"]) cast["thumbnail"] = artwork.get("thumb", "") # lookup tmdb if item is requested that is not in local db if not all_cast: tmdbdetails = {} if movie and not db_id: tmdbdetails = self.mutils.tmdb.search_movie(movie) elif tvshow and not db_id: tmdbdetails = self.mutils.tmdb.search_tvshow(tvshow) if tmdbdetails.get("cast"): all_cast = tmdbdetails["cast"] # save to cache self.cache.set(cache_str, all_cast) # process listing with the results... for cast in all_cast: if cast.get("name") not in all_cast_names: liz = xbmcgui.ListItem(label=cast.get("name"), label2=cast.get("role"), iconImage=cast.get("thumbnail")) if extended_cast_action: url = "RunScript(script.extendedinfo,info=extendedactorinfo,name=%s)" % cast.get( "name") url = "plugin://script.skin.helper.service/?action=launch&path=%s" % url is_folder = False else: url = "RunScript(script.skin.helper.service,action=getcastmedia,name=%s)" % cast.get( "name") url = "plugin://script.skin.helper.service/?action=launch&path=%s" % urlencode( url) is_folder = False all_cast_names.append(cast.get("name")) liz.setThumbnailImage(cast.get("thumbnail")) xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=url, listitem=liz, isFolder=is_folder) xbmcplugin.endOfDirectory(int(sys.argv[1])) @staticmethod def alphabet(): '''display an alphabet scrollbar in listings''' all_letters = [] if xbmc.getInfoLabel("Container.NumItems"): for i in range(int(xbmc.getInfoLabel("Container.NumItems"))): all_letters.append( xbmc.getInfoLabel("Listitem(%s).SortLetter" % i).upper()) start_number = "" for number in ["2", "3", "4", "5", "6", "7", "8", "9"]: if number in all_letters: start_number = number break for letter in [ start_number, "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ]: if letter == start_number: label = "#" else: label = letter listitem = xbmcgui.ListItem(label=label) if letter not in all_letters: lipath = "noop" listitem.setProperty("NotAvailable", "true") else: lipath = "plugin://script.skin.helper.service/?action=alphabetletter&letter=%s" % letter xbmcplugin.addDirectoryItem(int(sys.argv[1]), lipath, listitem, isFolder=False) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) def alphabetletter(self): '''used with the alphabet scrollbar to jump to a letter''' if KODI_VERSION > 16: xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=False, listitem=xbmcgui.ListItem()) letter = self.params.get("letter", "").upper() jumpcmd = "" if letter in ["A", "B", "C", "2"]: jumpcmd = "2" elif letter in ["D", "E", "F", "3"]: jumpcmd = "3" elif letter in ["G", "H", "I", "4"]: jumpcmd = "4" elif letter in ["J", "K", "L", "5"]: jumpcmd = "5" elif letter in ["M", "N", "O", "6"]: jumpcmd = "6" elif letter in ["P", "Q", "R", "S", "7"]: jumpcmd = "7" elif letter in ["T", "U", "V", "8"]: jumpcmd = "8" elif letter in ["W", "X", "Y", "Z", "9"]: jumpcmd = "9" if jumpcmd: xbmc.executebuiltin("SetFocus(50)") for i in range(40): xbmc.executeJSONRPC( '{ "jsonrpc": "2.0", "method": "Input.ExecuteAction",\ "params": { "action": "jumpsms%s" }, "id": 1 }' % (jumpcmd)) xbmc.sleep(50) if xbmc.getInfoLabel("ListItem.Sortletter").upper() == letter: break
class MetadataUtils(object): ''' Provides all kind of mediainfo for kodi media, returned as dict with details ''' close_called = False def __init__(self): '''Initialize and load all our helpers''' self._studiologos_path = "" self.cache = SimpleCache() self.addon = xbmcaddon.Addon(ADDON_ID) self.kodidb = KodiDb() self.omdb = Omdb(self.cache) self.tmdb = Tmdb(self.cache) self.channellogos = ChannelLogos(self.kodidb) self.fanarttv = FanartTv(self.cache) self.imdb = Imdb(self.cache) self.google = GoogleImages(self.cache) self.studiologos = StudioLogos(self.cache) self.animatedart = AnimatedArt(self.cache, self.kodidb) self.thetvdb = TheTvDb() self.musicart = MusicArtwork(self) self.pvrart = PvrArtwork(self) log_msg("Initialized") def close(self): '''Cleanup instances''' self.close_called = True self.cache.close() self.addon = None del self.addon del self.kodidb del self.omdb del self.tmdb del self.channellogos del self.fanarttv del self.imdb del self.google del self.studiologos del self.animatedart del self.thetvdb del self.musicart del self.pvrart log_msg("Exited") def __del__(self): '''make sure close is called''' if not self.close_called: self.close() @use_cache(14) def get_extrafanart(self, file_path): '''helper to retrieve the extrafanart path for a kodi media item''' from helpers.extrafanart import get_extrafanart return get_extrafanart(file_path) def get_music_artwork(self, artist, album="", track="", disc="", ignore_cache=False, flush_cache=False): '''method to get music artwork for the goven artist/album/song''' return self.musicart.get_music_artwork( artist, album, track, disc, ignore_cache=ignore_cache, flush_cache=flush_cache) def music_artwork_options(self, artist, album="", track="", disc=""): '''options for music metadata for specific item''' return self.musicart.music_artwork_options(artist, album, track, disc) @use_cache(7) def get_extended_artwork(self, imdb_id="", tvdb_id="", tmdb_id="", media_type=""): '''get extended artwork for the given imdbid or tvdbid''' result = None if "movie" in media_type and tmdb_id: result = self.fanarttv.movie(tmdb_id) elif "movie" in media_type and imdb_id: result = self.fanarttv.movie(imdb_id) elif media_type in ["tvshow", "tvshows", "seasons", "episodes"]: if not tvdb_id: if imdb_id and not imdb_id.startswith("tt"): tvdb_id = imdb_id elif imdb_id: tvdb_id = self.thetvdb.get_series_by_imdb_id(imdb_id).get("tvdb_id") if tvdb_id: result = self.fanarttv.tvshow(tvdb_id) # add additional art with special path if result: result = {"art": result} for arttype in ["fanarts", "posters", "clearlogos", "banners"]: if result["art"].get(arttype): result["art"][arttype] = "plugin://script.skin.helper.service/"\ "?action=extrafanart&fanarts=%s" % quote_plus(repr(result["art"][arttype])) return result def get_tmdb_details(self, imdb_id="", tvdb_id="", title="", year="", media_type="", preftype="", manual_select=False, ignore_cache=False): '''returns details from tmdb''' result = {} if imdb_id: result = self.tmdb.get_videodetails_by_externalid( imdb_id, "imdb_id") elif tvdb_id: result = self.tmdb.get_videodetails_by_externalid( tvdb_id, "tvdb_id") elif title and media_type in ["movies", "setmovies", "movie"]: result = self.tmdb.search_movie( title, year, manual_select=manual_select) elif title and media_type in ["tvshows", "tvshow"]: result = self.tmdb.search_tvshow( title, year, manual_select=manual_select) elif title: result = self.tmdb.search_video( title, year, preftype=preftype, manual_select=manual_select) if result and result.get("status"): result["status"] = self.translate_string(result["status"]) if result and result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result def get_moviesetdetails(self, title, set_id): '''get a nicely formatted dict of the movieset details which we can for example set as window props''' # get details from tmdb from helpers.moviesetdetails import get_moviesetdetails return get_moviesetdetails(self, title, set_id) @use_cache(14) def get_streamdetails(self, db_id, media_type, ignore_cache=False): '''get a nicely formatted dict of the streamdetails ''' from helpers.streamdetails import get_streamdetails return get_streamdetails(self.kodidb, db_id, media_type) def get_pvr_artwork(self, title, channel="", genre="", manual_select=False, ignore_cache=False): '''get artwork and mediadetails for PVR entries''' return self.pvrart.get_pvr_artwork( title, channel, genre, manual_select=manual_select, ignore_cache=ignore_cache) def pvr_artwork_options(self, title, channel="", genre=""): '''options for pvr metadata for specific item''' return self.pvrart.pvr_artwork_options(title, channel, genre) def get_channellogo(self, channelname): '''get channellogo for the given channel name''' return self.channellogos.get_channellogo(channelname) def get_studio_logo(self, studio): '''get studio logo for the given studio''' # dont use cache at this level because of changing logospath return self.studiologos.get_studio_logo(studio, self.studiologos_path) @property def studiologos_path(self): '''path to use to lookup studio logos, must be set by the calling addon''' return self._studiologos_path @studiologos_path.setter def studiologos_path(self, value): '''path to use to lookup studio logos, must be set by the calling addon''' self._studiologos_path = value def get_animated_artwork(self, imdb_id, manual_select=False, ignore_cache=False): '''get animated artwork, perform extra check if local version still exists''' artwork = self.animatedart.get_animated_artwork( imdb_id, manual_select=manual_select, ignore_cache=ignore_cache) if not (manual_select or ignore_cache): refresh_needed = False if artwork.get("animatedposter") and not xbmcvfs.exists( artwork["animatedposter"]): refresh_needed = True if artwork.get("animatedfanart") and not xbmcvfs.exists( artwork["animatedfanart"]): refresh_needed = True return {"art": artwork} def get_omdb_info(self, imdb_id="", title="", year="", content_type=""): '''Get (kodi compatible formatted) metadata from OMDB, including Rotten tomatoes details''' title = title.split(" (")[0] # strip year appended to title result = {} if imdb_id: result = self.omdb.get_details_by_imdbid(imdb_id) elif title and content_type in ["seasons", "season", "episodes", "episode", "tvshows", "tvshow"]: result = self.omdb.get_details_by_title(title, "", "tvshows") elif title and year: result = self.omdb.get_details_by_title(title, year, content_type) if result and result.get("status"): result["status"] = self.translate_string(result["status"]) if result and result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(self.get_duration(result["runtime"])) return result def get_top250_rating(self, imdb_id): '''get the position in the IMDB top250 for the given IMDB ID''' return self.imdb.get_top250_rating(imdb_id) @use_cache(14) def get_duration(self, duration): '''helper to get a formatted duration''' if isinstance(duration, (str, unicode)) and ":" in duration: dur_lst = duration.split(":") return { "Duration": "%s:%s" % (dur_lst[0], dur_lst[1]), "Duration.Hours": dur_lst[0], "Duration.Minutes": dur_lst[1], "Runtime": str((int(dur_lst[0]) * 60) + int(dur_lst[1])), } else: return _get_duration(duration) @use_cache(2) def get_tvdb_details(self, imdbid="", tvdbid=""): '''get metadata from tvdb by providing a tvdbid or tmdbid''' result = {} self.thetvdb.days_ahead = 365 if not tvdbid and imdbid and not imdbid.startswith("tt"): # assume imdbid is actually a tvdbid... tvdbid = imdbid if tvdbid: result = self.thetvdb.get_series(tvdbid) elif imdbid: result = self.thetvdb.get_series_by_imdb_id(imdbid) if result: if result["status"] == "Continuing": # include next episode info result["nextepisode"] = self.thetvdb.get_nextaired_episode(result["tvdb_id"]) # include last episode info result["lastepisode"] = self.thetvdb.get_last_episode_for_series(result["tvdb_id"]) result["status"] = self.translate_string(result["status"]) if result.get("runtime"): result["runtime"] = result["runtime"] / 60 result.update(_get_duration(result["runtime"])) return result @use_cache(90) def get_imdbtvdb_id(self, title, content_type, year="", imdbid="", tvshowtitle=""): '''try to figure out the imdbnumber and/or tvdbid''' tvdbid = "" if content_type in ["seasons", "episodes"] or tvshowtitle: title = tvshowtitle content_type = "tvshows" if imdbid and not imdbid.startswith("tt"): if content_type in ["tvshows", "seasons", "episodes"]: tvdbid = imdbid imdbid = "" if not imdbid and year: imdbid = self.get_omdb_info( "", title, year, content_type).get("imdbnumber", "") if not imdbid: # repeat without year imdbid = self.get_omdb_info("", title, "", content_type).get("imdbnumber", "") # return results return (imdbid, tvdbid) def translate_string(self, _str): '''translate the received english string from the various sources like tvdb, tmbd etc''' translation = _str _str = _str.lower() if "continuing" in _str: translation = self.addon.getLocalizedString(32037) elif "ended" in _str: translation = self.addon.getLocalizedString(32038) elif "released" in _str: translation = self.addon.getLocalizedString(32040) return translation
class TheTvDb(object): '''Our main class''' _win = None _addon = None _token = None cache = None api_key = 'A7613F5C1482A540' # default api key days_ahead = 120 ignore_cache = False _close_called = False def __init__(self, api_key=None): '''Initialize our Module''' if api_key: self.api_key = api_key self.cache = SimpleCache() self._win = xbmcgui.Window(10000) self._addon = xbmcaddon.Addon(ADDON_ID) addonversion = self._addon.getAddonInfo('version').decode("utf-8") self.cache.global_checksum = "%s%s" % (addonversion, KODI_LANGUAGE) self._log_msg("Initialized") def close(self): '''Cleanup Kodi cpython classes''' self._close_called = True self.cache.close() del self._win del self._addon self._log_msg("Exited") def __del__(self): '''make sure close is called''' if not self._close_called: self.close() def get_data(self, endpoint, prefer_localized=False): '''grab the results from the api''' data = {} url = 'https://api.thetvdb.com/' + endpoint headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'User-agent': 'Mozilla/5.0', 'Authorization': 'Bearer %s' % self._get_token()} if prefer_localized: headers["Accept-Language"] = KODI_LANGUAGE try: response = requests.get(url, headers=headers, timeout=20) if response and response.content and response.status_code == 200: data = json.loads(response.content.decode('utf-8', 'replace')) elif response.status_code == 401: # token expired, refresh it and repeat our request self._log_msg("Token expired, refreshing...") headers['Bearer'] = self._get_token(True) response = requests.get(url, headers=headers, timeout=5) if response and response.content and response.status_code == 200: data = json.loads(response.content.decode('utf-8', 'replace')) if data.get("data"): data = data["data"] except Exception as exc: self._log_msg("Exception in get_data --> %s" % repr(exc), xbmc.LOGERROR) return data @use_cache(60) def get_series_posters(self, seriesid, season=None): '''retrieves the URL for the series poster, prefer season poster if season number provided''' if season: images = self.get_data("series/%s/images/query?keyType=season&subKey=%s" % (seriesid, season)) else: images = self.get_data("series/%s/images/query?keyType=poster" % (seriesid)) return self.process_images(images) @use_cache(60) def get_series_fanarts(self, seriesid, landscape=False): '''retrieves the URL for the series fanart image''' if landscape: images = self.get_data("series/%s/images/query?keyType=fanart&subKey=text" % (seriesid)) else: images = self.get_data("series/%s/images/query?keyType=fanart&subKey=graphical" % (seriesid)) return self.process_images(images) @staticmethod def process_images(images): '''helper to sort and correct the images as the api output is rather messy''' result = [] if images: for image in images: if image["fileName"] and not image["fileName"].endswith("/"): if image["fileName"].startswith("http://"): image["fileName"] = image["fileName"].replace("http://", "https://") elif not image["fileName"].startswith("https://"): image["fileName"] = "https://thetvdb.com/banners/" + image["fileName"] image_score = image["ratingsInfo"]["average"] * image["ratingsInfo"]["count"] image["score"] = image_score result.append(image) return [item["fileName"] for item in sorted(result, key=itemgetter("score"), reverse=True)] @use_cache(7) def get_episode(self, episodeid, seriesdetails=None): ''' Returns the full information for a given episode id. Usage: specify the episode ID: TheTvDb().get_episode(episodeid) ''' episode = self.get_data("episodes/%s" % episodeid, True) # we prefer localized content but if that fails, fallback to default if episode and not episode.get("overview"): episode = self.get_data("episodes/%s" % episodeid) if episode: if not seriesdetails and "seriesid" in episode: seriesdetails = self.get_series(episode["seriesid"]) elif not seriesdetails and "seriesId" in episode: seriesdetails = self.get_series(episode["seriesId"]) episode = self._map_episode_data(episode, seriesdetails) return episode @use_cache(14) def get_series(self, seriesid): ''' Returns a series record that contains all information known about a particular series id. Usage: specify the serie ID: TheTvDb().get_series(seriesid) ''' seriesinfo = self.get_data("series/%s" % seriesid, True) # we prefer localized content but if that fails, fallback to default if not seriesinfo.get("overview"): seriesinfo = self.get_data("series/%s" % seriesid) return self._map_series_data(seriesinfo) @use_cache(7) def get_series_by_imdb_id(self, imdbid=""): '''get full series details by providing an imdbid''' items = self.get_data("search/series?imdbId=%s" % imdbid) if items: return self.get_series(items[0]["id"]) else: return {} @use_cache(3) def get_continuing_series(self): ''' only gets the continuing series, based on which series were recently updated as there is no other api call to get that information ''' recent_series = self.get_recently_updated_series() continuing_series = [] for recent_serie in recent_series: seriesinfo = self.get_series(recent_serie["id"]) if seriesinfo and seriesinfo.get("status", "") == "Continuing": continuing_series.append(seriesinfo) return continuing_series @use_cache(60) def get_series_actors(self, seriesid): ''' Returns actors for the given series id. Usage: specify the series ID: TheTvDb().get_series_actors(seriesid) ''' return self.get_data("series/%s/actors" % seriesid) @use_cache(30) def get_series_episodes(self, seriesid): ''' Returns all episodes for a given series. Usage: specify the series ID: TheTvDb().get_series_episodes(seriesid) Note: output is only summary of episode details (non kodi formatted) ''' all_episodes = [] page = 1 while True: # get all episodes by iterating over the pages data = self.get_data("series/%s/episodes?page=%s" % (seriesid, page)) if not data: break else: all_episodes += data page += 1 return all_episodes @use_cache(14) def get_last_season_for_series(self, seriesid): '''get the last season for the series''' highest_season = 0 summary = self.get_series_episodes_summary(seriesid) if summary: for season in summary["airedSeasons"]: if int(season) > highest_season: highest_season = int(season) return highest_season @use_cache(1) def get_last_episode_for_series(self, seriesid): ''' Returns the last aired episode for a given series Usage: specify the series ID: TheTvDb().get_last_episode_for_series(seriesid) ''' # somehow the absolutenumber is broken in the api so we have to get this info the hard way highest_season = self.get_last_season_for_series(seriesid) while not highest_season == -1: season_episodes = self.get_series_episodes_by_query(seriesid, "airedSeason=%s" % highest_season) season_episodes = sorted(season_episodes, key=lambda k: k.get('airedEpisodeNumber', 0), reverse=True) highest_eps = (arrow.get("1970-01-01").date(), 0) if season_episodes: for episode in season_episodes: if episode["firstAired"]: airdate = arrow.get(episode["firstAired"]).date() if (airdate < date.today()) and (airdate > highest_eps[0]): highest_eps = (airdate, episode["id"]) if highest_eps[1] != 0: return self.get_episode(highest_eps[1]) # go down one season untill we reach a match (there may be already announced seasons in the seasons list) highest_season -= 1 self._log_msg("No last episodes found for series %s" % seriesid) return None @use_cache(7) def get_series_episodes_by_query(self, seriesid, query=""): ''' This route allows the user to query against episodes for the given series. The response is an array of episode records that have been filtered down to basic information. Usage: specify the series ID: TheTvDb().get_series_episodes_by_query(seriesid) You must specify one or more fields for the query (combine multiple with &): absolutenumber=X --> Absolute number of the episode airedseason=X --> Aired season number airedepisode=X --> Aired episode number dvdseason=X --> DVD season number dvdepisode=X --> DVD episode number imdbid=X --> IMDB id of the series Note: output is only summary of episode details (non kodi formatted) ''' all_episodes = [] page = 1 while True: # get all episodes by iterating over the pages data = self.get_data("series/%s/episodes/query?%s&page=%s" % (seriesid, query, page)) if not data: break else: all_episodes += data page += 1 return all_episodes @use_cache(7) def get_series_episodes_summary(self, seriesid): ''' Returns a summary of the episodes and seasons available for the series. Note: Season 0 is for all episodes that are considered to be specials. Usage: specify the series ID: TheTvDb().get_series_episodes_summary(seriesid) ''' return self.get_data("series/%s/episodes/summary" % (seriesid)) @use_cache(8) def search_series(self, query="", prefer_localized=False): ''' Allows the user to search for a series based the name. Returns an array of results that match the query. Usage: specify the query: TheTvDb().search_series(searchphrase) Available parameter: prefer_localized --> True if you want to set the current kodi language as preferred in the results ''' return self.get_data("search/series?name=%s" % query, prefer_localized) @use_cache(7) def get_recently_updated_series(self): ''' Returns all series that have been updated in the last week ''' day = 24 * 60 * 60 utc_date = date.today() - timedelta(days=7) cur_epoch = (utc_date.toordinal() - date(1970, 1, 1).toordinal()) * day return self.get_data("updated/query?fromTime=%s" % cur_epoch) def get_unaired_episodes(self, seriesid): ''' Returns the unaired episodes for the specified seriesid Usage: specify the series ID: TheTvDb().get_unaired_episodes(seriesid) ''' next_episodes = [] seriesinfo = self.get_series(seriesid) if seriesinfo and seriesinfo.get("status", "") == "Continuing": highest_season = self.get_last_season_for_series(seriesid) episodes = self.get_series_episodes_by_query(seriesid, "airedSeason=%s" % highest_season) episodes = sorted(episodes, key=lambda k: k.get('airedEpisodeNumber', 0)) for episode in episodes: if episode["firstAired"] and episode["episodeName"]: airdate = arrow.get(episode["firstAired"]).date() if airdate >= date.today() and (airdate <= (date.today() + timedelta(days=self.days_ahead))): # if airdate is today or (max X days) in the future add to our list episode = self.get_episode(episode["id"], seriesinfo) if episode: # apparently some episode id's are reported that do not exist next_episodes.append(episode) # return our list sorted by episode return sorted(next_episodes, key=lambda k: k.get('episode', "")) def get_nextaired_episode(self, seriesid): ''' Returns the first next airing episode for the specified seriesid Usage: specify the series ID: TheTvDb().get_nextaired_episode(seriesid) ''' next_episodes = self.get_unaired_episodes(seriesid) if next_episodes: return next_episodes[0] else: return None def get_unaired_episode_list(self, seriesids): ''' Returns the next airing episode for each specified seriesid Usage: specify the series ID: TheTvDb().get_unaired_episode_list(list of seriesids) ''' next_episodes = [] for seriesid in seriesids: episodes = self.get_unaired_episodes(seriesid) if episodes and episodes[0] is not None: next_episodes.append(episodes[0]) # return our list sorted by date return sorted(next_episodes, key=lambda k: k.get('firstAired', "")) def get_kodishows(self, continuing_only=False): ''' get all tvshows in the kodi library and make sure we have a valid tvdb id returns combined tvshow details ''' kodi_series = self._get_kodi_json('VideoLibrary.GetTvShows', '{"properties": [ %s ] }' % KODI_TV_PROPS) all_series = [] monitor = xbmc.Monitor() if kodi_series and kodi_series.get("tvshows"): for kodi_serie in kodi_series["tvshows"]: if monitor.abortRequested() or self._close_called: break tvdb_details = self._parse_kodi_show(kodi_serie) if tvdb_details and "tvdb_status" in tvdb_details: if not continuing_only or (continuing_only and tvdb_details["tvdb_status"] == "Continuing"): all_series.append(tvdb_details) del monitor return all_series def get_kodishows_details(self, continuing_only=False): ''' returns full info for each tvshow in the kodi library returns both kodi and tvdb info combined, including next-/last episode ''' result = [] monitor = xbmc.Monitor() for series_info in self.get_kodishows(continuing_only=continuing_only): if monitor.abortRequested() or self._close_called: break details = self.get_kodishow_details(series_info["title"], serie_details=series_info) if details: result.append(series_info) del monitor return result def get_kodishows_airingtoday(self): ''' returns full info for each tvshow in the kodi library that airs today ''' result = [] monitor = xbmc.Monitor() for series_info in self.get_kodishows(continuing_only=True): if monitor.abortRequested() or self._close_called: break details = self.get_kodishow_details(series_info["title"], serie_details=series_info) if details and details.get("next_episode"): airdate = arrow.get(details["next_episode"]["firstaired"]).date() if airdate == date.today(): result.append(series_info) del monitor return sorted(result, key=lambda k: k.get('airtime', "")) @use_cache(2) def get_kodishow_details(self, title, serie_details=None): '''get full details for the kodi serie in library - search by title (as in kodi db)''' result = None if not serie_details and title: # get kodi details by title filter_str = '"filter": {"operator": "is", "field": "title", "value": "%s"}' % title kodi_series = self._get_kodi_json('VideoLibrary.GetTvShows', '{"properties": [ %s ], %s }' % (KODI_TV_PROPS, filter_str)) if kodi_series and kodi_series.get("tvshows"): kodi_details = kodi_series["tvshows"][0] serie_details = self._parse_kodi_show(kodi_details) if serie_details: # append next airing episode details serie_details["next_episode"] = self.get_nextaired_episode(serie_details["tvdb_id"]) # append last episode details serie_details["last_episode"] = self.get_last_episode_for_series(serie_details["tvdb_id"]) result = serie_details return result def get_kodi_unaired_episodes(self, single_episode_per_show=True, include_last_episode=False, tvshows_ids=None): ''' Returns the next unaired episode for all continuing tv shows in the Kodi library single_episode_per_show: Only return a single episode (next unaired) for each show, defaults to True. include_last_episode: Also include the last aired episode in the listing for each show. ''' kodi_series = self.get_kodishows(True) next_episodes = [] if tvshows_ids: kodi_series = [ tvshow for tvshow in kodi_series if tvshow["tvshowid"] in tvshows_ids ] for kodi_serie in kodi_series: serieid = kodi_serie["tvdb_id"] if single_episode_per_show: episodes = [self.get_nextaired_episode(serieid)] else: episodes = self.get_unaired_episodes(serieid) if include_last_episode: episodes.append(self.get_last_episode_for_series(serieid)) for next_episode in episodes: if next_episode: # make the json output kodi compatible next_episodes.append(self._map_kodi_episode_data(kodi_serie, next_episode)) # return our list sorted by date return sorted(next_episodes, key=lambda k: k.get('firstaired', "")) def _map_episode_data(self, episode_details, seriesdetails=None): '''maps full episode data from tvdb to kodi compatible format''' result = {} result["art"] = {} if episode_details.get("filename"): result["art"]["thumb"] = "https://thetvdb.com/banners/" + episode_details["filename"] result["thumbnail"] = result["art"]["thumb"] result["title"] = episode_details["episodeName"] result["label"] = "%sx%s. %s" % (episode_details["airedSeason"], episode_details["airedEpisodeNumber"], episode_details["episodeName"]) result["season"] = episode_details["airedSeason"] result["episode"] = episode_details["airedEpisodeNumber"] result["firstaired"] = episode_details["firstAired"] result["writer"] = episode_details["writers"] result["director"] = episode_details["directors"] result["gueststars"] = episode_details["guestStars"] result["rating"] = episode_details["siteRating"] # make sure we have a decimal in the rating if len(str(result["rating"])) == 1: result["rating"] = "%s.0" % result["rating"] result["plot"] = episode_details["overview"] result["airdate"] = self._get_local_date(episode_details["firstAired"]) result["airdate.long"] = self._get_local_date(episode_details["firstAired"], True) result["airdate.label"] = "%s (%s)" % (result["label"], result["airdate"]) result["seriesid"] = episode_details["seriesId"] # append seriesinfo to details if provided if seriesdetails: result["tvshowtitle"] = seriesdetails["title"] result["showtitle"] = seriesdetails["title"] result["network"] = seriesdetails["network"] result["studio"] = seriesdetails["studio"] result["genre"] = seriesdetails["genre"] result["classification"] = seriesdetails["classification"] result["tvshow.firstaired"] = seriesdetails["firstaired"] result["tvshow.status"] = seriesdetails["status"] result["airtime"] = seriesdetails["airtime"] result["airday"] = seriesdetails["airday"] result["airday.int"] = seriesdetails["airday.int"] result["airdatetime"] = "%s %s" % (result["airdate"], result["airtime"]) result["airdatetime.label"] = "%s - %s %s" % (result["airdatetime"], xbmc.getLocalizedString(145), result["network"]) result["art"]["tvshow.poster"] = seriesdetails["art"].get("poster", "") result["art"]["tvshow.landscape"] = seriesdetails["art"].get("landscape", "") result["art"]["tvshow.fanart"] = seriesdetails["art"].get("fanart", "") result["art"]["tvshow.banner"] = seriesdetails["art"].get("banner", "") try: result["runtime"] = int(seriesdetails["runtime"]) * 60 except Exception: pass season_posters = self.get_series_posters(episode_details["seriesId"], episode_details["airedSeason"]) if season_posters: result["art"]["season.poster"] = season_posters[0] result["library"] = seriesdetails.get("library", "") result["file"] = seriesdetails.get("file", "") result["year"] = seriesdetails.get("year", "") return result def _map_kodi_episode_data(self, kodi_tvshow_details, episode_details): '''combine kodi tvshow details with tvdb episode details''' result = episode_details # add images from kodi series details for key, value in kodi_tvshow_details["art"].items(): result["art"]["tvshow.%s" % key] = self._get_clean_image(value) result["art"]["season.poster"] = episode_details.get("season.poster", "") result["tvshowtitle"] = kodi_tvshow_details["title"] result["showtitle"] = kodi_tvshow_details["title"] result["studio"] = kodi_tvshow_details["studio"] result["genre"] = kodi_tvshow_details["genre"] result["cast"] = kodi_tvshow_details["cast"] result["kodi_tvshowid"] = kodi_tvshow_details["tvshowid"] result["episodeid"] = -1 result["file"] = "videodb://tvshows/titles/%s/" % kodi_tvshow_details["tvshowid"] result["type"] = "episode" result["DBTYPE"] = "episode" result["isFolder"] = True return result def _map_series_data(self, showdetails): '''maps the tvdb data to more kodi compatible format''' result = {} if showdetails: result["title"] = showdetails["seriesName"] result["status"] = showdetails["status"] result["tvdb_status"] = showdetails["status"] result["tvdb_id"] = showdetails["id"] result["network"] = showdetails["network"] result["studio"] = [showdetails["network"]] local_airday, local_airday_short, airday_int = self._get_local_weekday(showdetails["airsDayOfWeek"]) result["airday"] = local_airday result["airday.short"] = local_airday_short result["airday.int"] = airday_int result["airtime"] = self._get_local_time(showdetails["airsTime"]) result["airdaytime"] = "%s %s (%s)" % (result["airday"], result["airtime"], result["network"]) result["airdaytime.short"] = "%s %s" % (result["airday.short"], result["airtime"]) result["airdaytime.label"] = "%s %s - %s %s" % (result["airday"], result["airtime"], xbmc.getLocalizedString(145), result["network"]) result["airdaytime.label.short"] = "%s %s - %s %s" % ( result["airday.short"], result["airtime"], xbmc.getLocalizedString(145), result["network"]) result["votes"] = showdetails["siteRatingCount"] result["rating.tvdb"] = showdetails["siteRating"] # make sure we have a decimal in the rating if len(str(result["rating.tvdb"])) == 1: result["rating.tvdb"] = "%s.0" % result["rating.tvdb"] result["rating"] = result["rating.tvdb"] result["votes.tvdb"] = showdetails["siteRatingCount"] try: result["runtime"] = int(showdetails["runtime"]) * 60 except Exception: pass result["plot"] = showdetails["overview"] result["genre"] = showdetails["genre"] classification = CLASSIFICATION_REGEX.search("/".join(showdetails["genre"])) if isinstance(showdetails["genre"], list) else None result["classification"] = classification.group(1) if classification else 'Scripted' result["firstaired"] = showdetails["firstAired"] result["imdbnumber"] = showdetails["imdbId"] # artwork result["art"] = {} fanarts = self.get_series_fanarts(showdetails["id"]) if fanarts: result["art"]["fanart"] = fanarts[0] result["art"]["fanarts"] = fanarts landscapes = self.get_series_fanarts(showdetails["id"], True) if landscapes: result["art"]["landscapes"] = landscapes result["art"]["landscape"] = landscapes[0] posters = self.get_series_posters(showdetails["id"]) if posters: result["art"]["posters"] = posters result["art"]["poster"] = posters[0] if showdetails.get("banner"): result["art"]["banner"] = "https://thetvdb.com/banners/" + showdetails["banner"] return result def _parse_kodi_show(self, kodi_details): ''' get tvdb series details by providing kodi showdetails''' tvdb_details = None result = None if kodi_details["imdbnumber"] and kodi_details["imdbnumber"].startswith("tt"): # lookup serie by imdbid tvdb_details = self.get_series_by_imdb_id(kodi_details["imdbnumber"]) elif kodi_details["imdbnumber"]: # assume imdbid in kodidb is already tvdb id tvdb_details = self.get_series(kodi_details["imdbnumber"]) if not tvdb_details and "uniqueid" in kodi_details: # kodi 17+ uses the uniqueid field to store the imdb/tvdb number for value in kodi_details["uniqueid"]: if value.startswith("tt"): tvdb_details = self.get_series_by_imdb_id(value) elif value: tvdb_details = self.get_series(value) if tvdb_details: break if not tvdb_details: # lookup series by name as fallback tvdb_search = self.search_series(kodi_details["title"]) if tvdb_search: tvdb_details = self.get_series(tvdb_search[0]["id"]) if tvdb_details: # combine kodi tvshow details with tvdb series details result = kodi_details # append images from tvdb details for key, value in tvdb_details["art"].items(): if value and not result["art"].get(key): result["art"][key] = value # combine info from both dicts for key, value in tvdb_details.items(): if value and not result.get(key): result[key] = value result["isFolder"] = True result["tvdb_status"] = tvdb_details["status"] result["library"] = "videodb://tvshows/titles/%s/" % kodi_details["tvshowid"] return result @staticmethod def _log_msg(msg, level=xbmc.LOGDEBUG): '''logger to kodi log''' if isinstance(msg, unicode): msg = msg.encode("utf-8") xbmc.log('{0} --> {1}'.format(ADDON_ID, msg), level=level) @staticmethod def _get_clean_image(image): '''helper to strip all kodi tags/formatting of an image path/url''' if not image or isinstance(image, list): return image if image and "image://" in image: image = image.replace("image://", "") image = urllib.unquote(image.encode("utf-8")) if image.endswith("/"): image = image[:-1] if not isinstance(image, unicode): image = image.decode("utf8") return image def _get_token(self, refresh=False): '''get jwt token for api''' # get token from memory cache first if self._token and not refresh: return self._token token = self._win.getProperty("script.module.thetvdb.token").decode('utf-8') if token and not refresh: return token # refresh previous token prev_token = self._addon.getSetting("token") if prev_token: url = 'https://api.thetvdb.com/refresh_token' headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'User-agent': 'Mozilla/5.0', 'Authorization': 'Bearer %s' % prev_token} response = requests.get(url, headers=headers) if response and response.content and response.status_code == 200: data = json.loads(response.content.decode('utf-8', 'replace')) token = data["token"] if token: self._win.setProperty("script.module.thetvdb.token", token) self._token = token return token # do first login to get initial token url = 'https://api.thetvdb.com/login' payload = {'apikey': self.api_key} headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'User-agent': 'Mozilla/5.0'} response = requests.post(url, data=json.dumps(payload), headers=headers) if response and response.content and response.status_code == 200: data = json.loads(response.content.decode('utf-8', 'replace')) token = data["token"] self._addon.setSetting("token", token) self._win.setProperty("script.module.thetvdb.token", token) self._token = token return token else: self._log_msg("Error getting JWT token!", xbmc.LOGWARNING) return None @staticmethod def _get_kodi_json(method, params): '''helper to get data from the kodi json database''' json_response = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method" : "%s", "params": %s, "id":1 }' % (method, params.encode("utf-8"))) jsonobject = json.loads(json_response.decode('utf-8', 'replace')) if 'result' in jsonobject: jsonobject = jsonobject['result'] return jsonobject def _get_local_time(self, timestr): '''returns the correct localized representation of the time provided by the api''' result = "" try: if timestr and ":" in timestr: timestr = timestr.replace(".", ":") if "H" in xbmc.getRegion('time'): time_format = "HH:mm" else: time_format = "h:mm A" if " AM" in timestr or " PM" in timestr: result = arrow.get(timestr, 'h:mm A').format(time_format, locale=KODI_LANGUAGE) elif " am" in timestr or " pm" in timestr: result = arrow.get(timestr, 'h:mm a').format(time_format, locale=KODI_LANGUAGE) elif "AM" in timestr or "PM" in timestr: result = arrow.get(timestr, 'h:mmA').format(time_format, locale=KODI_LANGUAGE) elif "am" in timestr or "pm" in timestr: result = arrow.get(timestr, 'h:mma').format(time_format, locale=KODI_LANGUAGE) elif len(timestr.split(":")[0]) == 1: result = arrow.get(timestr, 'h:mm').format(time_format, locale=KODI_LANGUAGE) else: result = arrow.get(timestr, 'HH:mm').format(time_format, locale=KODI_LANGUAGE) except Exception as exc: self._log_msg(str(exc), xbmc.LOGWARNING) return timestr return result def _get_local_date(self, datestr, long_date=False): '''returns the localized representation of the date provided by the api''' result = "" try: if long_date: result = arrow.get(datestr).strftime(xbmc.getRegion('datelong')) else: result = arrow.get(datestr).strftime(xbmc.getRegion('dateshort')) except Exception as exc: self._log_msg("Exception in _get_local_date: %s" % exc) return result def _get_local_weekday(self, weekday): '''returns the localized representation of the weekday provided by the api''' if not weekday: return ("", "", 0) day_name = weekday day_name_short = day_name[:3] day_int = 0 try: locale = arrow.locales.get_locale(KODI_LANGUAGE) day_names = {"monday": 1, "tuesday": 2, "wednesday": 3, "thurday": 4, "friday": 5, "saturday": 6, "sunday": 7} day_int = day_names.get(weekday.lower(), 0) if day_int: day_name = locale.day_name(day_int).capitalize() day_name_short = locale.day_abbreviation(day_int).capitalize() except Exception as exc: self._log_msg(str(exc)) return (day_name, day_name_short, day_int)
class Main(object): '''Main entry path for our widget listing. Process the arguments and load correct class and module''' def __init__(self): ''' Initialization ''' self.artutils = ArtUtils() self.cache = SimpleCache() self.addon = xbmcaddon.Addon(ADDON_ID) self.win = xbmcgui.Window(10000) self.options = self.get_options() # skip if shutdown requested if self.win.getProperty("SkinHelperShutdownRequested"): log_msg("Not forfilling request: Kodi is exiting!", xbmc.LOGWARNING) xbmcplugin.endOfDirectory(handle=ADDON_HANDLE) return if not "mediatype" in self.options or not "action" in self.options: # we need both mediatype and action, so show the main listing self.mainlisting() else: # we have a mediatype and action so display the widget listing self.show_widget_listing() self.close() def close(self): '''Cleanup Kodi Cpython instances''' self.artutils.close() self.cache.close() del self.addon del self.win log_msg("MainModule exited") def get_options(self): '''get the options provided to the plugin path''' options = dict( urlparse.parse_qsl(sys.argv[2].replace( '?', '').lower().decode("utf-8"))) if not "mediatype" in options and "action" in options: # get the mediatype and action from the path (for backwards compatability with old style paths) for item in [("movies", "movies"), ("shows", "tvshows"), ("episode", "episodes"), ("musicvideos", "musicvideos"), ("pvr", "pvr"), ("albums", "albums"), ("songs", "songs"), ("artists", "artists"), ("media", "media"), ("favourites", "favourites")]: if item[0] in options["action"]: options["mediatype"] = item[1] options["action"] = options["action"].replace( item[1], "").replace(item[0], "") break # prefer reload param for the mediatype if "mediatype" in options: alt_reload = self.win.getProperty("widgetreload-%s" % options["mediatype"]) if options["mediatype"] == "favourites" or "favourite" in options[ "action"]: options["skipcache"] = "true" elif alt_reload: options["reload"] = alt_reload if not options.get( "action") and options["mediatype"] == "favourites": options["action"] = "favourites" elif not options.get("action"): options["action"] = "listing" if "listing" in options["action"]: options["skipcache"] = "true" # set the widget settings as options options["hide_watched"] = self.addon.getSetting( "hide_watched") == "true" if self.addon.getSetting( "hide_watched_recent") == "true" and "recent" in options.get( "action", ""): options["hide_watched"] = True options["next_inprogress_only"] = self.addon.getSetting( "nextup_inprogressonly") == "true" options["episodes_enable_specials"] = self.addon.getSetting( "episodes_enable_specials") == "true" options["group_episodes"] = self.addon.getSetting( "episodes_grouping") == "true" if "limit" in options: options["limit"] = int(options["limit"]) else: options["limit"] = int(self.addon.getSetting("default_limit")) return options def show_widget_listing(self): '''display the listing for the provided action and mediatype''' media_type = self.options["mediatype"] action = self.options["action"] # set widget content type xbmcplugin.setContent(ADDON_HANDLE, media_type) # try to get from cache first... # we use a checksum based on the options to make sure the cache is ignored when needed all_items = [] cache_str = "SkinHelper.Widgets.%s.%s" % (media_type, action) if not self.win.getProperty("widgetreload2"): # at startup we simply accept whatever is in the cache cache_checksum = None else: cache_checksum = "" for key in sorted(self.options): cache_checksum += "%s.%s" % (key, self.options[key]) cache = self.cache.get(cache_str, checksum=cache_checksum) if cache and not self.options.get("skipcache") == "true": log_msg( "MEDIATYPE: %s - ACTION: %s -- got items from cache - CHECKSUM: %s" % (media_type, action, cache_checksum)) all_items = cache # Call the correct method to get the content from json when no cache if not all_items: log_msg( "MEDIATYPE: %s - ACTION: %s -- no cache, quering kodi api to get items - CHECKSUM: %s" % (media_type, action, cache_checksum)) # dynamically import and load the correct module, class and function try: media_module = __import__(media_type) media_class = getattr(media_module, media_type.capitalize())(self.addon, self.artutils, self.options) all_items = getattr(media_class, action)() del media_class except AttributeError: log_exception(__name__, "Incorrect widget action or type called") except Exception as exc: log_exception(__name__, exc) # randomize output if requested by skinner or user if self.options.get("randomize", "") == "true": all_items = sorted(all_items, key=lambda k: random.random()) # prepare listitems and store in cache all_items = process_method_on_list( self.artutils.kodidb.prepare_listitem, all_items) self.cache.set(cache_str, all_items, checksum=cache_checksum) # fill that listing... xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED) all_items = process_method_on_list( self.artutils.kodidb.create_listitem, all_items) xbmcplugin.addDirectoryItems(ADDON_HANDLE, all_items, len(all_items)) # end directory listing xbmcplugin.endOfDirectory(handle=ADDON_HANDLE) def mainlisting(self): '''main listing''' all_items = [] # movie node if xbmc.getCondVisibility("Library.HasContent(movies)"): all_items.append((xbmc.getLocalizedString(342), "movieslisting", "DefaultMovies.png")) # tvshows and episodes nodes if xbmc.getCondVisibility("Library.HasContent(tvshows)"): all_items.append((xbmc.getLocalizedString(20343), "tvshowslisting", "DefaultTvShows.png")) all_items.append((xbmc.getLocalizedString(20360), "episodeslisting", "DefaultTvShows.png")) # pvr node if xbmc.getCondVisibility("Pvr.HasTVChannels"): all_items.append((self.addon.getLocalizedString(32054), "pvrlisting", "DefaultAddonPVRClient.png")) # music nodes if xbmc.getCondVisibility("Library.HasContent(music)"): all_items.append((xbmc.getLocalizedString(132), "albumslisting", "DefaultAlbumCover.png")) all_items.append((xbmc.getLocalizedString(134), "songslisting", "DefaultMusicSongs.png")) all_items.append((xbmc.getLocalizedString(133), "artistslisting", "DefaultArtist.png")) # musicvideo node if xbmc.getCondVisibility("Library.HasContent(musicvideos)"): all_items.append( (xbmc.getLocalizedString(20389), "musicvideoslisting", "DefaultAddonAlbumInfo.png")) # media node if xbmc.getCondVisibility( "Library.HasContent(movies) | Library.HasContent(tvshows) | Library.HasContent(music)" ): all_items.append((self.addon.getLocalizedString(32057), "medialisting", "DefaultAddonAlbumInfo.png")) # favourites node all_items.append((xbmc.getLocalizedString(10134), "favouriteslisting", "DefaultAddonAlbumInfo.png")) # process the listitems and display listing all_items = process_method_on_list(create_main_entry, all_items) all_items = process_method_on_list( self.artutils.kodidb.prepare_listitem, all_items) all_items = process_method_on_list( self.artutils.kodidb.create_listitem, all_items) xbmcplugin.addDirectoryItems(ADDON_HANDLE, all_items, len(all_items)) xbmcplugin.endOfDirectory(handle=ADDON_HANDLE)
def alphabet(): if sys.argv[2] != "?action=alphabet&reload=0": #log_msg("alphabet function", xbmc.LOGINFO) NumItems = xbmc.getInfoLabel('Container.NumItems') cache = SimpleCache() mycache = cache.get(NumItems) if mycache: all_letters = mycache #log_msg("alphabet function: all_letters CACHE", xbmc.LOGINFO) else: all_letters = [] for i in range(int(NumItems)): all_letters.append( xbmc.getInfoLabel('Listitem(%s).SortLetter' % i).upper()) cache.set(NumItems, all_letters) #log_msg("alphabet function: all_letters", xbmc.LOGINFO) if len(all_letters) > 1: numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] alphabet = [ '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' ] first_number = False for item in numbers: if item in all_letters: first_number = item break for letter in alphabet: listitem = xbmcgui.ListItem(label=letter) if letter == '#' and first_number: lipath = "plugin://script.skin.helper.service/?action=alphabetletter&letter=%s" % first_number listitem.setProperty('IsNumber', first_number) xbmcplugin.addDirectoryItem(int(sys.argv[1]), lipath, listitem, isFolder=False) elif letter in all_letters: lipath = "plugin://script.skin.helper.service/?action=alphabetletter&letter=%s" % letter listitem.setProperty('IsNumber', letter) xbmcplugin.addDirectoryItem(int(sys.argv[1]), lipath, listitem, isFolder=False) elif letter not in all_letters: lipath = "" listitem.setProperty("NotAvailable", "true") xbmcplugin.addDirectoryItem(int(sys.argv[1]), lipath, listitem, isFolder=False) xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) cache.close()