diff --git a/addon.py b/addon.py
index 4c35c29be..c42ed4dbb 100644
--- a/addon.py
+++ b/addon.py
@@ -29,6 +29,9 @@ def router(params_string):
_kodi = kodiwrapper.KodiWrapper(_ADDON_HANDLE, _ADDON_URL, addon)
_kodi.log_access(_ADDON_URL, params_string)
+ if action == actions.INVALIDATE_CACHES:
+ _kodi.invalidate_caches()
+ return
if action == actions.CLEAR_COOKIES:
from resources.lib.vrtplayer import tokenresolver
_tokenresolver = tokenresolver.TokenResolver(_kodi)
@@ -52,7 +55,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/pylintrc b/pylintrc
index 4fb4d82b5..3a9a79506 100644
--- a/pylintrc
+++ b/pylintrc
@@ -12,12 +12,13 @@ disable=
no-self-use,
old-style-class,
too-few-public-methods,
- too-many-public-methods,
too-many-arguments,
too-many-branches,
too-many-function-args,
too-many-instance-attributes,
too-many-locals,
+ too-many-public-methods,
+ too-many-return-statements,
too-many-statements,
unnecessary-lambda,
unused-argument,
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
index 55c3df3cc..4cff0085f 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 ""
@@ -339,26 +343,30 @@ msgid "Refresh favorites"
msgstr ""
msgctxt "#30867"
-msgid "Streaming"
+msgid "Invalidate HTTP caches"
msgstr ""
msgctxt "#30869"
-msgid "Use InputStream Adaptive"
+msgid "Streaming"
msgstr ""
msgctxt "#30871"
-msgid "InputStream Adaptive settings..."
+msgid "Use InputStream Adaptive"
msgstr ""
msgctxt "#30873"
-msgid "Install Widevine... [COLOR gray][I](needed for DRM content)[/I][/COLOR]"
+msgid "InputStream Adaptive settings..."
msgstr ""
msgctxt "#30875"
-msgid "Logging"
+msgid "Install Widevine... [COLOR gray][I](needed for DRM content)[/I][/COLOR]"
msgstr ""
msgctxt "#30877"
+msgid "Logging"
+msgstr ""
+
+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..7db4c6462 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,31 @@ msgctxt "#30065"
msgid "Refresh favorites"
msgstr "Ververs gevolgde programma's"
-msgctxt "#30067"
+msgctxt "#30867"
+msgid "Invalidate HTTP caches"
+msgstr "Invalideer HTTP caches"
+
+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..29fee5e65 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]
@@ -243,7 +249,7 @@ def set_locale(self):
locale.setlocale(locale.LC_ALL, locale_lang)
return True
except Exception as e:
- self.log_notice(e, 'Debug')
+ self.log_notice('Failed to set locale: %s' % e, 'Debug')
return False
def localize(self, string_id):
@@ -353,9 +359,19 @@ def get_addon_path(self):
def get_path(self, path):
return xbmc.translatePath(path)
- def make_dir(self, path):
+ def listdir(self, path):
+ import xbmcvfs
+ return xbmcvfs.listdir(path)
+
+ def mkdir(self, path):
+ import xbmcvfs
+ self.log_notice("Create directory '%s'." % path, 'Debug')
+ return xbmcvfs.mkdir(path)
+
+ def mkdirs(self, path):
import xbmcvfs
- xbmcvfs.mkdir(path)
+ self.log_notice("Recursively create directory '%s'." % path, 'Debug')
+ return xbmcvfs.mkdirs(path)
def check_if_path_exists(self, path):
import xbmcvfs
@@ -374,8 +390,72 @@ def stat_file(self, path):
def delete_file(self, path):
import xbmcvfs
+ self.log_notice("Delete file '%s'." % path, 'Debug')
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:
+ import json
+ 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, 'r') as f:
+ try:
+ return json.load(f, encoding='utf-8')
+ except ValueError:
+ return None
+
+ return None
+
+ def update_cache(self, path, data):
+ if self.get_setting('usehttpcaching') == 'false':
+ return
+
+ import hashlib
+ import json
+ 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.mkdirs(self._cache_path)
+
+ # Avoid writes if possible (i.e. SD cards)
+ if md5 != hashlib.md5(json.dumps(data, encoding='utf-8')):
+ self.log_notice("Write cache '%s'." % path, 'Debug')
+ with self.open_file(path, 'w') as f:
+ json.dump(data, f, encoding='utf-8')
+ else:
+ # Update timestamp
+ import os
+ self.log_notice("Cache '%s' has not changed, updating mtime only." % path, 'Debug')
+ os.utime(path)
+
+ def invalidate_cache(self, path):
+ self.delete_file(self._cache_path + path)
+
+ def invalidate_caches(self):
+ _, files = self.listdir(self._cache_path)
+ for f in files:
+ self.delete_file(self._cache_path + f)
+
def container_refresh(self):
self.log_notice('Execute: Container.Refresh', 'Debug')
xbmc.executebuiltin('Container.Refresh')
diff --git a/resources/lib/vrtplayer/__init__.py b/resources/lib/vrtplayer/__init__.py
index 22c479783..476332190 100644
--- a/resources/lib/vrtplayer/__init__.py
+++ b/resources/lib/vrtplayer/__init__.py
@@ -119,6 +119,7 @@ class actions:
CLEAR_COOKIES = 'clearcookies'
FOLLOW = 'follow'
INSTALL_WIDEVINE = 'installwidevine'
+ INVALIDATE_CACHES = 'invalidatecaches'
LISTING_ALL_EPISODES = 'listingallepisodes'
LISTING_AZ_TVSHOWS = 'listingaztvshows'
LISTING_CATEGORIES = 'listingcategories'
diff --git a/resources/lib/vrtplayer/favorites.py b/resources/lib/vrtplayer/favorites.py
index c563f7cb9..741cbc87a 100644
--- a/resources/lib/vrtplayer/favorites.py
+++ b/resources/lib/vrtplayer/favorites.py
@@ -3,8 +3,6 @@
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, unicode_literals
-import json
-import time
from resources.lib.vrtplayer import tokenresolver
@@ -21,36 +19,38 @@ 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):
+ import json
+ api_json = self._kodi.get_cache('favorites.json', ttl)
+ if not api_json:
+ 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:
+ api_json = json.load(urlopen(req))
+ except Exception:
+ # Force favorites from cache
+ api_json = self._kodi.get_cache('favorites.json', ttl=None)
+ else:
+ self._kodi.update_cache('favorites.json', api_json)
+ self._favorites = api_json
def set_favorite(self, program, path, value=True):
+ import json
if value is not self.is_favorite(path):
xvrttoken = self._tokenresolver.get_fav_xvrttoken()
headers = {
@@ -69,11 +69,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', self._favorites)
def is_favorite(self, path):
value = False
@@ -103,3 +99,7 @@ def names(self):
def titles(self):
return [p.get('value').get('title') for p in self._favorites.values() if p.get('value').get('isFavorite')]
+
+ def invalidate_cache(self):
+ self._kodi.invalidate_cache('offline-filtered.json')
+ self._kodi.invalidate_cache('recent-filtered.json')
diff --git a/resources/lib/vrtplayer/streamservice.py b/resources/lib/vrtplayer/streamservice.py
index 191c737d0..47cc6862b 100644
--- a/resources/lib/vrtplayer/streamservice.py
+++ b/resources/lib/vrtplayer/streamservice.py
@@ -34,13 +34,13 @@ def __init__(self, _kodi, _tokenresolver):
self._vualto_license_url = None
def _get_vualto_license_url(self):
- self._vualto_license_url = json.loads(urlopen(self._VUPLAY_API_URL).read()).get('drm_providers', dict()).get('widevine', dict()).get('la_url')
+ self._vualto_license_url = json.load(urlopen(self._VUPLAY_API_URL)).get('drm_providers', dict()).get('widevine', dict()).get('la_url')
self._kodi.log_notice('URL get: ' + unquote(self._VUPLAY_API_URL), 'Verbose')
def _create_settings_dir(self):
settingsdir = self._kodi.get_userdata_path()
if not self._kodi.check_if_path_exists(settingsdir):
- self._kodi.make_dir(settingsdir)
+ self._kodi.mkdir(settingsdir)
def _get_license_key(self, key_url, key_type='R', key_headers=None, key_value=None):
''' Generates a propery license key value
@@ -152,7 +152,7 @@ def _get_stream_json(self, api_data):
api_data.video_id + '?vrtPlayerToken=' + playertoken + '&client=' + api_data.client
self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose')
try:
- stream_json = json.loads(urlopen(api_url).read())
+ stream_json = json.load(urlopen(api_url))
except HTTPError as e:
stream_json = json.loads(e.read())
diff --git a/resources/lib/vrtplayer/tokenresolver.py b/resources/lib/vrtplayer/tokenresolver.py
index f166d39f3..52af2eae4 100644
--- a/resources/lib/vrtplayer/tokenresolver.py
+++ b/resources/lib/vrtplayer/tokenresolver.py
@@ -82,7 +82,7 @@ def _get_new_playertoken(self, path, token_url, headers):
import json
self._kodi.log_notice('URL post: ' + unquote(token_url), 'Verbose')
req = Request(token_url, data=b'', headers=headers)
- playertoken = json.loads(urlopen(req).read())
+ playertoken = json.load(urlopen(req))
with self._kodi.open_file(path, 'w') as f:
json.dump(playertoken, f)
return playertoken.get('vrtPlayerToken')
@@ -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
@@ -125,7 +125,7 @@ def _get_new_xvrttoken(self, path, get_roaming_token):
data = urlencode(payload).encode('utf8')
self._kodi.log_notice('URL post: ' + unquote(self._LOGIN_URL), 'Verbose')
req = Request(self._LOGIN_URL, data=data)
- logon_json = json.loads(urlopen(req).read())
+ logon_json = json.load(urlopen(req))
token = None
if logon_json.get('errorCode') != 0:
@@ -179,7 +179,7 @@ def _get_fav_xvrttoken(self, path):
data = urlencode(payload).encode('utf8')
self._kodi.log_notice('URL post: ' + unquote(self._LOGIN_URL), 'Verbose')
req = Request(self._LOGIN_URL, data=data)
- logon_json = json.loads(urlopen(req).read())
+ logon_json = json.load(urlopen(req))
token = None
if logon_json.get('errorCode') != 0:
diff --git a/resources/lib/vrtplayer/tvguide.py b/resources/lib/vrtplayer/tvguide.py
index c24faed45..da4c28685 100644
--- a/resources/lib/vrtplayer/tvguide.py
+++ b/resources/lib/vrtplayer/tvguide.py
@@ -126,8 +126,18 @@ 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
+ schedule = self._kodi.get_cache(cache_file, ttl=60 * 60)
+ if not schedule:
+ self._kodi.log_notice('URL get: ' + api_url, 'Verbose')
+ schedule = json.load(urlopen(api_url))
+ self._kodi.update_cache(cache_file, schedule)
+ else:
+ schedule = json.load(urlopen(api_url))
+
name = channel
try:
channel = next(c for c in CHANNELS if c.get('name') == name)
@@ -160,7 +170,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 +199,13 @@ 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
+ schedule = self._kodi.get_cache('schedule.today.json', ttl=60 * 60)
+ if not schedule:
+ api_url = epg.strftime(self.VRT_TVGUIDE)
+ self._kodi.log_notice('URL get: ' + api_url, 'Verbose')
+ schedule = json.load(urlopen(api_url))
+ self._kodi.update_cache('schedule.today.json', schedule)
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..e8b8882f6 100644
--- a/resources/lib/vrtplayer/vrtapihelper.py
+++ b/resources/lib/vrtplayer/vrtapihelper.py
@@ -34,17 +34,24 @@ 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
+ api_json = self._kodi.get_cache(cache_file, ttl=60 * 60)
+ if not api_json:
+ api_url = self._VRTNU_SUGGEST_URL + '?' + urlencode(params)
+ self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose')
+ api_json = json.load(urlopen(api_url))
+ self._kodi.update_cache(cache_file, api_json)
return self._map_to_tvshow_items(api_json, filtered=statichelper.is_filtered(filtered))
def _map_to_tvshow_items(self, tvshows, filtered=False):
@@ -126,15 +133,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
+ api_json = self._kodi.get_cache(cache_file, ttl=60 * 60)
+ if not api_json:
+ api_url = self._VRTNU_SEARCH_URL + '?' + urlencode(params)
+ self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose')
+ api_json = json.load(urlopen(api_url))
+ self._kodi.update_cache(cache_file, api_json)
+ 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/', '/'),
@@ -146,7 +159,7 @@ def get_episode_items(self, path=None, page=None, all_seasons=False, filtered=Fa
api_url = path
self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose')
- api_json = json.loads(urlopen(api_url).read())
+ api_json = json.load(urlopen(api_url))
episodes = api_json.get('results', [{}])
if episodes:
@@ -359,7 +372,7 @@ def search(self, search_string, page=0):
}
api_url = 'https://search.vrt.be/search?' + urlencode(params)
self._kodi.log_notice('URL get: ' + unquote(api_url), 'Verbose')
- api_json = json.loads(urlopen(api_url).read())
+ api_json = json.load(urlopen(api_url))
episodes = api_json.get('results', [{}])
episode_items, sort, ascending, content = self._map_to_episode_items(episodes, titletype='recent')
@@ -405,7 +418,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..733584b2c 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,26 @@ 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):
+ categories = []
+
+ # Try the cache if it is fresh
+ categories = self._kodi.get_cache('categories.json', ttl=7 * 24 * 60 * 60)
+
+ # 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', categories)
+
+ # Use the cache anyway (better than hard-coded)
+ if not categories:
+ categories = self._kodi.get_cache('categories.json', ttl=None)
- # 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..16a106207 100644
--- a/resources/settings.xml
+++ b/resources/settings.xml
@@ -11,6 +11,7 @@
+
@@ -23,11 +24,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
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/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..08c18d8dc
--- /dev/null
+++ b/test/xbmcvfs.py
@@ -0,0 +1,48 @@
+# -*- 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 os.remove(path)
+
+
+def exists(path):
+ return os.path.exists(path)
+
+
+def listdir(path):
+ files = []
+ dirs = []
+ for f in os.listdir(path):
+ if os.path.isfile(f):
+ files.append(f)
+ if os.path.isdir(f):
+ dirs.append(f)
+ return dirs, files
+
+
+def mkdir(path):
+ return os.mkdir(path)
+
+
+def mkdirs(path):
+ return os.makedirs(path)