From a313c4e56279253c0b2b0e03b7cc26b9777d5ea8 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sat, 18 Jan 2020 03:40:44 +0100 Subject: [PATCH] More assorted fixes This PR includes: - Fix requirements.txt for xmlschema on Python 2.7 - Add tagline and plotoutline to TV channels - Add label2Mask to sort methods - Add label2 episodes to folders (does not work in Kodi yet) - Add episode and season to folders (see video pane) --- Makefile | 1 + .../resource.language.en_gb/strings.po | 12 --- .../resource.language.nl_nl/strings.po | 12 --- resources/lib/apihelper.py | 39 ++++++++-- resources/lib/kodiutils.py | 74 +++++++++++-------- resources/lib/metadata.py | 5 +- resources/lib/resumepoints.py | 6 +- resources/lib/tvguide.py | 2 +- resources/settings.xml | 4 - tests/userdata/search_history.json | 2 +- tests/xbmc.py | 5 +- tests/xbmcaddon.py | 8 +- tests/xbmcextra.py | 2 + tests/xbmcgui.py | 19 +---- tests/xbmcplugin.py | 2 +- tests/xbmcvfs.py | 2 +- 16 files changed, 93 insertions(+), 102 deletions(-) diff --git a/Makefile b/Makefile index bc6f2b34..2aeca569 100644 --- a/Makefile +++ b/Makefile @@ -80,6 +80,7 @@ build: clean @echo -e "$(white)=$(blue) Successfully wrote package as: $(white)../$(zip_name)$(reset)" clean: + @echo -e "$(white)=$(blue) Cleaning up$(reset)" find . -name '*.py[cod]' -type f -delete find . -name '__pycache__' -type d -delete rm -rf .pytest_cache/ .tox/ diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 9d39d073..0ffd9249 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -756,18 +756,6 @@ msgctxt "#30874" msgid "Open Up Next service add-on settings." msgstr "" -msgctxt "#30875" -msgid "Install Twitter add-on" -msgstr "" - -msgctxt "#30877" -msgid "Enable Twitter integration" -msgstr "" - -msgctxt "#30879" -msgid "Twitter settings…" -msgstr "" - msgctxt "#30881" msgid "Install PySocks library" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 57c80018..1be283ff 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -756,18 +756,6 @@ msgctxt "#30874" msgid "Open Up Next service add-on settings." msgstr "Open de Up Next service add-on instellingen." -msgctxt "#30875" -msgid "Install Twitter add-on" -msgstr "Installeer de Twitter add-on" - -msgctxt "#30877" -msgid "Enable Twitter integration" -msgstr "Activeer Twitter integratie" - -msgctxt "#30879" -msgid "Twitter settings…" -msgstr "Twitter instellingen…" - msgctxt "#30881" msgid "Install PySocks library" msgstr "Installeer de PySocks library" diff --git a/resources/lib/apihelper.py b/resources/lib/apihelper.py index f21c1895..31ac8b34 100644 --- a/resources/lib/apihelper.py +++ b/resources/lib/apihelper.py @@ -160,8 +160,14 @@ def __map_seasons(self, program, seasons, episodes): content = 'seasons' episode = random.choice(episodes) - info_labels = self._metadata.get_info_labels(episode, season=True) program_type = episode.get('programType') + info_labels = self._metadata.get_info_labels(episode, season=True) + info_labels.update( + episode=len(episodes), # Total number of episodes in '* All seasons' + season=len(seasons), # Total number of seasons in '* All seasons' + tagline=localize(30133), # All seasons + title=localize(30133), # All seasons + ) # Reverse sort seasons if program_type is 'reeksaflopend' or 'daily' if program_type in ('daily', 'reeksaflopend'): @@ -173,7 +179,7 @@ def __map_seasons(self, program, seasons, episodes): label=localize(30133), # All seasons path=url_for('programs', program=program, season='allseasons'), art_dict=self._metadata.get_art(episode, season='allseasons'), - info_dict=info_labels, + info_dict=info_labels.copy(), )) # NOTE: Sort the episodes ourselves, because Kodi does not allow to set to 'ascending' @@ -181,18 +187,26 @@ def __map_seasons(self, program, seasons, episodes): for season in seasons: season_key = season.get('key', '') + episodelist = [e for e in episodes if e.get('seasonName') == season_key] # If more than 300 episodes exist, we may end up with an empty season (Winteruur) try: - episode = random.choice([e for e in episodes if e.get('seasonName') == season_key]) + episode = random.choice(episodelist) except IndexError: episode = episodes[0] label = '%s %s' % (localize(30131), season_key) # Season X + info_labels.update( + episode=len(episodelist), # Number of episodes in this folder + season=1, # Number of seasons in this folder + tagline=label, + title=label, + ) + season_items.append(TitleItem( label=label, path=url_for('programs', program=program, season=season_key), art_dict=self._metadata.get_art(episode, season=True), - info_dict=info_labels, + info_dict=info_labels.copy(), prop_dict=self._metadata.get_properties(episode), )) return season_items, sort, ascending, content @@ -641,7 +655,7 @@ def list_channels(self, channels=None, live=True): label += ' [COLOR=yellow]| %s[/COLOR]' % playing_now # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: - label = '[B]%s[/B]' % label + label = '[COLOR yellow][B]%s[/B][/COLOR]' % label is_playable = True if channel.get('name') in ['een', 'canvas', 'ketnet']: if get_setting_bool('showfanart', default=True): @@ -650,7 +664,16 @@ def list_channels(self, channels=None, live=True): else: plot = localize(30142, **channel) # Watch live # NOTE: Playcount and resumetime are required to not have live streams as "Watched" and resumed - info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0, duration=0) + info_dict = dict( + title=label, + plot=plot, + plotoutline=playing_now, + tagline=playing_now, + studio=channel.get('studio'), + mediatype='video', + playcount=0, + duration=0, + ) prop_dict = dict(resumetime=0) stream_dict = dict(duration=0) context_menu.append(( @@ -700,10 +723,10 @@ def list_youtube(channels=None): label = localize(30143, **youtube) # Channel on YouTube # A single Live channel means it is the entry for channel's TV Show listing, so make it stand out if channels and len(channels) == 1: - label = '[B]%s[/B]' % label + label = '[COLOR yellow][B]%s[/B][/COLOR]' % label plot = localize(30144, **youtube) # Watch on YouTube # NOTE: Playcount is required to not have live streams as "Watched" - info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), mediatype='video', playcount=0) + info_dict = dict(title=label, plot=plot, studio=channel.get('studio'), playcount=0) context_menu = [( localize(30413), # Refresh menu diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index dafa3b8e..1ea104f9 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -15,17 +15,17 @@ SORT_METHODS = dict( # date=xbmcplugin.SORT_METHOD_DATE, - dateadded=xbmcplugin.SORT_METHOD_DATEADDED, - duration=xbmcplugin.SORT_METHOD_DURATION, - episode=xbmcplugin.SORT_METHOD_EPISODE, - # genre=xbmcplugin.SORT_METHOD_GENRE, - # label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE, - label=xbmcplugin.SORT_METHOD_LABEL, - title=xbmcplugin.SORT_METHOD_TITLE, - # none=xbmcplugin.SORT_METHOD_UNSORTED, + dateadded=dict(method=xbmcplugin.SORT_METHOD_DATEADDED, label2='%a'), + duration=dict(method=xbmcplugin.SORT_METHOD_DURATION, label2='%D'), + episode=dict(method=xbmcplugin.SORT_METHOD_EPISODE, label2='%D'), + # genre=dict(method=xbmcplugin.SORT_METHOD_GENRE, label2='%D'), + # label=dict(method=xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE, label2='%D'), + label=dict(method=xbmcplugin.SORT_METHOD_LABEL, label2='%D'), + title=dict(method=xbmcplugin.SORT_METHOD_TITLE, label2='%D'), + # none=dict(method=xbmcplugin.SORT_METHOD_UNSORTED, label2='%D'), # FIXME: We would like to be able to sort by unprefixed title (ignore date/episode prefix) - # title=xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, - unsorted=xbmcplugin.SORT_METHOD_UNSORTED, + # title=dict(method=xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, label2='%D'), + unsorted=dict(method=xbmcplugin.SORT_METHOD_UNSORTED, label2='%D'), ) WEEKDAY_LONG = { @@ -86,6 +86,16 @@ def __missing__(self, key): return '{' + key + '}' +def translate_path(path): + """Translate special xbmc paths""" + return to_unicode(xbmc.translatePath(path)) + + +def get_addon_info(key): + """Return addon information""" + return to_unicode(ADDON.getAddonInfo(key)) + + def addon_icon(): """Cache and return add-on icon""" return get_addon_info('icon') @@ -108,12 +118,17 @@ def addon_name(): def addon_path(): """Cache and return add-on path""" - return get_addon_info('path') + return translate_path(get_addon_info('path')) def addon_profile(): """Cache and return add-on profile""" - return to_unicode(xbmc.translatePath(ADDON.getAddonInfo('profile'))) + return translate_path(ADDON.getAddonInfo('profile')) + + +def addon_version(): + """Cache and return add-on version""" + return get_addon_info('version') def url_for(name, *args, **kwargs): @@ -164,12 +179,12 @@ def show_listing(list_items, category=None, sort='unsorted', ascending=True, con sort = 'unsorted' # Add all sort methods to GUI (start with preferred) - xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[sort]) + xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[sort]['method'], label2Mask=SORT_METHODS[sort]['label2']) for key in sorted(SORT_METHODS): if key != sort: - xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[key]) + xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[key]['method'], label2Mask=SORT_METHODS[key]['label2']) - # FIXME: This does not appear to be working, we have to order it ourselves + # FIXME: This does not appear to be working, we have to order it ourselves and use 'unsorted' method # xbmcplugin.setProperty(handle=plugin.handle, key='sort.ascending', value='true' if ascending else 'false') # if ascending: # xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS[sort])) @@ -185,7 +200,7 @@ def show_listing(list_items, category=None, sort='unsorted', ascending=True, con # - item is a playable file (playable, path) # - item is non-actionable item (not playable, no path) is_folder = bool(not title_item.is_playable and title_item.path) - is_playable = bool(title_item.is_playable and title_item.path) + is_playable = bool(title_item.is_playable and not title_item.path.endswith('/noop')) list_item = ListItem(label=title_item.label) @@ -220,6 +235,10 @@ def show_listing(list_items, category=None, sort='unsorted', ascending=True, con # type is one of: video, music, pictures, game list_item.setInfo(type='video', infoLabels=title_item.info_dict) + # Add number of episodes to folders + if is_folder and title_item.info_dict.get('episode'): + list_item.setLabel2(str(title_item.info_dict.get('episode'))) + if title_item.stream_dict: # type is one of: video, audio, subtitle list_item.addStreamInfo('video', title_item.stream_dict) @@ -299,6 +318,14 @@ def get_search_string(search_string=None): return search_string +def multiselect(heading='', options=None, autoclose=0, preselect=None, use_details=False): + """Show a Kodi multi-select dialog""" + from xbmcgui import Dialog + if not heading: + heading = addon_name() + return Dialog().multiselect(heading=heading, options=options, autoclose=autoclose, preselect=preselect, useDetails=use_details) + + def ok_dialog(heading='', message=''): """Show Kodi's OK dialog""" from xbmcgui import Dialog @@ -317,14 +344,6 @@ def notification(heading='', message='', icon='info', time=4000): Dialog().notification(heading=heading, message=message, icon=icon, time=time) -def multiselect(heading='', options=None, autoclose=0, preselect=None, use_details=False): - """Show a Kodi multi-select dialog""" - from xbmcgui import Dialog - if not heading: - heading = addon_name() - return Dialog().multiselect(heading=heading, options=options, autoclose=autoclose, preselect=preselect, useDetails=use_details) - - def set_locale(): """Load the proper locale for date strings, only once""" if hasattr(set_locale, 'cached'): @@ -355,7 +374,7 @@ def localize(string_id, **kwargs): def localize_time(time): """Return localized time""" time_format = xbmc.getRegion('time').replace(':%S', '') # Strip off seconds - return time.strftime(time_format).lstrip('0') # Remove leading zero on all platforms + return time.strftime(time_format) def localize_date(date, strftime): @@ -699,11 +718,6 @@ def get_cache_path(): return getattr(get_cache_path, 'cached') -def get_addon_info(key): - """Return addon information""" - return to_unicode(ADDON.getAddonInfo(key)) - - def listdir(path): """Return all files in a directory (using xbmcvfs)""" from xbmcvfs import listdir as vfslistdir diff --git a/resources/lib/metadata.py b/resources/lib/metadata.py index d2b576ad..d02a24e5 100644 --- a/resources/lib/metadata.py +++ b/resources/lib/metadata.py @@ -414,7 +414,7 @@ def get_episode(self, api_data): # VRT NU Suggest API if api_data.get('type') == 'program': - return int() + return api_data.get('episode_count', int()) # The number of episodes # VRT NU Schedule API (some are missing vrt.whatson-id) if api_data.get('vrt.whatson-id') or api_data.get('startTime'): @@ -604,7 +604,6 @@ def get_info_labels(self, api_data, season=False, date=None, channel=None): if api_data.get('type') == 'episode': info_labels = dict( title=self.get_title(api_data), - # sorttitle=self.get_title(api_data), # NOTE: Does not appear to work tvshowtitle=self.get_tvshowtitle(api_data), # date=self.get_date(api_data), # NOTE: Not sure when or how this is used aired=self.get_aired(api_data), @@ -628,6 +627,7 @@ def get_info_labels(self, api_data, season=False, date=None, channel=None): info_labels = dict( tvshowtitle=self.get_tvshowtitle(api_data), plot=self.get_plot(api_data), + episode=self.get_episode(api_data), mediatype=self.get_mediatype(api_data, season=season), studio=self.get_studio(api_data), tag=self.get_tag(api_data), @@ -638,7 +638,6 @@ def get_info_labels(self, api_data, season=False, date=None, channel=None): if api_data.get('vrt.whatson-id') or api_data.get('startTime'): info_labels = dict( title=self.get_title(api_data), - # sorttitle=self.get_title(api_data), # NOTE: Does not appear to work tvshowtitle=self.get_tvshowtitle(api_data), aired=self.get_aired(api_data), plot=self.get_plot(api_data, date=date), diff --git a/resources/lib/resumepoints.py b/resources/lib/resumepoints.py index e5a6b7aa..2f7d57f9 100644 --- a/resources/lib/resumepoints.py +++ b/resources/lib/resumepoints.py @@ -75,9 +75,9 @@ def update(self, asset_id, title, url, watch_later=None, position=None, total=No # Update if (self.still_watching(position, total) or watch_later is True or (path and path.startswith('plugin://plugin.video.vrt.nu/play/upnext'))): - # Normally, VRT NU resumepoints are deleted when an episode is (un)watched and Kodi GUI automatically sets the (un)watched status when Kodi Player exits. - # This mechanism doesn't work with "Up Next" episodes because these episodes are not initiated from a ListItem in Kodi GUI. - # For "Up Next" episodes, we should never delete the VRT NU resumepoints to make sure the watched status can be forced in Kodi GUI using the playcount infolabel. + # Normally, VRT NU resumepoints are deleted when an episode is (un)watched and Kodi GUI automatically sets the (un)watched status when Player exits + # This mechanism doesn't work with "Up Next" episodes because these episodes are not initiated from a ListItem in Kodi GUI + # For "Up Next" episodes, we should never delete the VRT NU resumepoints to make sure the watched status can be forced using the playcount infolabel log(3, "[Resumepoints] Update resumepoint '{asset_id}' {position}/{total}", asset_id=asset_id, position=position, total=total) diff --git a/resources/lib/tvguide.py b/resources/lib/tvguide.py index 450e7b23..4b0739cf 100644 --- a/resources/lib/tvguide.py +++ b/resources/lib/tvguide.py @@ -136,7 +136,7 @@ def get_channel_items(self, date=None, channel=None): path = url_for('tvguide', date=date, channel=chan.get('name')) plot = '[B]%s[/B]\n%s' % (datelong, localize(30302, **chan)) else: - label = '[B]%s[/B]' % localize(30303, **chan) + label = '[COLOR yellow][B]%s[/B][/COLOR]' % localize(30303, **chan) path = url_for('tvguide_channel', channel=chan.get('name')) plot = '%s\n\n%s' % (localize(30302, **chan), self.live_description(chan.get('name'))) diff --git a/resources/settings.xml b/resources/settings.xml index 7a1d2355..a390214d 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -60,10 +60,6 @@ - - - - diff --git a/tests/userdata/search_history.json b/tests/userdata/search_history.json index 9d4cc0b2..9c6f78c6 100644 --- a/tests/userdata/search_history.json +++ b/tests/userdata/search_history.json @@ -1 +1 @@ -["winter", "dag", "test", "foobar"] \ No newline at end of file +["foobar"] \ No newline at end of file diff --git a/tests/xbmc.py b/tests/xbmc.py index 4ecf54d8..ead9a0ce 100644 --- a/tests/xbmc.py +++ b/tests/xbmc.py @@ -11,8 +11,8 @@ import json import time import weakref -from xbmcextra import ADDON_ID, global_settings, import_language from utils import to_unicode +from xbmcextra import ADDON_ID, GLOBAL_SETTINGS as settings, LANGUAGE LOGLEVELS = ['Debug', 'Info', 'Notice', 'Warning', 'Error', 'Severe', 'Fatal', 'None'] LOGDEBUG = 0 @@ -34,9 +34,6 @@ 'dateshort': '%Y-%m-%d', } -settings = global_settings() -LANGUAGE = import_language(language=settings.get('locale.language')) - class Keyboard(object): # pylint: disable=useless-object-inheritance """A stub implementation of the xbmc Keyboard class""" diff --git a/tests/xbmcaddon.py b/tests/xbmcaddon.py index 7890c6a6..c50e53ea 100644 --- a/tests/xbmcaddon.py +++ b/tests/xbmcaddon.py @@ -9,9 +9,6 @@ from xbmc import getLocalizedString from xbmcextra import ADDON_ID, ADDON_INFO, addon_settings -# Ensure the addon settings are retained (as we don't write to disk) -ADDON_SETTINGS = addon_settings(ADDON_ID) - class Addon: """A reimplementation of the xbmcaddon Addon class""" @@ -19,10 +16,7 @@ class Addon: def __init__(self, id=ADDON_ID): # pylint: disable=redefined-builtin """A stub constructor for the xbmcaddon Addon class""" self.id = id - if id == ADDON_ID: - self.settings = ADDON_SETTINGS - else: - self.settings = addon_settings(id) + self.settings = addon_settings(id) def getAddonInfo(self, key): """A working implementation for the xbmcaddon Addon class getAddonInfo() method""" diff --git a/tests/xbmcextra.py b/tests/xbmcextra.py index 93d85cee..7ea9caaa 100644 --- a/tests/xbmcextra.py +++ b/tests/xbmcextra.py @@ -186,3 +186,5 @@ def import_language(language): ADDON_INFO = read_addon_xml('addon.xml') ADDON_ID = next(iter(list(ADDON_INFO.values()))).get('id') +GLOBAL_SETTINGS = global_settings() +LANGUAGE = import_language(language=GLOBAL_SETTINGS.get('locale.language')) diff --git a/tests/xbmcgui.py b/tests/xbmcgui.py index d1103be4..58546d00 100644 --- a/tests/xbmcgui.py +++ b/tests/xbmcgui.py @@ -19,7 +19,6 @@ def __init__(self): @staticmethod def selectItem(index): """A stub implementation for the xbmcgui Control class selectItem() method""" - return class ControlLabel(Control): @@ -210,37 +209,34 @@ def __init__(self, label='', label2='', iconImage='', thumbnailImage='', path='' @staticmethod def addContextMenuItems(items, replaceItems=False): """A stub implementation for the xbmcgui ListItem class addContextMenuItems() method""" - return @staticmethod def addStreamInfo(stream_type, stream_values): """A stub implementation for the xbmcgui LitItem class addStreamInfo() method""" - return @staticmethod def setArt(key): """A stub implementation for the xbmcgui ListItem class setArt() method""" - return @staticmethod def setContentLookup(enable): """A stub implementation for the xbmcgui ListItem class setContentLookup() method""" - return @staticmethod def setInfo(type, infoLabels): # pylint: disable=redefined-builtin """A stub implementation for the xbmcgui ListItem class setInfo() method""" - return @staticmethod def setIsFolder(isFolder): """A stub implementation for the xbmcgui ListItem class setIsFolder() method""" - return + + @staticmethod + def setLabel2(label): + """A stub implementation for the xbmcgui ListItem class setLabel2() method""" @staticmethod def setMimeType(mimetype): """A stub implementation for the xbmcgui ListItem class setMimeType() method""" - return def setPath(self, path): """A stub implementation for the xbmcgui ListItem class setPath() method""" @@ -249,22 +245,18 @@ def setPath(self, path): @staticmethod def setProperty(key, value): """A stub implementation for the xbmcgui ListItem class setProperty() method""" - return @staticmethod def setProperties(dictionary): """A stub implementation for the xbmcgui ListItem class setProperties() method""" - return @staticmethod def setSubtitles(subtitleFiles): """A stub implementation for the xbmcgui ListItem class setSubtitles() method""" - return @staticmethod def setUniqueIDs(values, defaultrating=None): """A stub implementation for the xbmcgui ListItem class setUniqueIDs() method""" - return class Window: @@ -272,7 +264,6 @@ class Window: def __init__(self, windowId): """A stub constructor for the xbmcgui Window class""" - return None def close(self): """A stub implementation for the xbmcgui Window class close() method""" @@ -295,12 +286,10 @@ def getProperty(key): @staticmethod def setProperty(key, value): """A stub implementation for the xbmcgui Window class setProperty() method""" - return @staticmethod def clearProperty(key): """A stub implementation for the xbmcgui Window class clearProperty() method""" - return def show(self): """A stub implementation for the xbmcgui Window class show() method""" diff --git a/tests/xbmcplugin.py b/tests/xbmcplugin.py index de02e475..4cac29e0 100644 --- a/tests/xbmcplugin.py +++ b/tests/xbmcplugin.py @@ -79,7 +79,7 @@ def addDirectoryItems(handle, listing, length): return True -def addSortMethod(handle, sortMethod): +def addSortMethod(handle, sortMethod, label2Mask='%D'): """A stub implementation of the xbmcplugin addSortMethod() function""" diff --git a/tests/xbmcvfs.py b/tests/xbmcvfs.py index f2ebb1c2..e454e7cd 100644 --- a/tests/xbmcvfs.py +++ b/tests/xbmcvfs.py @@ -3,7 +3,7 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """This file implements the Kodi xbmcvfs module, either using stubs or alternative functionality""" -# pylint: disable=invalid-name +# pylint: disable=invalid-name,too-few-public-methods from __future__ import absolute_import, division, print_function, unicode_literals import os