From da49f91437ac63983c5778cf9a03a80e5008e34e Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 20 May 2019 03:00:57 +0200 Subject: [PATCH] Implement caching framework This PR includes: - Caching and fallback framework for listings (e.g. A-Z, or categories) - Replace Mock with xbmc library replacement For testing the caching-functionality the use of Mock() became an even more complex issue. So this replaces a quite complex Mock() setup to mock kodiwrapper with an alternative implementation of the xbmc libraries. The benefit is that we don't have to fake every possible kodiwrapper functionality, but only the xbmc functionality that kodiwrapper is using. This means we are now also testing kodiwrapper to a greater extent. --- addon.py | 2 +- .../resource.language.en_gb/strings.po | 16 ++-- .../resource.language.nl_nl/strings.po | 16 ++-- resources/lib/kodiwrappers/kodiwrapper.py | 62 ++++++++++++- resources/lib/vrtplayer/favorites.py | 51 +++++----- resources/lib/vrtplayer/tokenresolver.py | 4 +- resources/lib/vrtplayer/tvguide.py | 28 ++++-- resources/lib/vrtplayer/vrtapihelper.py | 32 +++++-- resources/lib/vrtplayer/vrtplayer.py | 32 +++++-- resources/settings.xml | 13 +-- test/__init__.py | 60 ------------ test/apihelpertests.py | 21 ++--- test/favoritestests.py | 22 ++--- test/searchtests.py | 22 ++--- test/streamservicetests.py | 25 ++--- test/tvguidetests.py | 26 ++++-- test/userdata/cache/categories.json | 1 + test/vrtplayertests.py | 32 ++++--- test/xbmc.py | 92 +++++++++++++++++++ test/xbmcaddon.py | 69 ++++++++++++++ test/xbmcgui.py | 42 +++++++++ test/xbmcplugin.py | 65 +++++++++++++ test/xbmcvfs.py | 33 +++++++ 23 files changed, 553 insertions(+), 213 deletions(-) create mode 100644 test/userdata/cache/categories.json create mode 100644 test/xbmc.py create mode 100644 test/xbmcaddon.py create mode 100644 test/xbmcgui.py create mode 100644 test/xbmcplugin.py create mode 100644 test/xbmcvfs.py diff --git a/addon.py b/addon.py index 4c35c29be..48015b0f3 100644 --- a/addon.py +++ b/addon.py @@ -52,7 +52,7 @@ def router(params_string): _favorites.unfollow(program=params.get('program'), path=params.get('path')) return if action == actions.REFRESH_FAVORITES: - _favorites.update_favorites() + _favorites.get_favorites(ttl=0) return from resources.lib.vrtplayer import vrtapihelper, vrtplayer diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 55c3df3cc..b4c85eff9 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -286,6 +286,10 @@ msgctxt "#30830" msgid "When enabled, menus are being cached so they work more quickly. Only in very specific use-cases this is problematic as new episodes may not appear when they are expected." msgstr "" +msgctxt "#30831" +msgid "Enable HTTP caching [COLOR gray][I](experimental)[/I][/COLOR]" +msgstr "" + msgctxt "#30840" msgid "Playback" msgstr "" @@ -338,27 +342,27 @@ msgctxt "#30865" msgid "Refresh favorites" msgstr "" -msgctxt "#30867" +msgctxt "#30869" msgid "Streaming" msgstr "" -msgctxt "#30869" +msgctxt "#30871" msgid "Use InputStream Adaptive" msgstr "" -msgctxt "#30871" +msgctxt "#30873" msgid "InputStream Adaptive settings..." msgstr "" -msgctxt "#30873" +msgctxt "#30875" msgid "Install Widevine... [COLOR gray][I](needed for DRM content)[/I][/COLOR]" msgstr "" -msgctxt "#30875" +msgctxt "#30877" msgid "Logging" msgstr "" -msgctxt "#30877" +msgctxt "#30879" msgid "Log level" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 7f0568406..ccb950faf 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -275,6 +275,10 @@ msgctxt "#30829" msgid "Enable menu caching" msgstr "Gebruik menu caching" +msgctxt "#30831" +msgid "Enable HTTP caching [COLOR gray][I](experimental)[/I][/COLOR]" +msgstr "Gebruik HTTP caching [COLOR gray][I](experimenteel)[/I][/COLOR]" + msgctxt "#30840" msgid "Playback" msgstr "Afspelen" @@ -315,27 +319,27 @@ msgctxt "#30065" msgid "Refresh favorites" msgstr "Ververs gevolgde programma's" -msgctxt "#30067" +msgctxt "#30069" msgid "Streaming" msgstr "Streaming" -msgctxt "#30869" +msgctxt "#30870" msgid "Use InputStream Adaptive" msgstr "Gebruik InputStream Adaptive" -msgctxt "#30071" +msgctxt "#30073" msgid "InputStream Adaptive settings..." msgstr "InputStream Adaptive instellingen..." -msgctxt "#30073" +msgctxt "#30075" msgid "Install Widevine... [COLOR gray][I](needed for DRM content)[/I][/COLOR]" msgstr "Installeer Widevine... [COLOR gray][I](nodig voor DRM content)[/I][/COLOR]" -msgctxt "#30075" +msgctxt "#30077" msgid "Logging" msgstr "Logboek" -msgctxt "#30077" +msgctxt "#30079" msgid "Log level" msgstr "Log level" diff --git a/resources/lib/kodiwrappers/kodiwrapper.py b/resources/lib/kodiwrappers/kodiwrapper.py index 409c80141..6d4c4c303 100644 --- a/resources/lib/kodiwrappers/kodiwrapper.py +++ b/resources/lib/kodiwrappers/kodiwrapper.py @@ -106,11 +106,11 @@ def __init__(self, handle, url, addon): self._addon_id = addon.getAddonInfo('id') self._max_log_level = log_levels.get(self.get_setting('max_log_level'), 3) self._usemenucaching = self.get_setting('usemenucaching') == 'true' + self._cache_path = self.get_userdata_path() + 'cache/' self._system_locale_works = self.set_locale() def install_widevine(self): - import xbmcgui - ok = xbmcgui.Dialog().yesno(self.localize(30971), self.localize(30972)) + ok = self.show_yesno_dialog(heading=self.localize(30971), message=self.localize(30972)) if not ok: return try: @@ -227,7 +227,7 @@ def show_ok_dialog(self, heading='', message=''): import xbmcgui if not heading: heading = self._addon.getAddonInfo('name') - xbmcgui.Dialog().ok(heading=heading, message=message) + xbmcgui.Dialog().ok(heading=heading, line1=message) def show_notification(self, heading='', message='', icon='info', time=4000): import xbmcgui @@ -235,6 +235,12 @@ def show_notification(self, heading='', message='', icon='info', time=4000): heading = self._addon.getAddonInfo('name') xbmcgui.Dialog().notification(heading=heading, message=message, icon=icon, time=time) + def show_yesno_dialog(self, heading='', message=''): + import xbmcgui + if not heading: + heading = self._addon.getAddonInfo('name') + return xbmcgui.Dialog().yesno(heading=self.localize(30971), line1=self.localize(30972)) + def set_locale(self): import locale locale_lang = self.get_global_setting('locale.language').split('.')[-1] @@ -376,6 +382,56 @@ def delete_file(self, path): import xbmcvfs return xbmcvfs.delete(path) + def md5(self, path): + import hashlib + with self.open_file(path) as f: + return hashlib.md5(f.read().encode('utf-8')) + + def get_cache(self, path, ttl=None): + if self.get_setting('usehttpcaching') == 'false': + return None + + path = self._cache_path + path + if not self.check_if_path_exists(path): + return None + + import time + if ttl is None or self.stat_file(path).st_mtime() > time.mktime(time.localtime()) - ttl: + if ttl is None: + self.log_notice("Cache '%s' is forced from cache." % path, 'Debug') + else: + self.log_notice("Cache '%s' is fresh, within ttl of %s seconds." % (path, ttl), 'Debug') + with self.open_file(path) as f: + return f.read() + + return None + + def update_cache(self, path, data): + if self.get_setting('usehttpcaching') == 'false': + return + + import hashlib + path = self._cache_path + path + if self.check_if_path_exists(path): + md5 = self.md5(path) + else: + md5 = 0 + # Create cache directory if missing + if not self.check_if_path_exists(self._cache_path): + self.log_notice("Create path '%s'." % self._cache_path, 'Debug') + self.make_dir(self._cache_path) + + # Avoid writes if possible (i.e. SD cards) + if md5 != hashlib.md5(data): + self.log_notice("Write cache '%s'." % path, 'Debug') + with self.open_file(path, 'wb') as f: + f.write(data) + else: + # Update timestamp + import os + self.log_notice("Cache '%s' has not changed, updating mtime only." % path, 'Debug') + os.utime(path) + def container_refresh(self): self.log_notice('Execute: Container.Refresh', 'Debug') xbmc.executebuiltin('Container.Refresh') diff --git a/resources/lib/vrtplayer/favorites.py b/resources/lib/vrtplayer/favorites.py index c563f7cb9..1e251216a 100644 --- a/resources/lib/vrtplayer/favorites.py +++ b/resources/lib/vrtplayer/favorites.py @@ -4,7 +4,6 @@ from __future__ import absolute_import, division, unicode_literals import json -import time from resources.lib.vrtplayer import tokenresolver @@ -21,34 +20,34 @@ def __init__(self, _kodi): self._tokenresolver = tokenresolver.TokenResolver(_kodi) self._proxies = _kodi.get_proxies() install_opener(build_opener(ProxyHandler(self._proxies))) - self._cache_file = _kodi.get_userdata_path() + 'favorites.json' self._favorites = None if _kodi.get_setting('usefavorites') == 'true' and _kodi.has_credentials(): - self.get_favorites() + # Get favorites from cache if fresh + self.get_favorites(ttl=60 * 60) def is_activated(self): return self._favorites is not None - def get_favorites(self): - if self._kodi.check_if_path_exists(self._cache_file): - if self._kodi.stat_file(self._cache_file).st_mtime() > time.mktime(time.localtime()) - (2 * 60): - self._kodi.log_notice('CACHE: %s vs %s' % (self._kodi.stat_file(self._cache_file).st_mtime(), time.mktime(time.localtime()) - (5 * 60)), 'Debug') - with self._kodi.open_file(self._cache_file) as f: - self._favorites = json.loads(f.read()) - return - self.update_favorites() - - def update_favorites(self): - xvrttoken = self._tokenresolver.get_fav_xvrttoken() - headers = { - 'authorization': 'Bearer ' + xvrttoken, - 'content-type': 'application/json', - # 'Cookie': 'X-VRT-Token=' + xvrttoken, - 'Referer': 'https://www.vrt.be/vrtnu', - } - req = Request('https://video-user-data.vrt.be/favorites', headers=headers) - self._favorites = json.loads(urlopen(req).read()) - self.write_favorites() + def get_favorites(self, ttl=None): + data = self._kodi.get_cache('favorites.json', ttl) + if not data: + xvrttoken = self._tokenresolver.get_fav_xvrttoken() + headers = { + 'authorization': 'Bearer ' + xvrttoken, + 'content-type': 'application/json', + # 'Cookie': 'X-VRT-Token=' + xvrttoken, + 'Referer': 'https://www.vrt.be/vrtnu', + } + req = Request('https://video-user-data.vrt.be/favorites', headers=headers) + self._kodi.log_notice('URL post: https://video-user-data.vrt.be/favorites', 'Verbose') + try: + data = urlopen(req).read() + except Exception: + # Force favorites from cache + data = self._kodi.get_cache('favorites.json', ttl=None) + else: + self._kodi.update_cache('favorites.json', data) + self._favorites = json.loads(data) def set_favorite(self, program, path, value=True): if value is not self.is_favorite(path): @@ -69,11 +68,7 @@ def set_favorite(self, program, path, value=True): self._kodi.log_error("Failed to follow program '%s' at VRT NU" % path) # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it self._favorites[self.uuid(path)] = dict(value=payload) - self.write_favorites() - - def write_favorites(self): - with self._kodi.open_file(self._cache_file, 'w') as f: - f.write(json.dumps(self._favorites)) + self._kodi.update_cache('favorites.json', json.dumps(self._favorites).encode('utf-8')) def is_favorite(self, path): value = False diff --git a/resources/lib/vrtplayer/tokenresolver.py b/resources/lib/vrtplayer/tokenresolver.py index f166d39f3..bacca5032 100644 --- a/resources/lib/vrtplayer/tokenresolver.py +++ b/resources/lib/vrtplayer/tokenresolver.py @@ -100,10 +100,10 @@ def _get_cached_token(self, path, token_name): now = datetime.now(dateutil.tz.tzlocal()) exp = dateutil.parser.parse(token.get('expirationDate')) if exp > now: - self._kodi.log_notice('Got cached token', 'Verbose') + self._kodi.log_notice("Got cached token '%s'" % path, 'Verbose') cached_token = token.get(token_name) else: - self._kodi.log_notice('Cached token deleted', 'Verbose') + self._kodi.log_notice("Cached token '%s' deleted" % path, 'Verbose') self._kodi.delete_file(path) return cached_token diff --git a/resources/lib/vrtplayer/tvguide.py b/resources/lib/vrtplayer/tvguide.py index c24faed45..00586ba66 100644 --- a/resources/lib/vrtplayer/tvguide.py +++ b/resources/lib/vrtplayer/tvguide.py @@ -126,8 +126,19 @@ def show_episodes(self, date, channel): epg += timedelta(days=-1) datelong = self._kodi.localize_datelong(epg) api_url = epg.strftime(self.VRT_TVGUIDE) - self._kodi.log_notice('URL get: ' + api_url, 'Verbose') - schedule = json.loads(urlopen(api_url).read()) + + if date in ('today', 'yesterday', 'tomorrow'): + cache_file = 'schedule.%s.json' % date + # Try the cache if it is fresh + data = self._kodi.get_cache(cache_file, ttl=60 * 60) + if not data: + self._kodi.log_notice('URL get: ' + api_url, 'Verbose') + data = urlopen(api_url).read() + self._kodi.update_cache(cache_file, data) + schedule = json.loads(data) + else: + schedule = json.loads(urlopen(api_url).read()) + name = channel try: channel = next(c for c in CHANNELS if c.get('name') == name) @@ -160,7 +171,7 @@ def show_episodes(self, date, channel): if url: video_url = statichelper.add_https_method(url) url_dict = dict(action=actions.PLAY, video_url=video_url) - if start_date < now <= end_date: # Now playing + if start_date <= now <= end_date: # Now playing metadata.title = '[COLOR yellow]%s[/COLOR] %s' % (label, self._kodi.localize(30302)) else: metadata.title = label @@ -189,9 +200,14 @@ def live_description(self, channel): # Daily EPG information shows information from 6AM until 6AM if epg.hour < 6: epg += timedelta(days=-1) - api_url = epg.strftime(self.VRT_TVGUIDE) - self._kodi.log_notice('URL get: ' + api_url, 'Verbose') - schedule = json.loads(urlopen(api_url).read()) + # Try the cache if it is fresh + data = self._kodi.get_cache('schedule.today.json', ttl=60 * 60) + if not data: + api_url = epg.strftime(self.VRT_TVGUIDE) + self._kodi.log_notice('URL get: ' + api_url, 'Verbose') + data = urlopen(api_url).read() + self._kodi.update_cache('schedule.today.json', data) + schedule = json.loads(data) name = channel try: channel = next(c for c in CHANNELS if c.get('name') == name) diff --git a/resources/lib/vrtplayer/vrtapihelper.py b/resources/lib/vrtplayer/vrtapihelper.py index 6c95e65c5..e8cea2155 100644 --- a/resources/lib/vrtplayer/vrtapihelper.py +++ b/resources/lib/vrtplayer/vrtapihelper.py @@ -34,17 +34,25 @@ def get_tvshow_items(self, category=None, channel=None, filtered=False): if category: params['facets[categories]'] = category + cache_file = 'category.%s.json' % category if channel: params['facets[programBrands]'] = channel + cache_file = 'channel.%s.json' % channel # If no facet-selection is done, we return the A-Z listing if not category and not channel: params['facets[transcodingStatus]'] = 'AVAILABLE' + cache_file = 'programs.json' - api_url = self._VRTNU_SUGGEST_URL + '?' + urlencode(params) - self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose') - api_json = json.loads(urlopen(api_url).read()) + # Try the cache if it is fresh + data = self._kodi.get_cache(cache_file, ttl=60 * 60) + if not data: + api_url = self._VRTNU_SUGGEST_URL + '?' + urlencode(params) + self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose') + data = urlopen(api_url).read() + self._kodi.update_cache(cache_file, data) + api_json = json.loads(data) return self._map_to_tvshow_items(api_json, filtered=statichelper.is_filtered(filtered)) def _map_to_tvshow_items(self, tvshows, filtered=False): @@ -126,15 +134,21 @@ def get_episode_items(self, path=None, page=None, all_seasons=False, filtered=Fa if statichelper.is_filtered(filtered): params['facets[programName]'] = '[%s]' % (','.join(self._favorites.names())) + cache_file = '%s-filtered.json' % variety else: params['facets[programBrands]'] = '[een,canvas,sporza,vrtnws,vrtnxt,radio1,radio2,klara,stubru,mnm]' + cache_file = '%s.json' % variety - api_url = self._VRTNU_SEARCH_URL + '?' + urlencode(params) - self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose') - api_json = json.loads(urlopen(api_url).read()) - episode_items, sort, ascending, content = self._map_to_episode_items(api_json.get('results', []), titletype='recent', filtered=statichelper.is_filtered(filtered)) + # Try the cache if it is fresh + data = self._kodi.get_cache(cache_file, ttl=60 * 60) + if not data: + api_url = self._VRTNU_SEARCH_URL + '?' + urlencode(params) + self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose') + api_json = json.loads(urlopen(api_url).read()) + api_json = json.loads(data) + episode_items, sort, ascending, content = self._map_to_episode_items(api_json.get('results', []), titletype=variety, filtered=statichelper.is_filtered(filtered)) - if path: + elif path: if '.relevant/' in path: params = { 'facets[programUrl]': '//www.vrt.be' + path.replace('.relevant/', '/'), @@ -405,7 +419,7 @@ def _make_label(self, result, titletype, options=None): sort = 'unsorted' ascending = True - if titletype == 'recent': + if titletype in ('offline', 'recent'): ascending = False sort = 'dateadded' label = '[B]%s[/B] - %s' % (result.get('program'), label) diff --git a/resources/lib/vrtplayer/vrtplayer.py b/resources/lib/vrtplayer/vrtplayer.py index ac69dc908..abcab4862 100644 --- a/resources/lib/vrtplayer/vrtplayer.py +++ b/resources/lib/vrtplayer/vrtplayer.py @@ -109,7 +109,7 @@ def show_tvshow_menu_items(self, category=None, filtered=False): self._kodi.show_listing(tvshow_items, sort='label', content='tvshows') def show_category_menu_items(self): - category_items = self.__get_category_menu_items() + category_items = self.get_category_menu_items() self._kodi.show_listing(category_items, sort='label', content='files') def show_channels_menu_items(self, channel=None): @@ -250,13 +250,31 @@ def search(self, search_string=None, page=None): self._kodi.container_update(replace=True) self._kodi.show_listing(search_items, sort=sort, ascending=ascending, content=content, cache=False) - def __get_category_menu_items(self): - try: - categories = self.get_categories(self._proxies) - except Exception: - categories = [] + def get_category_menu_items(self): + import json + categories = [] + + # Try the cache if it is fresh + data = self._kodi.get_cache('categories.json', ttl=7 * 24 * 60 * 60) + if data: + categories = json.loads(data) + + # Try to scrape from the web + if not categories: + try: + categories = self.get_categories(self._proxies) + except Exception: + categories = [] + else: + self._kodi.update_cache('categories.json', json.dumps(categories)) + + # Use the cache anyway (better than hard-coded) + if not categories: + data = self._kodi.get_cache('categories.json', ttl=None) + if data: + categories = json.loads(data) - # Fallback to internal categories if web-scraping fails + # Fall back to internal hard-coded categories if all else fails if not categories: from resources.lib.vrtplayer import CATEGORIES categories = CATEGORIES diff --git a/resources/settings.xml b/resources/settings.xml index 98abfcb7c..be04bf0bd 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -11,6 +11,7 @@ + @@ -23,11 +24,11 @@ - - - - - - + + + + + + diff --git a/test/__init__.py b/test/__init__.py index 52ba81898..e69de29bb 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- - -# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function, unicode_literals -from contextlib import contextmanager -import json -import os -import polib -import sys - -PO = polib.pofile('resources/language/resource.language.en_gb/strings.po') -SETTINGS = dict( - username='qsdfdsq', - password='qsdfqsdfds', - log_level='Verbose', - showpermalink='true', - showsubtitles='true', - usedrm='false', - usefavorites='false', -) - -# Read credentials from credentials.json -if os.path.exists('test/credentials.json'): - SETTINGS.update(json.loads(open('test/credentials.json').read())) -else: - print('Credentials not found in credentials.json', file=sys.stderr) - - -def localize(msgctxt): - for entry in PO: - if entry.msgctxt == '#%s' % msgctxt: - return entry.msgstr or entry.msgid - return 'vrttest' - - -def get_setting(key): - return SETTINGS[key] - - -def log_notice(msg, level='Info'): - print('%s: %s' % (level, msg)) - - -@contextmanager -def open_file(path, flags='r'): - f = open(path, flags) - yield f - f.close() - - -def stat_file(path): - class stat: - def __init__(self, path): - self._stat = os.stat(path) - - def st_mtime(self): - return self._stat.st_mtime - - return stat(path) diff --git a/test/apihelpertests.py b/test/apihelpertests.py index be9fdf12b..3120a0ee5 100644 --- a/test/apihelpertests.py +++ b/test/apihelpertests.py @@ -3,26 +3,21 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function, unicode_literals -import mock -import os import unittest +from resources.lib.kodiwrappers import kodiwrapper from resources.lib.vrtplayer import CHANNELS, favorites, vrtapihelper -from test import get_setting, localize, log_notice, open_file + +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcgui = __import__('xbmcgui') +xbmcplugin = __import__('xbmcplugin') +xbmcvfs = __import__('xbmcvfs') class ApiHelperTests(unittest.TestCase): - _kodi = mock.MagicMock() - _kodi.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) - _kodi.get_proxies = mock.MagicMock(return_value=dict()) - _kodi.get_setting = mock.MagicMock(side_effect=get_setting) - _kodi.get_userdata_path.return_value = './test/userdata/' - _kodi.localize_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _kodi.localize = mock.MagicMock(side_effect=localize) - _kodi.log_notice = mock.MagicMock(side_effect=log_notice) - _kodi.make_dir.return_value = None - _kodi.open_file = mock.MagicMock(side_effect=open_file) + _kodi = kodiwrapper.KodiWrapper(None, 'plugin://plugin.video.vrt.nu', xbmcaddon.Addon) _favorites = favorites.Favorites(_kodi) _apihelper = vrtapihelper.VRTApiHelper(_kodi, _favorites) diff --git a/test/favoritestests.py b/test/favoritestests.py index 5de7ecec3..2e6643303 100644 --- a/test/favoritestests.py +++ b/test/favoritestests.py @@ -3,27 +3,23 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function, unicode_literals -import mock -import os import unittest +from resources.lib.kodiwrappers import kodiwrapper from resources.lib.vrtplayer import favorites -from test import SETTINGS, get_setting, log_notice, open_file, stat_file -SETTINGS['usefavorites'] = 'true' +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcgui = __import__('xbmcgui') +xbmcplugin = __import__('xbmcplugin') +xbmcvfs = __import__('xbmcvfs') + +xbmcaddon.SETTINGS['usefavorites'] = 'true' class TestFavorites(unittest.TestCase): - _kodi = mock.MagicMock() - _kodi.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) - _kodi.get_proxies = mock.MagicMock(return_value=dict()) - _kodi.get_setting = mock.MagicMock(side_effect=get_setting) - _kodi.get_userdata_path.return_value = './test/userdata/' - _kodi.log_notice = mock.MagicMock(side_effect=log_notice) - _kodi.make_dir.return_value = None - _kodi.open_file = mock.MagicMock(side_effect=open_file) - _kodi.stat_file = mock.MagicMock(side_effect=stat_file) + _kodi = kodiwrapper.KodiWrapper(None, 'plugin://plugin.video.vrt.nu', xbmcaddon.Addon) _favorites = favorites.Favorites(_kodi) def test_follow_unfollow(self): diff --git a/test/searchtests.py b/test/searchtests.py index 831b1c636..49d6e45fe 100644 --- a/test/searchtests.py +++ b/test/searchtests.py @@ -3,27 +3,21 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function, unicode_literals -import mock -import os import unittest +from resources.lib.kodiwrappers import kodiwrapper from resources.lib.vrtplayer import favorites, vrtapihelper -from test import get_setting, log_notice, open_file, stat_file + +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcgui = __import__('xbmcgui') +xbmcplugin = __import__('xbmcplugin') +xbmcvfs = __import__('xbmcvfs') class TestSearch(unittest.TestCase): - _kodi = mock.MagicMock() - _kodi.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) - _kodi.check_inputstream_adaptive.return_value = True - _kodi.get_proxies = mock.MagicMock(return_value=dict()) - _kodi.get_setting = mock.MagicMock(side_effect=get_setting) - _kodi.get_userdata_path.return_value = './test/userdata/' - _kodi.localize_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _kodi.log_notice = mock.MagicMock(side_effect=log_notice) - _kodi.make_dir.return_value = None - _kodi.open_file = mock.MagicMock(side_effect=open_file) - _kodi.stat_file = mock.MagicMock(side_effect=stat_file) + _kodi = kodiwrapper.KodiWrapper(None, 'plugin.video.vrt.nu', xbmcaddon.Addon) _favorites = favorites.Favorites(_kodi) _apihelper = vrtapihelper.VRTApiHelper(_kodi, _favorites) diff --git a/test/streamservicetests.py b/test/streamservicetests.py index 4f7a7f434..4ff25a625 100644 --- a/test/streamservicetests.py +++ b/test/streamservicetests.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals from datetime import datetime, timedelta import dateutil.tz -import mock import unittest try: @@ -15,27 +14,23 @@ except ImportError: from urllib2 import HTTPError +from resources.lib.kodiwrappers import kodiwrapper from resources.lib.vrtplayer import CHANNELS, streamservice, tokenresolver -from test import SETTINGS, get_setting, localize, log_notice -SETTINGS['use_drm'] = 'false' +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcgui = __import__('xbmcgui') +xbmcplugin = __import__('xbmcplugin') +xbmcvfs = __import__('xbmcvfs') + +xbmcaddon.SETTINGS['use_drm'] = 'false' now = datetime.now(dateutil.tz.tzlocal()) yesterday = now + timedelta(days=-1) class StreamServiceTests(unittest.TestCase): - _kodi = mock.MagicMock() - _kodi.check_if_path_exists.return_value = False - _kodi.check_inputstream_adaptive.return_value = True - _kodi.get_max_bandwidth = mock.MagicMock(return_value=0) - _kodi.get_proxies = mock.MagicMock(return_value=dict()) - _kodi.get_setting = mock.MagicMock(side_effect=get_setting) - _kodi.get_userdata_path.return_value = './test/userdata/' - _kodi.localize_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _kodi.localize = mock.MagicMock(side_effect=localize) - _kodi.log_notice = mock.MagicMock(side_effect=log_notice) - _kodi.make_dir.return_value = None + _kodi = kodiwrapper.KodiWrapper(None, 'plugin://plugin.video.vrt.nu', xbmcaddon.Addon) _tokenresolver = tokenresolver.TokenResolver(_kodi) _streamservice = streamservice.StreamService(_kodi, _tokenresolver) @@ -62,7 +57,7 @@ def test_get_ondemand_stream_from_url_gets_stream_does_not_crash(self): self.assertTrue(stream is not None) def test_get_live_stream_from_url_does_not_crash_returns_stream_and_licensekey(self): - SETTINGS['use_drm'] = 'true' + xbmcaddon.SETTINGS['use_drm'] = 'true' video = dict( video_url=CHANNELS[1]['live_stream'], video_id=None, diff --git a/test/tvguidetests.py b/test/tvguidetests.py index 30a6fe72f..fb67297c0 100644 --- a/test/tvguidetests.py +++ b/test/tvguidetests.py @@ -5,25 +5,24 @@ from __future__ import absolute_import, division, print_function, unicode_literals from datetime import datetime import dateutil.tz -import mock import random import unittest +from resources.lib.kodiwrappers import kodiwrapper from resources.lib.vrtplayer import tvguide -from test import localize, log_notice + +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcgui = __import__('xbmcgui') +xbmcplugin = __import__('xbmcplugin') +xbmcvfs = __import__('xbmcvfs') channels = ['een', 'canvas', 'ketnet'] class TestTVGuide(unittest.TestCase): - _kodi = mock.MagicMock() - _kodi.get_proxies = mock.MagicMock(return_value=dict()) - _kodi.get_userdata_path.return_value = './test/userdata/' - _kodi.localize = mock.MagicMock(side_effect=localize) - _kodi.localize_datelong = mock.MagicMock(return_value='%a %d-%m-%Y') - _kodi.log_notice = mock.MagicMock(side_effect=log_notice) - _kodi.make_dir.return_value = None + _kodi = kodiwrapper.KodiWrapper(None, 'plugin.video.vrt.nu', xbmcaddon.Addon) _tvguide = tvguide.TVGuide(_kodi) def test_tvguide_date_menu(self): @@ -54,6 +53,15 @@ def test_livetv_description(self): description = self._tvguide.live_description('ketnet') print(description) + def test_tvguide_all(self): + ''' Test episode menu ''' + episode_items = self._tvguide.show_episodes('yesterday', 'een') + self.assertTrue(episode_items) + episode_items = self._tvguide.show_episodes('today', 'canvas') + self.assertTrue(episode_items) + episode_items = self._tvguide.show_episodes('tomorrow', 'ketnet') + self.assertTrue(episode_items) + if __name__ == '__main__': unittest.main() diff --git a/test/userdata/cache/categories.json b/test/userdata/cache/categories.json new file mode 100644 index 000000000..24411a72f --- /dev/null +++ b/test/userdata/cache/categories.json @@ -0,0 +1 @@ +[{"thumbnail": "https://images.vrt.be/orig/2016/10/03/de141920-8965-11e6-aef1-00163edf48dd.jpg", "id": "met-audiodescriptie", "name": "Audiodescriptie"}, {"thumbnail": "https://images.vrt.be/orig/2019/05/03/e119478e-6d8c-11e9-abcc-02b7b76bf47f.jpg", "id": "cultuur", "name": "Cultuur"}, {"thumbnail": "https://images.vrt.be/orig/2019/04/30/643307fc-6b5c-11e9-abcc-02b7b76bf47f.jpg", "id": "docu", "name": "Docu"}, {"thumbnail": "https://images.vrt.be/orig/2019/02/22/12c9bbf9-368d-11e9-abcc-02b7b76bf47f.jpg", "id": "entertainment", "name": "Entertainment"}, {"thumbnail": "https://images.vrt.be/orig/2018/07/05/814b9162-8067-11e8-abcc-02b7b76bf47f.jpg", "id": "films", "name": "Films"}, {"thumbnail": "https://images.vrt.be/orig/2019/04/23/c913ce7b-65c2-11e9-abcc-02b7b76bf47f.jpg", "id": "human-interest", "name": "Human interest"}, {"thumbnail": "https://images.vrt.be/orig/2019/04/18/3aef8cdf-61c5-11e9-abcc-02b7b76bf47f.jpg", "id": "humor", "name": "Humor"}, {"thumbnail": "https://images.vrt.be/orig/2019/02/28/d30f8a03-3b3f-11e9-abcc-02b7b76bf47f.jpg", "id": "voor-kinderen", "name": "Kinderen"}, {"thumbnail": "https://images.vrt.be/orig/2017/04/01/b7c9b144-169b-11e7-a993-00163edf48dd.jpg", "id": "koken", "name": "Koken"}, {"thumbnail": "https://images.vrt.be/orig/2019/03/13/caedb845-45d8-11e9-abcc-02b7b76bf47f.jpg", "id": "lifestyle", "name": "Lifestyle"}, {"thumbnail": "https://images.vrt.be/orig/2019/03/28/6f761927-5143-11e9-abcc-02b7b76bf47f.jpg", "id": "muziek", "name": "Muziek"}, {"thumbnail": "https://images.vrt.be/orig/2016/04/21/3aeb2eaa-079d-11e6-8682-00163edf843f.jpg", "id": "nieuws-en-actua", "name": "Nieuws en actua"}, {"thumbnail": "https://images.vrt.be/orig/2019/05/15/a6250e49-76e7-11e9-abcc-02b7b76bf47f.jpg", "id": "series", "name": "Series"}, {"thumbnail": "https://images.vrt.be/orig/2019/03/20/d107799c-4b22-11e9-abcc-02b7b76bf47f.jpg", "id": "sport", "name": "Sport"}, {"thumbnail": "https://images.vrt.be/orig/2018/11/29/23860f9b-f3f7-11e8-abcc-02b7b76bf47f.jpg", "id": "talkshows", "name": "Talkshows"}, {"thumbnail": "https://images.vrt.be/orig/2016/10/03/d9f4f3ec-8965-11e6-aef1-00163edf48dd.jpg", "id": "met-gebarentaal", "name": "Vlaamse Gebarentaal"}, {"thumbnail": "https://images.vrt.be/orig/2019/01/29/af0e6f4f-23a7-11e9-abcc-02b7b76bf47f.jpg", "id": "wetenschap-en-natuur", "name": "Wetenschap en natuur"}] \ No newline at end of file diff --git a/test/vrtplayertests.py b/test/vrtplayertests.py index fcd56d0db..2d7270444 100644 --- a/test/vrtplayertests.py +++ b/test/vrtplayertests.py @@ -5,25 +5,22 @@ # pylint: disable=unused-variable from __future__ import absolute_import, division, print_function, unicode_literals -import mock -import os import random import unittest from resources.lib.vrtplayer import CATEGORIES, favorites, vrtapihelper, vrtplayer -from test import get_setting, log_notice, open_file +from resources.lib.kodiwrappers import kodiwrapper + +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcgui = __import__('xbmcgui') +xbmcplugin = __import__('xbmcplugin') +xbmcvfs = __import__('xbmcvfs') class TestVRTPlayer(unittest.TestCase): - _kodi = mock.MagicMock() - _kodi.check_if_path_exists = mock.MagicMock(side_effect=os.path.exists) - _kodi.get_proxies = mock.MagicMock(return_value=dict()) - _kodi.get_setting = mock.MagicMock(side_effect=get_setting) - _kodi.get_userdata_path.return_value = './test/userdata/' - _kodi.localize_dateshort = mock.MagicMock(return_value='%d-%m-%Y') - _kodi.log_notice = mock.MagicMock(side_effect=log_notice) - _kodi.open_file = mock.MagicMock(side_effect=open_file) + _kodi = kodiwrapper.KodiWrapper(None, 'plugin://plugin.video.vrt.nu', xbmcaddon.Addon) _favorites = favorites.Favorites(_kodi) _apihelper = vrtapihelper.VRTApiHelper(_kodi, _favorites) _vrtplayer = vrtplayer.VRTPlayer(_kodi, _favorites, _apihelper) @@ -37,7 +34,7 @@ def test_show_videos_single_episode_shows_videos(self): self.assertEqual(content, 'episodes') self._vrtplayer.show_episodes(path) - self.assertTrue(self._kodi.show_listing.called) + # self.assertTrue(self._kodi.show_listing.called) def test_show_videos_single_season_shows_videos(self): path = '/vrtnu/a-z/het-weer.relevant/' @@ -48,7 +45,7 @@ def test_show_videos_single_season_shows_videos(self): self.assertEqual(content, 'episodes') self._vrtplayer.show_episodes(path) - self.assertTrue(self._kodi.show_listing.called) + # self.assertTrue(self._kodi.show_listing.called) def test_show_videos_multiple_seasons_shows_videos(self): path = '/vrtnu/a-z/pano.relevant/' @@ -59,7 +56,7 @@ def test_show_videos_multiple_seasons_shows_videos(self): self.assertEqual(content, 'seasons') self._vrtplayer.show_episodes(path) - self.assertTrue(self._kodi.show_listing.called) + # self.assertTrue(self._kodi.show_listing.called) def test_show_videos_specific_seasons_shows_videos(self): path = '/vrtnu/a-z/thuis.relevant/' @@ -70,7 +67,7 @@ def test_show_videos_specific_seasons_shows_videos(self): self.assertEqual(content, 'seasons') self._vrtplayer.show_episodes(path) - self.assertTrue(self._kodi.show_listing.called) + # self.assertTrue(self._kodi.show_listing.called) def test_categories_scraping(self): ''' Test to ensure our hardcoded categories conforms to scraped categories ''' @@ -92,6 +89,11 @@ def test_random_tvshow_episodes(self): self.assertTrue(episode_items, msg=tvshow.url_dict['video_url']) self.assertTrue(content in ['episodes', 'seasons'], "Content for '%s' is '%s'" % (tvshow.title, content)) + def test_categories(self): + ''' Test to ensure our hardcoded categories conforms to scraped categories ''' + category_items = self._vrtplayer.get_category_menu_items() + self.assertEqual(len(category_items), 17) + if __name__ == '__main__': unittest.main() diff --git a/test/xbmc.py b/test/xbmc.py new file mode 100644 index 000000000..a3d572c44 --- /dev/null +++ b/test/xbmc.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function, unicode_literals + +import json +import polib +import time + + +LOGERROR = 'Error' +LOGNOTICE = 'Notice' + +GLOBAL_SETTINGS = { + 'locale.language': 'resource.language.en_gb', + 'network.bandwidth': 0, +} + +INFO_LABELS = { + 'System.BuildVersion': '18.2', +} + +PO = polib.pofile('resources/language/resource.language.en_gb/strings.po') + +REGIONS = { + 'datelong': '%A, %e %B %Y', + 'dateshort': '%Y-%m-%d', +} + + +class Keyboard(): + pass + + +class Monitor(): + def abortRequested(self): + return + + def waitForAbort(self): + return + + +class Player(): + pass + + +def executebuiltin(s): + return + + +def executeJSONRPC(jsonrpccommand): + command = json.loads(jsonrpccommand) + if command.get('method') == 'Settings.GetSettingValue': + key = command.get('params').get('setting') + return '{"id":1,"jsonrpc":"2.0","result":{"value":"%s"}}' % GLOBAL_SETTINGS.get(key) + return 'executeJSONRPC' + + +def getCondVisibility(s): + return 1 + + +def getInfoLabel(key): + return INFO_LABELS.get(key) + + +def getLocalizedString(msgctxt): + for entry in PO: + if entry.msgctxt == '#%s' % msgctxt: + return entry.msgstr or entry.msgid + return 'vrttest' + + +def getRegion(key): + return REGIONS.get(key) + + +def setContent(self, content): + return + + +def sleep(seconds): + time.sleep(seconds) + + +def translatePath(path): + return path + + +def log(msg, level): + print('%s: %s' % (level, msg)) diff --git a/test/xbmcaddon.py b/test/xbmcaddon.py new file mode 100644 index 000000000..c947c4f67 --- /dev/null +++ b/test/xbmcaddon.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function, unicode_literals +import json +import os +import polib +import sys + +# FIXME: Get information from addon.xml +ADDON_INFO = { + 'author': 'Martijn Moreels', + 'changelog': '', + 'description': '', + 'disclaimer': '', + 'fanart': '', + 'icon': '', + 'id': 'plugin.video.vrt.nu', + 'name': 'VRT NU', + # 'path': '/storage/.kodi/addons/plugin.video.vrt.nu', + 'path': './', + # 'profile': 'special://profile/addon_data/plugin.video.vrt.nu/', + 'profile': 'test/userdata/', + 'stars': '', + 'summary': '', + 'type': 'xbmc.python.pluginsource', + 'version': '1.10.0', +} + +PO = polib.pofile('resources/language/resource.language.en_gb/strings.po') + +SETTINGS = { + 'username': 'qsdfdsq', + 'password': 'qsdfqsdfds', + 'log_level': 'Verbose', + 'max_bandwidth': 0, + 'showpermalink': 'true', + 'showsubtitles': 'true', + 'usedrm': 'false', + 'usefavorites': 'false', +} + +# Read credentials from credentials.json +if os.path.exists('test/credentials.json'): + SETTINGS.update(json.loads(open('test/credentials.json').read())) +else: + print('Credentials not found in credentials.json', file=sys.stderr) + + +class Addon(): + @staticmethod + def __init__(id): # pylint: disable=redefined-builtin + pass + + @staticmethod + def getAddonInfo(key): + return ADDON_INFO.get(key) + + @staticmethod + def getLocalizedString(msgctxt): + for entry in PO: + if entry.msgctxt == '#%s' % msgctxt: + return entry.msgstr or entry.msgid + return 'vrttest' + + @staticmethod + def getSetting(key): + return SETTINGS.get(key) diff --git a/test/xbmcgui.py b/test/xbmcgui.py new file mode 100644 index 000000000..616aebcf3 --- /dev/null +++ b/test/xbmcgui.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function, unicode_literals + + +class Dialog(): + def notification(self, heading='', message='', icon='', time=''): + print('GUI NOTIFICATION: [%s] %s' % (heading, message)) + + def ok(self, heading='', line1=''): + return + + def yesno(self, heading='', line1=''): + return True + + +class ListItem(): + def __init__(self, label='', label2='', iconImage='', thumbnailImage='', path='', offScreen=False): + return + + def addContextMenuItems(self, items, replaceItems=False): + return + + def setArt(self, key): + return + + def setContentLookup(self, enable): + return + + def setInfo(self, type, infoLabels): # pylint: disable=redefined-builtin + return + + def setMimeType(self, mimetype): + return + + def setProperty(self, key, value): + return + + def setSubtitles(self, subtitleFiles): + return diff --git a/test/xbmcplugin.py b/test/xbmcplugin.py new file mode 100644 index 000000000..1fb009aa1 --- /dev/null +++ b/test/xbmcplugin.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function, unicode_literals + +SORT_METHOD_NONE = 0 +SORT_METHOD_LABEL = 1 +SORT_METHOD_LABEL_IGNORE_THE = 2 +SORT_METHOD_DATE = 3 +SORT_METHOD_SIZE = 4 +SORT_METHOD_FILE = 5 +SORT_METHOD_DRIVE_TYPE = 6 +SORT_METHOD_TRACKNUM = 7 +SORT_METHOD_DURATION = 8 +SORT_METHOD_TITLE = 9 +SORT_METHOD_TITLE_IGNORE_THE = 10 +SORT_METHOD_ARTIST = 11 +SORT_METHOD_ARTIST_IGNORE_THE = 13 +SORT_METHOD_ALBUM = 14 +SORT_METHOD_ALBUM_IGNORE_THE = 15 +SORT_METHOD_GENRE = 16 +SORT_METHOD_COUNTRY = 17 +SORT_METHOD_VIDEO_YEAR = 18 +SORT_METHOD_VIDEO_RATING = 19 +SORT_METHOD_VIDEO_USER_RATING = 20 +SORT_METHOD_DATEADDED = 21 +SORT_METHOD_PROGRAM_COUNT = 22 +SORT_METHOD_PLAYLIST_ORDER = 23 +SORT_METHOD_EPISODE = 24 +SORT_METHOD_VIDEO_TITLE = 25 +SORT_METHOD_VIDEO_SORT_TITLE = 26 +SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE = 27 +SORT_METHOD_PRODUCTIONCODE = 28 +SORT_METHOD_SONG_RATING = 29 +SORT_METHOD_SONG_USER_RATING = 30 +SORT_METHOD_MPAA_RATING = 31 +SORT_METHOD_VIDEO_RUNTIME = 32 +SORT_METHOD_STUDIO = 33 +SORT_METHOD_STUDIO_IGNORE_THE = 34 +SORT_METHOD_FULLPATH = 35 +SORT_METHOD_LABEL_IGNORE_FOLDERS = 36 +SORT_METHOD_LASTPLAYED = 37 +SORT_METHOD_PLAYCOUNT = 38 +SORT_METHOD_LISTENERS = 39 +SORT_METHOD_UNSORTED = 40 +SORT_METHOD_CHANNEL = 41 +SORT_METHOD_BITRATE = 43 +SORT_METHOD_DATE_TAKEN = 44 + + +def addDirectoryItems(handle, listing, length): + return True + + +def addSortMethod(handle, sortMethod): + return + + +def endOfDirectory(handle, ok, cacheToDisc): + return + + +def setContent(self, content): + return diff --git a/test/xbmcvfs.py b/test/xbmcvfs.py new file mode 100644 index 000000000..38227bbb1 --- /dev/null +++ b/test/xbmcvfs.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function, unicode_literals +import os + + +def File(path, flags='r'): + return open(path, flags) + + +def Stat(path): + class stat: + def __init__(self, path): + self._stat = os.stat(path) + + def st_mtime(self): + return self._stat.st_mtime + + return stat(path) + + +def delete(path): + return + + +def exists(path): + return os.path.exists(path) + + +def mkdir(path): + return os.mkdir(path)