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)