#!/usr/bin/env python # -*- coding: utf-8 -*- """ Code from script.module.metadatautils, kodidb.py Loads of different functions called in SEPARATE Python instances through e.g. plugin://... calls. Hence be careful to only rely on window variables. """ from __future__ import absolute_import, division, unicode_literals from logging import getLogger try: from multiprocessing.pool import ThreadPool SUPPORTS_POOL = True except Exception: SUPPORTS_POOL = False import xbmc import xbmcgui import xbmcvfs from .plex_api import API from .plex_db import PlexDB from . import json_rpc as js, utils, variables as v LOG = getLogger('PLEX.widget') # To easily use threadpool, we can only pass one argument PLEX_TYPE = None SECTION_ID = None APPEND_SHOW_TITLE = None APPEND_SXXEXX = None # Need to chain the PMS keys KEY = None def get_listitem(xml_element): """ Returns a valid xbmcgui.ListItem() for xml_element """ item = generate_item(xml_element) prepare_listitem(item) return create_listitem(item) def process_method_on_list(method_to_run, items): """ helper method that processes a method on each listitem with pooling if the system supports it """ all_items = [] if SUPPORTS_POOL: pool = ThreadPool() try: all_items = pool.map(method_to_run, items) except Exception: # catch exception to prevent threadpool running forever utils.ERROR(notify=True) pool.close() pool.join() else: all_items = [method_to_run(item) for item in items] all_items = filter(None, all_items) return all_items def get_clean_image(image): ''' helper to strip all kodi tags/formatting of an image path/url Pass in either unicode or str; returns unicode ''' if not image: return "" if not isinstance(image, str): image = image.encode('utf-8') if b"music@" in image: # fix for embedded images thumbcache = xbmc.getCacheThumbName(image) thumbcache = thumbcache.replace(b".tbn", b".jpg") thumbcache = b"special://thumbnails/%s/%s" % (thumbcache[0], thumbcache) if not xbmcvfs.exists(thumbcache): xbmcvfs.copy(image, thumbcache) image = thumbcache if image and b"image://" in image: image = image.replace(b"image://", b"") image = utils.unquote(image) if image.endswith("/"): image = image[:-1] return image else: return image.decode('utf-8') def generate_item(xml_element): """ Meant to be consumed by metadatautils.kodidb.prepare_listitem(), and then subsequently by metadatautils.kodidb.create_listitem() Do NOT set resumetime - otherwise Kodi always resumes at that time even if the user chose to start element from the beginning listitem.setProperty('resumetime', str(userdata['Resume'])) The key 'file' needs to be set later with the item's path """ try: if xml_element.tag in ('Directory', 'Playlist', 'Hub'): return _generate_folder(xml_element) else: return _generate_content(xml_element) except Exception: # Usefull to catch everything here since we're using threadpool LOG.error('xml that caused the crash: "%s": %s', xml_element.tag, xml_element.attrib) utils.ERROR(notify=True) def _generate_folder(xml_element): '''Generates "folder"/"directory" items that user can further navigate''' api = API(xml_element) art = api.artwork() return { 'title': api.title(), 'label': api.title(), 'file': api.directory_path(section_id=SECTION_ID, plex_type=PLEX_TYPE, old_key=KEY), 'icon': 'DefaultFolder.png', 'art': { 'thumb': art['thumb'] if 'thumb' in art else (art['poster'] if 'poster' in art else 'special://home/addons/%s/icon.png' % v.ADDON_ID), 'fanart': art['fanart'] if 'fanart' in art else 'special://home/addons/%s/fanart.jpg' % v.ADDON_ID}, 'isFolder': True, 'type': '', 'IsPlayable': 'false', } def _generate_content(xml_element): api = API(xml_element) plex_type = api.plex_type() kodi_type = v.KODITYPE_FROM_PLEXTYPE[plex_type] userdata = api.userdata() _, _, tvshowtitle, season_no, episode_no = api.episode_data() db_item = xml_element.get('pkc_db_item') if db_item: # Item is synched to the Kodi db - let's use that info # (will thus e.g. include additional artwork or metadata) item = js.item_details(db_item['kodi_id'], kodi_type) else: people = api.people() cast = [{ 'name': x[0], 'thumbnail': x[1], 'role': x[2], 'order': x[3], } for x in api.people_list()['actor']] item = { 'cast': cast, 'country': api.country_list(), 'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59' 'director': people['Director'], # list of [str] 'duration': userdata['Runtime'], 'episode': episode_no, # 'file': '', # e.g. 'videodb://tvshows/titles/20' 'genre': api.genre_list(), # 'imdbnumber': '', # e.g.'341663' 'label': api.title(), # e.g. '1x05. Category 55 Emergency Doomsday Crisis' 'lastplayed': userdata['LastPlayedDate'], # e.g. '2019-01-04 16:05:03' 'mpaa': api.content_rating(), # e.g. 'TV-MA' 'originaltitle': '', # e.g. 'Titans (2018)' 'playcount': userdata['PlayCount'], # [int] 'plot': api.plot(), # [str] 'plotoutline': api.tagline(), 'premiered': api.premiere_date(), # '2018-10-12' 'rating': api.audience_rating(), # [float] 'season': season_no, 'sorttitle': api.sorttitle(), # 'Titans (2018)' 'studio': api.music_studio_list(), # e.g. 'DC Universe' 'tag': [], # List of tags this item belongs to 'tagline': api.tagline(), 'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv' 'title': api.title(), # 'Titans (2018)' 'type': kodi_type, 'trailer': api.trailer(), 'tvshowtitle': tvshowtitle, 'uniqueid': { 'imdbnumber': api.provider('imdb') or '', 'tvdb_id': api.provider('tvdb') or '' }, 'votes': '0', # [str]! 'writer': people['Writer'], # list of [str] 'year': api.year(), # [int] } if plex_type in (v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW): leaves = api.leave_count() if leaves: item['extraproperties'] = leaves # Add all the artwork we can item['art'] = api.artwork(full_artwork=True) # Add all info for e.g. video and audio streams item['streamdetails'] = api.mediastreams() # Cleanup required due to the way metadatautils works if not item['lastplayed']: del item['lastplayed'] for stream in item['streamdetails']['video']: stream['height'] = utils.cast(int, stream['height']) stream['width'] = utils.cast(int, stream['width']) stream['aspect'] = utils.cast(float, stream['aspect']) item['streamdetails']['subtitle'] = [{'language': x} for x in item['streamdetails']['subtitle']] # Resume point resume = api.resume_point() if resume: item['resume'] = { 'position': resume, 'total': userdata['Runtime'] } item['icon'] = v.ICON_FROM_PLEXTYPE[plex_type] # Some customization if plex_type == v.PLEX_TYPE_EPISODE: # Prefix to the episode's title/label if season_no is not None and episode_no is not None: if APPEND_SXXEXX is True: item['title'] = "S%.2dE%.2d - %s" % (season_no, episode_no, item['title']) if APPEND_SHOW_TITLE is True: item['title'] = "%s - %s " % (tvshowtitle, item['title']) item['label'] = item['title'] # Determine the path for this item key = api.path_and_plex_id() if key.startswith('/system/services') or key.startswith('http'): params = { 'mode': 'plex_node', 'key': key, 'offset': xml_element.attrib.get('viewOffset', '0'), } url = utils.extend_url('plugin://%s' % v.ADDON_ID, params) elif plex_type == v.PLEX_TYPE_PHOTO: url = api.get_picture_path() else: url = api.path() if not db_item and plex_type == v.PLEX_TYPE_EPISODE: # Hack - Item is not synched to the Kodi database # We CANNOT use paths that show up in the Kodi paths table! url = url.replace('plugin.video.plexkodiconnect.tvshows', 'plugin.video.plexkodiconnect') item['file'] = url return item def attach_kodi_ids(xml): """ Attaches the kodi db_item to the xml's children, attribute 'pkc_db_item' """ with PlexDB(lock=False) as plexdb: for child in xml: api = API(child) db_item = plexdb.item_by_id(api.plex_id(), api.plex_type()) child.set('pkc_db_item', db_item) return xml def prepare_listitem(item): """ helper to convert kodi output from json api to compatible format for listitems Code from script.module.metadatautils, kodidb.py """ try: # fix values returned from json to be used as listitem values properties = item.get("extraproperties", {}) # set type for idvar in [ ('episode', 'DefaultTVShows.png'), ('tvshow', 'DefaultTVShows.png'), ('movie', 'DefaultMovies.png'), ('song', 'DefaultAudio.png'), ('album', 'DefaultAudio.png'), ('artist', 'DefaultArtist.png'), ('musicvideo', 'DefaultMusicVideos.png'), ('recording', 'DefaultTVShows.png'), ('channel', 'DefaultAddonPVRClient.png')]: dbid = item.get(idvar[0] + "id") if dbid: properties["DBID"] = str(dbid) if not item.get("type"): item["type"] = idvar[0] if not item.get("icon"): item["icon"] = idvar[1] break # general properties if "genre" in item and isinstance(item['genre'], list): item["genre"] = " / ".join(item['genre']) if "studio" in item and isinstance(item['studio'], list): item["studio"] = " / ".join(item['studio']) if "writer" in item and isinstance(item['writer'], list): item["writer"] = " / ".join(item['writer']) if 'director' in item and isinstance(item['director'], list): item["director"] = " / ".join(item['director']) if 'artist' in item and not isinstance(item['artist'], list): item["artist"] = [item['artist']] if 'artist' not in item: item["artist"] = [] if item['type'] == "album" and 'album' not in item and 'label' in item: item['album'] = item['label'] if "duration" not in item and "runtime" in item: if (item["runtime"] / 60) > 300: item["duration"] = item["runtime"] / 60 else: item["duration"] = item["runtime"] if "plot" not in item and "comment" in item: item["plot"] = item["comment"] if "tvshowtitle" not in item and "showtitle" in item: item["tvshowtitle"] = item["showtitle"] if "premiered" not in item and "firstaired" in item: item["premiered"] = item["firstaired"] if "firstaired" in item and "aired" not in item: item["aired"] = item["firstaired"] if "imdbnumber" not in properties and "imdbnumber" in item: properties["imdbnumber"] = item["imdbnumber"] if "imdbnumber" not in properties and "uniqueid" in item: for value in item["uniqueid"].values(): if value.startswith("tt"): properties["imdbnumber"] = value properties["dbtype"] = item["type"] properties["DBTYPE"] = item["type"] properties["type"] = item["type"] properties["path"] = item.get("file") # cast list_cast = [] list_castandrole = [] item["cast_org"] = item.get("cast", []) if "cast" in item and isinstance(item["cast"], list): for castmember in item["cast"]: if isinstance(castmember, dict): list_cast.append(castmember.get("name", "")) list_castandrole.append((castmember["name"], castmember["role"])) else: list_cast.append(castmember) list_castandrole.append((castmember, "")) item["cast"] = list_cast item["castandrole"] = list_castandrole if "season" in item and "episode" in item: properties["episodeno"] = "s%se%s" % (item.get("season"), item.get("episode")) if "resume" in item: properties["resumetime"] = str(item['resume']['position']) properties["totaltime"] = str(item['resume']['total']) properties['StartOffset'] = str(item['resume']['position']) # streamdetails if "streamdetails" in item: streamdetails = item["streamdetails"] audiostreams = streamdetails.get('audio', []) videostreams = streamdetails.get('video', []) subtitles = streamdetails.get('subtitle', []) if len(videostreams) > 0: stream = videostreams[0] height = stream.get("height", "") width = stream.get("width", "") if height and width: resolution = "" if width <= 720 and height <= 480: resolution = "480" elif width <= 768 and height <= 576: resolution = "576" elif width <= 960 and height <= 544: resolution = "540" elif width <= 1280 and height <= 720: resolution = "720" elif width <= 1920 and height <= 1080: resolution = "1080" elif width * height >= 6000000: resolution = "4K" properties["VideoResolution"] = resolution if stream.get("codec", ""): properties["VideoCodec"] = str(stream["codec"]) if stream.get("aspect", ""): properties["VideoAspect"] = str(round(stream["aspect"], 2)) item["streamdetails"]["video"] = stream # grab details of first audio stream if len(audiostreams) > 0: stream = audiostreams[0] properties["AudioCodec"] = stream.get('codec', '') properties["AudioChannels"] = str(stream.get('channels', '')) properties["AudioLanguage"] = stream.get('language', '') item["streamdetails"]["audio"] = stream # grab details of first subtitle if len(subtitles) > 0: properties["SubtitleLanguage"] = subtitles[0].get('language', '') item["streamdetails"]["subtitle"] = subtitles[0] else: item["streamdetails"] = {} item["streamdetails"]["video"] = {'duration': item.get('duration', 0)} # additional music properties if 'album_description' in item: properties["Album_Description"] = item.get('album_description') # pvr properties if "channellogo" in item: properties["channellogo"] = item["channellogo"] properties["channelicon"] = item["channellogo"] if "episodename" in item: properties["episodename"] = item["episodename"] if "channel" in item: properties["channel"] = item["channel"] properties["channelname"] = item["channel"] item["label2"] = item["title"] # artwork art = item.get("art", {}) if item["type"] in ["episode", "season"]: if not art.get("fanart") and art.get("season.fanart"): art["fanart"] = art["season.fanart"] if not art.get("poster") and art.get("season.poster"): art["poster"] = art["season.poster"] if not art.get("landscape") and art.get("season.landscape"): art["poster"] = art["season.landscape"] if not art.get("fanart") and art.get("tvshow.fanart"): art["fanart"] = art.get("tvshow.fanart") if not art.get("poster") and art.get("tvshow.poster"): art["poster"] = art.get("tvshow.poster") if not art.get("clearlogo") and art.get("tvshow.clearlogo"): art["clearlogo"] = art.get("tvshow.clearlogo") if not art.get("banner") and art.get("tvshow.banner"): art["banner"] = art.get("tvshow.banner") if not art.get("landscape") and art.get("tvshow.landscape"): art["landscape"] = art.get("tvshow.landscape") if not art.get("fanart") and item.get('fanart'): art["fanart"] = item.get('fanart') if not art.get("thumb") and item.get('thumbnail'): art["thumb"] = get_clean_image(item.get('thumbnail')) if not art.get("thumb") and art.get('poster'): art["thumb"] = get_clean_image(art.get('poster')) if not art.get("thumb") and item.get('icon'): art["thumb"] = get_clean_image(item.get('icon')) if not item.get("thumbnail") and art.get('thumb'): item["thumbnail"] = art["thumb"] # clean art for key, value in art.iteritems(): if not isinstance(value, (str, unicode)): art[key] = "" elif value: art[key] = get_clean_image(value) item["art"] = art item["extraproperties"] = properties # return the result return item except Exception: utils.ERROR(notify=True) LOG.error('item that caused crash: %s', item) def create_listitem(item, as_tuple=True, offscreen=True): """ helper to create a kodi listitem from kodi compatible dict with mediainfo WARNING: paths, so item['file'] for items NOT synched to the Kodi DB shall NOT occur in the Kodi paths table! Kodi information screen does not work otherwise Code from script.module.metadatautils, kodidb.py """ try: if v.KODIVERSION > 17: liz = xbmcgui.ListItem( label=item.get("label", ""), label2=item.get("label2", ""), path=item['file'], offscreen=offscreen) else: liz = xbmcgui.ListItem( label=item.get("label", ""), label2=item.get("label2", ""), path=item['file']) # only set isPlayable prop if really needed if item.get("isFolder", False): liz.setProperty('IsPlayable', 'false') elif "plugin://script.skin.helper" not in item['file']: liz.setProperty('IsPlayable', 'true') nodetype = "Video" if item["type"] in ["song", "album", "artist"]: nodetype = "Music" # extra properties for key, value in item["extraproperties"].iteritems(): liz.setProperty(key, value) # video infolabels if nodetype == "Video": infolabels = { "title": item.get("title"), "size": item.get("size"), "genre": item.get("genre"), "year": item.get("year"), "top250": item.get("top250"), "tracknumber": item.get("tracknumber"), "rating": item.get("rating"), "playcount": item.get("playcount"), "overlay": item.get("overlay"), "cast": item.get("cast"), "castandrole": item.get("castandrole"), "director": item.get("director"), "mpaa": item.get("mpaa"), "plot": item.get("plot"), "plotoutline": item.get("plotoutline"), "originaltitle": item.get("originaltitle"), "sorttitle": item.get("sorttitle"), "duration": item.get("duration"), "studio": item.get("studio"), "tagline": item.get("tagline"), "writer": item.get("writer"), "tvshowtitle": item.get("tvshowtitle"), "premiered": item.get("premiered"), "status": item.get("status"), "code": item.get("imdbnumber"), "imdbnumber": item.get("imdbnumber"), "aired": item.get("aired"), "credits": item.get("credits"), "album": item.get("album"), "artist": item.get("artist"), "votes": item.get("votes"), "trailer": item.get("trailer"), # "progress": item.get('progresspercentage') } if item["type"] == "episode": infolabels["season"] = item["season"] infolabels["episode"] = item["episode"] # streamdetails if item.get("streamdetails"): liz.addStreamInfo("video", item["streamdetails"].get("video", {})) liz.addStreamInfo("audio", item["streamdetails"].get("audio", {})) liz.addStreamInfo("subtitle", item["streamdetails"].get("subtitle", {})) if "dateadded" in item: infolabels["dateadded"] = item["dateadded"] if "date" in item: infolabels["date"] = item["date"] # music infolabels else: infolabels = { "title": item.get("title"), "size": item.get("size"), "genre": item.get("genre"), "year": item.get("year"), "tracknumber": item.get("track"), "album": item.get("album"), "artist": " / ".join(item.get('artist')), "rating": str(item.get("rating", 0)), "lyrics": item.get("lyrics"), "playcount": item.get("playcount") } if "date" in item: infolabels["date"] = item["date"] if "duration" in item: infolabels["duration"] = item["duration"] if "lastplayed" in item: infolabels["lastplayed"] = item["lastplayed"] # setting the dbtype and dbid is supported from kodi krypton and up # PKC hack: ignore empty type if item["type"] not in ["recording", "channel", "favourite", ""]: infolabels["mediatype"] = item["type"] # setting the dbid on music items is not supported ? if nodetype == "Video" and "DBID" in item["extraproperties"]: infolabels["dbid"] = item["extraproperties"]["DBID"] if "lastplayed" in item: infolabels["lastplayed"] = item["lastplayed"] # assign the infolabels liz.setInfo(type=nodetype, infoLabels=infolabels) # artwork liz.setArt(item.get("art", {})) if "icon" in item: liz.setIconImage(item['icon']) if "thumbnail" in item: liz.setThumbnailImage(item['thumbnail']) # contextmenu if item["type"] in ["episode", "season"] and "season" in item and "tvshowid" in item: # add series and season level to widgets if "contextmenu" not in item: item["contextmenu"] = [] item["contextmenu"] += [ (xbmc.getLocalizedString(20364), "ActivateWindow(Video,videodb://tvshows/titles/%s/,return)" % (item["tvshowid"])), (xbmc.getLocalizedString(20373), "ActivateWindow(Video,videodb://tvshows/titles/%s/%s/,return)" % (item["tvshowid"], item["season"]))] if "contextmenu" in item: liz.addContextMenuItems(item["contextmenu"]) if as_tuple: return (item["file"], liz, item.get("isFolder", False)) else: return liz except Exception: utils.ERROR(notify=True) LOG.error('item that should have been turned into a listitem: %s', item) def create_main_entry(item): '''helper to create a simple (directory) listitem''' return { 'title': item[0], 'label': item[0], 'file': item[1], 'icon': item[2], 'art': { 'thumb': 'special://home/addons/%s/icon.png' % v.ADDON_ID, 'fanart': 'special://home/addons/%s/fanart.jpg' % v.ADDON_ID}, 'isFolder': True, 'type': '', 'IsPlayable': 'false' }