diff --git a/addon.xml b/addon.xml index 41fa5f77..7335a2b4 100644 --- a/addon.xml +++ b/addon.xml @@ -8,6 +8,7 @@ + video audio image diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index ea4a4685..1f6fa56a 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -585,6 +585,15 @@ def item_details(kodi_id, kodi_type): ret = JsonRPC(json).execute({'%sid' % kodi_type: kodi_id, 'properties': fields}) try: - return ret['result']['%sdetails' % kodi_type] + ret = ret['result']['%sdetails' % kodi_type] except (KeyError, TypeError): return {} + if kodi_type == v.KODI_TYPE_SHOW: + # append watched counts to tvshow details + ret["extraproperties"] = { + "totalseasons": str(ret["season"]), + "totalepisodes": str(ret["episode"]), + "watchedepisodes": str(ret["watchedepisodes"]), + "unwatchedepisodes": str(ret["episode"] - ret["watchedepisodes"]) + } + return ret diff --git a/resources/lib/kodi_constants.py b/resources/lib/kodi_constants.py index 61f88cd9..35ff00d9 100644 --- a/resources/lib/kodi_constants.py +++ b/resources/lib/kodi_constants.py @@ -5,88 +5,45 @@ script.module.metadatautils kodi_constants.py Several common constants for use with Kodi json api ''' -FIELDS_BASE = ['dateadded', 'file', 'lastplayed', 'plot', 'title', 'art', - 'playcount'] -FIELDS_FILE = FIELDS_BASE + ['streamdetails', 'director', 'resume', 'runtime'] -FIELDS_MOVIES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes', - 'showlink', 'top250', 'trailer', 'year', 'country', 'studio', 'set', - 'genre', 'mpaa', 'setid', 'rating', 'tag', 'tagline', 'writer', - 'originaltitle', 'imdbnumber', 'uniqueid'] -FIELDS_TVSHOWS = FIELDS_BASE + ['sorttitle', 'mpaa', 'premiered', 'year', - 'episode', 'watchedepisodes', 'votes', 'rating', 'studio', 'season', - 'genre', 'cast', 'episodeguide', 'tag', 'originaltitle', 'imdbnumber'] +FIELDS_BASE = ["dateadded", "file", "lastplayed", "plot", "title", "art", "playcount"] +FIELDS_FILE = FIELDS_BASE + ["streamdetails", "director", "resume", "runtime"] +FIELDS_MOVIES = FIELDS_FILE + ["plotoutline", "sorttitle", "cast", "votes", "showlink", "top250", "trailer", "year", + "country", "studio", "set", "genre", "mpaa", "setid", "rating", "tag", "tagline", + "writer", "originaltitle", + "imdbnumber"] +FIELDS_MOVIES.append("uniqueid") +FIELDS_TVSHOWS = FIELDS_BASE + ["sorttitle", "mpaa", "premiered", "year", "episode", "watchedepisodes", "votes", + "rating", "studio", "season", "genre", "cast", "episodeguide", "tag", "originaltitle", + "imdbnumber"] FIELDS_SEASON = ['art', 'playcount', 'season', 'showtitle', 'episode', - 'tvshowid', 'watchedepisodes', 'userrating', 'fanart', 'thumbnail'] -FIELDS_EPISODES = FIELDS_FILE + ['cast', 'productioncode', 'rating', 'votes', - 'episode', 'showtitle', 'tvshowid', 'season', 'firstaired', 'writer', - 'originaltitle'] -FIELDS_MUSICVIDEOS = FIELDS_FILE + ['genre', 'artist', 'tag', 'album', 'track', - 'studio', 'year'] -FIELDS_FILES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes', - 'trailer', 'year', 'country', 'studio', 'genre', 'mpaa', 'rating', - 'tagline', 'writer', 'originaltitle', 'imdbnumber', 'premiered', 'episode', - 'showtitle', 'firstaired', 'watchedepisodes', 'duration', 'season'] -FIELDS_SONGS = ['artist', 'displayartist', 'title', 'rating', 'fanart', - 'thumbnail', 'duration', 'disc', 'playcount', 'comment', 'file', 'album', - 'lastplayed', 'genre', 'musicbrainzartistid', 'track', 'dateadded'] -FIELDS_ALBUMS = ['title', 'fanart', 'thumbnail', 'genre', 'displayartist', - 'artist', 'musicbrainzalbumartistid', 'year', 'rating', 'artistid', - 'musicbrainzalbumid', 'theme', 'description', 'type', 'style', 'playcount', - 'albumlabel', 'mood', 'dateadded'] -FIELDS_ARTISTS = ['born', 'formed', 'died', 'style', 'yearsactive', 'mood', - 'fanart', 'thumbnail', 'musicbrainzartistid', 'disbanded', 'description', - 'instrument'] -FIELDS_RECORDINGS = ['art', 'channel', 'directory', 'endtime', 'file', 'genre', - 'icon', 'playcount', 'plot', 'plotoutline', 'resume', 'runtime', - 'starttime', 'streamurl', 'title'] -FIELDS_CHANNELS = ['broadcastnow', 'channeltype', 'hidden', 'locked', - 'lastplayed', 'thumbnail', 'channel'] + 'tvshowid', 'watchedepisodes', 'userrating', 'fanart', 'thumbnail'] +FIELDS_EPISODES = FIELDS_FILE + ["cast", "productioncode", "rating", "votes", "episode", "showtitle", "tvshowid", + "season", "firstaired", "writer", "originaltitle"] +FIELDS_MUSICVIDEOS = FIELDS_FILE + ["genre", "artist", "tag", "album", "track", "studio", "year"] +FIELDS_FILES = FIELDS_FILE + ["plotoutline", "sorttitle", "cast", "votes", "trailer", "year", "country", "studio", + "genre", "mpaa", "rating", "tagline", "writer", "originaltitle", "imdbnumber", + "premiered", "episode", "showtitle", + "firstaired", "watchedepisodes", "duration", "season"] +FIELDS_SONGS = ["artist", "displayartist", "title", "rating", "fanart", "thumbnail", "duration", "disc", + "playcount", "comment", "file", "album", "lastplayed", "genre", "musicbrainzartistid", "track", + "dateadded"] +FIELDS_ALBUMS = ["title", "fanart", "thumbnail", "genre", "displayartist", "artist", + "musicbrainzalbumartistid", "year", "rating", "artistid", "musicbrainzalbumid", "theme", "description", + "type", "style", "playcount", "albumlabel", "mood", "dateadded"] +FIELDS_ARTISTS = ["born", "formed", "died", "style", "yearsactive", "mood", "fanart", "thumbnail", + "musicbrainzartistid", "disbanded", "description", "instrument"] +FIELDS_RECORDINGS = ["art", "channel", "directory", "endtime", "file", "genre", "icon", "playcount", "plot", + "plotoutline", "resume", "runtime", "starttime", "streamurl", "title"] +FIELDS_CHANNELS = ["broadcastnow", "channeltype", "hidden", "locked", "lastplayed", "thumbnail", "channel"] -FILTER_UNWATCHED = { - 'operator': 'lessthan', - 'field': 'playcount', - 'value': '1' -} -FILTER_WATCHED = { - 'operator': 'isnot', - 'field': 'playcount', - 'value': '0' -} -FILTER_RATING = { - 'operator': 'greaterthan', - 'field': 'rating', - 'value': '7' -} -FILTER_RATING_MUSIC = { - 'operator': 'greaterthan', - 'field': 'rating', - 'value': '3' -} -FILTER_INPROGRESS = { - 'operator': 'true', - 'field': 'inprogress', - 'value': '' -} -SORT_RATING = { - 'method': 'rating', - 'order': 'descending' -} -SORT_RANDOM = { - 'method': 'random', - 'order': 'descending' -} -SORT_TITLE = { - 'method': 'title', - 'order': 'ascending' -} -SORT_DATEADDED = { - 'method': 'dateadded', - 'order': 'descending' -} -SORT_LASTPLAYED = { - 'method': 'lastplayed', - 'order': 'descending' -} -SORT_EPISODE = { - 'method': 'episode' -} +FILTER_UNWATCHED = {"operator": "lessthan", "field": "playcount", "value": "1"} +FILTER_WATCHED = {"operator": "isnot", "field": "playcount", "value": "0"} +FILTER_RATING = {"operator": "greaterthan", "field": "rating", "value": "7"} +FILTER_RATING_MUSIC = {"operator": "greaterthan", "field": "rating", "value": "3"} +FILTER_INPROGRESS = {"operator": "true", "field": "inprogress", "value": ""} +SORT_RATING = {"method": "rating", "order": "descending"} +SORT_RANDOM = {"method": "random", "order": "descending"} +SORT_TITLE = {"method": "title", "order": "ascending"} +SORT_DATEADDED = {"method": "dateadded", "order": "descending"} +SORT_LASTPLAYED = {"method": "lastplayed", "order": "descending"} +SORT_EPISODE = {"method": "episode"} diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 002e96ca..3f69f303 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -9,6 +9,7 @@ from datetime import datetime from unicodedata import normalize from threading import Lock import urllib +import arrow # Originally tried faster cElementTree, but does NOT work reliably with Kodi # etree parse unsafe; make sure we're always receiving unicode from . import defused_etree as etree @@ -616,6 +617,21 @@ def indent(elem, level=0): LOG.info('Indentation failed with: %s', err) +def localdate_from_utc_string(timestring): + """helper to convert internal utc time (used in pvr) to local timezone""" + utc_datetime = arrow.get(timestring) + local_datetime = utc_datetime.to('local') + return local_datetime.format("YYYY-MM-DD HH:mm:ss") + + +def localized_date_time(timestring): + """returns localized version of the timestring (used in pvr)""" + date_time = arrow.get(timestring) + local_date = date_time.strftime(xbmc.getRegion("dateshort")) + local_time = date_time.strftime(xbmc.getRegion("time").replace(":%S", "")) + return local_date, local_time + + class XmlKodiSetting(object): """ Used to load a Kodi XML settings file from special://profile as an etree diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index 9da16035..5ee37e35 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -7,6 +7,7 @@ Loads of different functions called in SEPARATE Python instances through e.g. plugin://... calls. Hence be careful to only rely on window variables. """ from logging import getLogger +import arrow import xbmc import xbmcgui @@ -157,7 +158,6 @@ def _generate_content(api): 'director': api.directors(), # list of [str] 'duration': api.runtime(), 'episode': api.index(), - # 'file': '', # e.g. 'videodb://tvshows/titles/20' 'genre': api.genres(), # 'imdbnumber': '', # e.g.'341663' 'label': api.title(), # e.g. '1x05. Category 55 Emergency Doomsday Crisis' @@ -246,12 +246,8 @@ def _generate_content(api): def prepare_listitem(item): - """ - helper to convert kodi output from json api to compatible format for - listitems - - Code from script.module.metadatautils, kodidb.py - """ + """helper to convert kodi output from json api to compatible format for + listitems""" try: # fix values returned from json to be used as listitem values properties = item.get("extraproperties", {}) @@ -292,8 +288,8 @@ def prepare_listitem(item): 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 + if (item["runtime"] // 60) > 300: + item["duration"] = item["runtime"] // 60 else: item["duration"] = item["runtime"] if "plot" not in item and "comment" in item: @@ -307,7 +303,7 @@ def prepare_listitem(item): 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 list(item["uniqueid"].values()): + for value in item["uniqueid"].values(): if value.startswith("tt"): properties["imdbnumber"] = value @@ -391,6 +387,22 @@ def prepare_listitem(item): properties["Album_Description"] = item.get('album_description') # pvr properties + if "starttime" in item: + # convert utc time to local time + item["starttime"] = utils.localdate_from_utc_string(item["starttime"]) + item["endtime"] = utils.localdate_from_utc_string(item["endtime"]) + # set localized versions of the time and date as additional props + startdate, starttime = utils.localized_date_time(item['starttime']) + enddate, endtime = utils.localized_date_time(item['endtime']) + properties["StartTime"] = starttime + properties["StartDate"] = startdate + properties["EndTime"] = endtime + properties["EndDate"] = enddate + properties["Date"] = "%s %s-%s" % (startdate, starttime, endtime) + properties["StartDateTime"] = "%s %s" % (startdate, starttime) + properties["EndDateTime"] = "%s %s" % (enddate, endtime) + # set date to startdate + item["date"] = arrow.get(item["starttime"]).format("DD.MM.YYYY") if "channellogo" in item: properties["channellogo"] = item["channellogo"] properties["channelicon"] = item["channellogo"] @@ -441,51 +453,47 @@ def prepare_listitem(item): item["extraproperties"] = properties - # return the result + if "file" not in item or not item['file']: + LOG.warn('No filepath for item: %s', item) + item["file"] = "" + return item - except Exception: - utils.ERROR(notify=True) - LOG.error('item that caused crash: %s', item) + except Exception as exc: + LOG.error('item: %s', item) + LOG.exception('Exception encountered: %s', exc) -def create_listitem(item, as_tuple=True, offscreen=True, - listitem=xbmcgui.ListItem): - """ - 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 - """ +def create_listitem(item, as_tuple=True, offscreen=True): + """helper to create a kodi listitem from kodi compatible dict with mediainfo""" try: - liz = listitem( + liz = xbmcgui.ListItem( label=item.get("label", ""), label2=item.get("label2", ""), path=item['file'], offscreen=offscreen) + # 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" + nodetype = "Music" elif item['type'] == 'photo': - nodetype = 'pictures' - else: - nodetype = 'video' + nodetype = 'Pictures' # extra properties for key, value in item["extraproperties"].items(): liz.setProperty(key, value) - if nodetype == 'video': + # video infolabels + if nodetype == "Video": infolabels = { "title": item.get("title"), + "path": item.get("file"), "size": item.get("size"), "genre": item.get("genre"), "year": item.get("year"), @@ -516,8 +524,7 @@ def create_listitem(item, as_tuple=True, offscreen=True, "album": item.get("album"), "artist": item.get("artist"), "votes": item.get("votes"), - "trailer": item.get("trailer"), - # "progress": item.get('progresspercentage') + "trailer": item.get("trailer") } if item["type"] == "episode": infolabels["season"] = item["season"] @@ -534,7 +541,8 @@ def create_listitem(item, as_tuple=True, offscreen=True, if "date" in item: infolabels["date"] = item["date"] - elif nodetype == 'music': + # music infolabels + elif nodetype == 'Music': infolabels = { "title": item.get("title"), "size": item.get("size"), @@ -560,12 +568,12 @@ def create_listitem(item, as_tuple=True, offscreen=True, "title": item.get("title"), 'picturepath': item['file'] } + # 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", ""]: + if item["type"] not in ["recording", "channel", "favourite", "genre", "categorie"]: infolabels["mediatype"] = item["type"] # setting the dbid on music items is not supported ? - if nodetype == "video" and "DBID" in item["extraproperties"]: + if nodetype == "Video" and "DBID" in item["extraproperties"]: infolabels["dbid"] = item["extraproperties"]["DBID"] if "lastplayed" in item: @@ -575,9 +583,11 @@ def create_listitem(item, as_tuple=True, offscreen=True, liz.setInfo(type=nodetype, infoLabels=infolabels) # artwork - if "icon" in item: - item['art']['icon'] = item['icon'] liz.setArt(item.get("art", {})) + if "icon" in item: + liz.setArt({"icon": item['icon']}) + if "thumbnail" in item: + liz.setArt({"thumb": item['thumbnail']}) # contextmenu if item["type"] in ["episode", "season"] and "season" in item and "tvshowid" in item: @@ -585,20 +595,20 @@ def create_listitem(item, as_tuple=True, offscreen=True, if "contextmenu" not in item: item["contextmenu"] = [] item["contextmenu"] += [ - (xbmc.getLocalizedString(20364), "ActivateWindow(Video,videodb://tvshows/titles/%s/,return)" + (xbmc.getLocalizedString(20364), "ActivateWindow(Videos,videodb://tvshows/titles/%s/,return)" % (item["tvshowid"])), - (xbmc.getLocalizedString(20373), "ActivateWindow(Video,videodb://tvshows/titles/%s/%s/,return)" + (xbmc.getLocalizedString(20373), "ActivateWindow(Videos,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)) + 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) + except Exception as exc: + LOG.error('item: %s', item) + LOG.exception('Exception encountered: %s', exc) def create_main_entry(item):