Rework cache (#15)

* Rework cache

* Cancel at 5%

* Check before removing auth token

* Fix cache refreshing from service
This commit is contained in:
Michaël Arnauts 2020-04-01 11:01:22 +02:00 committed by GitHub
parent 3d7a05cf24
commit 6e9a4718fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 151 additions and 221 deletions

View File

@ -177,8 +177,3 @@ msgstr ""
msgctxt "#30831" msgctxt "#30831"
msgid "Update local metadata now" msgid "Update local metadata now"
msgstr "" msgstr ""
msgctxt "#30833"
msgid "Clear local metadata"
msgstr ""

View File

@ -178,7 +178,3 @@ msgstr "Vernieuw de lokale metdata automatisch in de achtergrond"
msgctxt "#30831" msgctxt "#30831"
msgid "Update local metadata now" msgid "Update local metadata now"
msgstr "De lokale metadata nu vernieuwen" msgstr "De lokale metadata nu vernieuwen"
msgctxt "#30833"
msgid "Clear local metadata"
msgstr "De lokale metadata verwijderen"

View File

@ -111,13 +111,6 @@ def metadata_update():
Metadata().update() Metadata().update()
@routing.route('/metadata/clean')
def metadata_clean():
""" Clear metadata (called from settings) """
from resources.lib.modules.metadata import Metadata
Metadata().clean()
def run(params): def run(params):
""" Run the routing plugin """ """ Run the routing plugin """
routing.run(params) routing.run(params)

View File

@ -4,7 +4,6 @@
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
import logging import logging
from contextlib import contextmanager
import xbmc import xbmc
import xbmcaddon import xbmcaddon
@ -420,54 +419,6 @@ def get_addon_info(key):
return to_unicode(ADDON.getAddonInfo(key)) return to_unicode(ADDON.getAddonInfo(key))
def listdir(path):
"""Return all files in a directory (using xbmcvfs)"""
from xbmcvfs import listdir as vfslistdir
return vfslistdir(path)
def mkdir(path):
"""Create a directory (using xbmcvfs)"""
from xbmcvfs import mkdir as vfsmkdir
_LOGGER.debug("Create directory '%s'.", path)
return vfsmkdir(path)
def mkdirs(path):
"""Create directory including parents (using xbmcvfs)"""
from xbmcvfs import mkdirs as vfsmkdirs
_LOGGER.debug("Recursively create directory '%s'.", path)
return vfsmkdirs(path)
def exists(path):
"""Whether the path exists (using xbmcvfs)"""
from xbmcvfs import exists as vfsexists
return vfsexists(path)
@contextmanager
def open_file(path, flags='r'):
"""Open a file (using xbmcvfs)"""
from xbmcvfs import File
fdesc = File(path, flags)
yield fdesc
fdesc.close()
def stat_file(path):
"""Return information about a file (using xbmcvfs)"""
from xbmcvfs import Stat
return Stat(path)
def delete(path):
"""Remove a file (using xbmcvfs)"""
from xbmcvfs import delete as vfsdelete
_LOGGER.debug("Delete file '%s'.", path)
return vfsdelete(path)
def container_refresh(url=None): def container_refresh(url=None):
"""Refresh the current container or (re)load a container by URL""" """Refresh the current container or (re)load a container by URL"""
if url: if url:
@ -518,56 +469,3 @@ def jsonrpc(*args, **kwargs):
if kwargs.get('jsonrpc') is None: if kwargs.get('jsonrpc') is None:
kwargs.update(jsonrpc='2.0') kwargs.update(jsonrpc='2.0')
return loads(xbmc.executeJSONRPC(dumps(kwargs))) return loads(xbmc.executeJSONRPC(dumps(kwargs)))
def get_cache(key, ttl=None):
""" Get an item from the cache """
import time
path = get_cache_path()
filename = '.'.join(key)
fullpath = path + filename
if not exists(fullpath):
return None
if ttl and time.mktime(time.localtime()) - stat_file(fullpath).st_mtime() > ttl:
return None
with open_file(fullpath, 'r') as fdesc:
try:
_LOGGER.debug('Fetching %s from cache', filename)
import json
value = json.load(fdesc)
return value
except (ValueError, TypeError):
return None
def set_cache(key, data):
""" Store an item in the cache """
path = get_cache_path()
filename = '.'.join(key)
fullpath = path + filename
if not exists(path):
mkdirs(path)
with open_file(fullpath, 'w') as fdesc:
_LOGGER.debug('Storing to cache as %s', filename)
import json
json.dump(data, fdesc)
def invalidate_cache(ttl=None):
""" Clear the cache """
path = get_cache_path()
if not exists(path):
return
_, files = listdir(path)
import time
now = time.mktime(time.localtime())
for filename in files:
fullpath = path + filename
if ttl and now - stat_file(fullpath).st_mtime() < ttl:
continue
delete(fullpath)

View File

@ -20,8 +20,8 @@ class Catalog:
def __init__(self): def __init__(self):
""" Initialise object """ """ Initialise object """
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password')) auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
self._api = ContentApi(auth) self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
self._menu = Menu() self._menu = Menu()
def show_catalog(self): def show_catalog(self):

View File

@ -5,7 +5,7 @@ from __future__ import absolute_import, division, unicode_literals
from resources.lib import kodiutils from resources.lib import kodiutils
from resources.lib.viervijfzes import CHANNELS from resources.lib.viervijfzes import CHANNELS
from resources.lib.viervijfzes.content import ContentApi, Program from resources.lib.viervijfzes.content import ContentApi, Program, CACHE_PREVENT, CACHE_AUTO
class Metadata: class Metadata:
@ -13,7 +13,7 @@ class Metadata:
def __init__(self): def __init__(self):
""" Initialise object """ """ Initialise object """
self._api = ContentApi() self._api = ContentApi(cache_path=kodiutils.get_cache_path())
def update(self): def update(self):
""" Update the metadata with a foreground progress indicator """ """ Update the metadata with a foreground progress indicator """
@ -25,25 +25,26 @@ class Metadata:
progress.update(int(((i + 1) / total) * 100), kodiutils.localize(30716, index=i + 1, total=total)) # Updating metadata ({index}/{total}) progress.update(int(((i + 1) / total) * 100), kodiutils.localize(30716, index=i + 1, total=total)) # Updating metadata ({index}/{total})
return progress.iscanceled() return progress.iscanceled()
self.fetch_metadata(callback=update_status) self.fetch_metadata(callback=update_status, refresh=True)
# Close progress indicator # Close progress indicator
progress.close() progress.close()
def fetch_metadata(self, callback=None): def fetch_metadata(self, callback=None, refresh=False):
""" Fetch the metadata for all the items in the catalog """ Fetch the metadata for all the items in the catalog
:type callback: callable :type callback: callable
:type refresh: bool
""" """
# Fetch all items from the catalog # Fetch all items from the catalog
items = [] items = []
for channel in list(CHANNELS): for channel in list(CHANNELS):
items.extend(self._api.get_programs(channel)) items.extend(self._api.get_programs(channel, CACHE_PREVENT))
count = len(items) count = len(items)
# Loop over all of them and download the metadata # Loop over all of them and download the metadata
for index, item in enumerate(items): for index, item in enumerate(items):
if isinstance(item, Program): if isinstance(item, Program):
self._api.get_program(item.channel, item.path) self._api.get_program(item.channel, item.path, CACHE_PREVENT if refresh else CACHE_AUTO)
# Run callback after every item # Run callback after every item
if callback and callback(index, count): if callback and callback(index, count):
@ -51,10 +52,3 @@ class Metadata:
return False return False
return True return True
@staticmethod
def clean():
""" Clear metadata (called from settings) """
kodiutils.invalidate_cache()
kodiutils.set_setting('metadata_last_updated', '0')
kodiutils.ok_dialog(message=kodiutils.localize(30714)) # Local metadata is cleared

View File

@ -53,7 +53,7 @@ class Player:
# Fetch an auth token now # Fetch an auth token now
try: try:
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password')) auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
# Get stream information # Get stream information
resolved_stream = ContentApi(auth).get_stream_by_uuid(item) resolved_stream = ContentApi(auth).get_stream_by_uuid(item)

View File

@ -5,6 +5,7 @@ from __future__ import absolute_import, division, unicode_literals
import hashlib import hashlib
import logging import logging
import os
from time import time from time import time
from xbmc import Monitor from xbmc import Monitor
@ -23,6 +24,7 @@ class BackgroundService(Monitor):
Monitor.__init__(self) Monitor.__init__(self)
self.update_interval = 24 * 3600 # Every 24 hours self.update_interval = 24 * 3600 # Every 24 hours
self.cache_expiry = 30 * 24 * 3600 # One month self.cache_expiry = 30 * 24 * 3600 # One month
self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
def run(self): def run(self):
""" Background loop for maintenance tasks """ """ Background loop for maintenance tasks """
@ -43,7 +45,7 @@ class BackgroundService(Monitor):
""" Callback when a setting has changed """ """ Callback when a setting has changed """
if self._has_credentials_changed(): if self._has_credentials_changed():
_LOGGER.info('Clearing auth tokens due to changed credentials') _LOGGER.info('Clearing auth tokens due to changed credentials')
AuthApi.clear_tokens() self._auth.clear_tokens()
# Refresh container # Refresh container
kodiutils.container_refresh() kodiutils.container_refresh()
@ -64,19 +66,34 @@ class BackgroundService(Monitor):
""" Update the metadata for the listings """ """ Update the metadata for the listings """
from resources.lib.modules.metadata import Metadata from resources.lib.modules.metadata import Metadata
# Clear outdated metadata
kodiutils.invalidate_cache(self.cache_expiry)
def update_status(_i, _total): def update_status(_i, _total):
""" Allow to cancel the background job """ """ Allow to cancel the background job """
return self.abortRequested() or not kodiutils.get_setting_bool('metadata_update') return self.abortRequested() or not kodiutils.get_setting_bool('metadata_update')
# Clear metadata that has expired for 30 days
self._remove_expired_metadata(30 * 24 * 60 * 60)
# Fetch new metadata
success = Metadata().fetch_metadata(callback=update_status) success = Metadata().fetch_metadata(callback=update_status)
# Update metadata_last_updated # Update metadata_last_updated
if success: if success:
kodiutils.set_setting('metadata_last_updated', str(int(time()))) kodiutils.set_setting('metadata_last_updated', str(int(time())))
@staticmethod
def _remove_expired_metadata(keep_expired=None):
""" Clear the cache """
path = kodiutils.get_cache_path()
if not os.path.exists(path):
return
now = time()
for filename in os.listdir(path):
fullpath = path + filename
if keep_expired and os.stat(fullpath).st_mtime + keep_expired > now:
continue
os.unlink(fullpath)
def run(): def run():
""" Run the BackgroundService """ """ Run the BackgroundService """

View File

@ -5,9 +5,9 @@ from __future__ import absolute_import, division, unicode_literals
import json import json
import logging import logging
import os
import time import time
from resources.lib import kodiutils
from resources.lib.viervijfzes.auth_awsidp import AwsIdp, InvalidLoginException, AuthenticationException from resources.lib.viervijfzes.auth_awsidp import AwsIdp, InvalidLoginException, AuthenticationException
_LOGGER = logging.getLogger('auth-api') _LOGGER = logging.getLogger('auth-api')
@ -21,18 +21,18 @@ class AuthApi:
TOKEN_FILE = 'auth-tokens.json' TOKEN_FILE = 'auth-tokens.json'
def __init__(self, username, password): def __init__(self, username, password, token_path):
""" Initialise object """ """ Initialise object """
self._username = username self._username = username
self._password = password self._password = password
self._cache_dir = kodiutils.get_tokens_path() self._token_path = token_path
self._id_token = None self._id_token = None
self._expiry = 0 self._expiry = 0
self._refresh_token = None self._refresh_token = None
# Load tokens from cache # Load tokens from cache
try: try:
with kodiutils.open_file(self._cache_dir + self.TOKEN_FILE, 'rb') as fdesc: with open(self._token_path + self.TOKEN_FILE, 'rb') as fdesc:
data_json = json.loads(fdesc.read()) data_json = json.loads(fdesc.read())
self._id_token = data_json.get('id_token') self._id_token = data_json.get('id_token')
self._refresh_token = data_json.get('refresh_token') self._refresh_token = data_json.get('refresh_token')
@ -72,9 +72,9 @@ class AuthApi:
self._expiry = now + 3600 self._expiry = now + 3600
# Store new tokens in cache # Store new tokens in cache
if not kodiutils.exists(self._cache_dir): if not os.path.exists(self._token_path):
kodiutils.mkdirs(self._cache_dir) os.mkdir(self._token_path)
with kodiutils.open_file(self._cache_dir + self.TOKEN_FILE, 'wb') as fdesc: with open(self._token_path + self.TOKEN_FILE, 'wb') as fdesc:
data = json.dumps(dict( data = json.dumps(dict(
id_token=self._id_token, id_token=self._id_token,
refresh_token=self._refresh_token, refresh_token=self._refresh_token,
@ -84,10 +84,10 @@ class AuthApi:
return self._id_token return self._id_token
@staticmethod def clear_tokens(self):
def clear_tokens():
""" Remove the cached tokens. """ """ Remove the cached tokens. """
kodiutils.delete(kodiutils.get_tokens_path() + AuthApi.TOKEN_FILE) if os.path.exists(self._token_path + AuthApi.TOKEN_FILE):
os.unlink(self._token_path + AuthApi.TOKEN_FILE)
@staticmethod @staticmethod
def _authenticate(username, password): def _authenticate(username, password):

View File

@ -5,13 +5,14 @@ from __future__ import absolute_import, division, unicode_literals
import json import json
import logging import logging
import os
import re import re
import time
from datetime import datetime from datetime import datetime
from six.moves.html_parser import HTMLParser from six.moves.html_parser import HTMLParser
import requests import requests
from resources.lib import kodiutils
from resources.lib.viervijfzes import CHANNELS from resources.lib.viervijfzes import CHANNELS
_LOGGER = logging.getLogger('content-api') _LOGGER = logging.getLogger('content-api')
@ -141,33 +142,49 @@ class ContentApi:
'zes': 'https://www.zestv.be/api', 'zes': 'https://www.zestv.be/api',
} }
def __init__(self, auth=None): def __init__(self, auth=None, cache_path=None):
""" Initialise object """ """ Initialise object """
self._session = requests.session() self._session = requests.session()
self._auth = auth self._auth = auth
self._cache_path = cache_path
def get_programs(self, channel): def get_programs(self, channel, cache=CACHE_AUTO):
""" Get a list of all programs of the specified channel. """ Get a list of all programs of the specified channel.
:type channel: str :type channel: str
:type cache: str
:rtype list[Program] :rtype list[Program]
NOTE: This function doesn't use an API.
""" """
if channel not in CHANNELS: if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel) raise Exception('Unknown channel %s' % channel)
# Load webpage def update():
data = self._get_url(CHANNELS[channel]['url']) """ Fetch the program listing by scraping """
# Load webpage
raw_html = self._get_url(CHANNELS[channel]['url'])
# Parse programs # Parse programs
parser = HTMLParser() parser = HTMLParser()
regex_programs = re.compile(r'<a class="program-overview__link" href="(?P<path>[^"]+)">\s+' regex_programs = re.compile(r'<a class="program-overview__link" href="(?P<path>[^"]+)">\s+'
r'<span class="program-overview__title">\s+(?P<title>[^<]+)</span>.*?' r'<span class="program-overview__title">\s+(?P<title>[^<]+)</span>.*?'
r'</a>', re.DOTALL) r'</a>', re.DOTALL)
data = {
item.group('path').lstrip('/'): parser.unescape(item.group('title').strip())
for item in regex_programs.finditer(raw_html)
}
if not data:
raise Exception('No programs found for %s' % channel)
return data
# Fetch listing from cache or update if needed
data = self._handle_cache(key=['programs', channel], cache_mode=cache, update=update, ttl=30 * 5)
if not data:
return []
programs = [] programs = []
for item in regex_programs.finditer(data): for path in data:
path = item.group('path').lstrip('/') title = data[path]
program = self.get_program(channel, path, CACHE_ONLY) # Get program details, but from cache only program = self.get_program(channel, path, CACHE_ONLY) # Get program details, but from cache only
if program: if program:
# Use program with metadata from cache # Use program with metadata from cache
@ -176,7 +193,7 @@ class ContentApi:
# Use program with the values that we've parsed from the page # Use program with the values that we've parsed from the page
programs.append(Program(channel=channel, programs.append(Program(channel=channel,
path=path, path=path,
title=parser.unescape(item.group('title').strip()))) title=title))
return programs return programs
def get_program(self, channel, path, cache=CACHE_AUTO): def get_program(self, channel, path, cache=CACHE_AUTO):
@ -185,20 +202,12 @@ class ContentApi:
:type path: str :type path: str
:type cache: int :type cache: int
:rtype Program :rtype Program
NOTE: This function doesn't use an API.
""" """
if channel not in CHANNELS: if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel) raise Exception('Unknown channel %s' % channel)
if cache in [CACHE_AUTO, CACHE_ONLY]: def update():
# Try to fetch from cache """ Fetch the program metadata by scraping """
data = kodiutils.get_cache(['program', channel, path])
if data is None and cache == CACHE_ONLY:
return None
else:
data = None
if data is None:
# Fetch webpage # Fetch webpage
page = self._get_url(CHANNELS[channel]['url'] + '/' + path) page = self._get_url(CHANNELS[channel]['url'] + '/' + path)
@ -207,46 +216,15 @@ class ContentApi:
json_data = HTMLParser().unescape(regex_program.search(page).group(1)) json_data = HTMLParser().unescape(regex_program.search(page).group(1))
data = json.loads(json_data)['data'] data = json.loads(json_data)['data']
# Store response in cache return data
kodiutils.set_cache(['program', channel, path], data)
# Fetch listing from cache or update if needed
data = self._handle_cache(key=['program', channel, path], cache_mode=cache, update=update)
program = self._parse_program_data(data) program = self._parse_program_data(data)
return program return program
def get_program_by_uuid(self, uuid, cache=CACHE_AUTO):
""" Get a Program object.
:type uuid: str
:type cache: int
:rtype Program
"""
if cache in [CACHE_AUTO, CACHE_ONLY]:
# Try to fetch from cache
data = kodiutils.get_cache(['program', uuid])
if data is None and cache == CACHE_ONLY:
return None
else:
data = None
if data is None:
# Fetch from API
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
data = json.loads(response)
if not data:
raise UnavailableException()
# Store response in cache
kodiutils.set_cache(['program', uuid], data)
return Program(
uuid=uuid,
path=data['url']['S'].strip('/'),
title=data['label']['S'],
description=data['description']['S'],
cover=data['image']['S'],
)
def get_episode(self, channel, path): def get_episode(self, channel, path):
""" Get a Episode object from the specified page. """ Get a Episode object from the specified page.
:type channel: str :type channel: str
@ -295,6 +273,9 @@ class ContentApi:
:type data: dict :type data: dict
:rtype Program :rtype Program
""" """
if data is None:
return None
# Create Program info # Create Program info
program = Program( program = Program(
uuid=data['id'], uuid=data['id'],
@ -385,3 +366,62 @@ class ContentApi:
raise Exception('Could not fetch data') raise Exception('Could not fetch data')
return response.text return response.text
def _handle_cache(self, key, cache_mode, update, ttl=30 * 24 * 60 * 60):
""" Fetch something from the cache, and update if needed """
if cache_mode in [CACHE_AUTO, CACHE_ONLY]:
# Try to fetch from cache
data = self._get_cache(key)
if data is None and cache_mode == CACHE_ONLY:
return None
else:
data = None
if data is None:
try:
# Fetch fresh data
_LOGGER.debug('Fetching fresh data for key %s', '.'.join(key))
data = update()
if data:
# Store fresh response in cache
self._set_cache(key, data, ttl)
except Exception as exc: # pylint: disable=broad-except
_LOGGER.warning('Something went wrong when refreshing live data: %s. Using expired cached values.', exc)
data = self._get_cache(key, allow_expired=True)
return data
def _get_cache(self, key, allow_expired=False):
""" Get an item from the cache """
filename = '.'.join(key) + '.json'
fullpath = self._cache_path + filename
if not os.path.exists(fullpath):
return None
if not allow_expired and os.stat(fullpath).st_mtime < time.time():
return None
with open(fullpath, 'r') as fdesc:
try:
_LOGGER.debug('Fetching %s from cache', filename)
value = json.load(fdesc)
return value
except (ValueError, TypeError):
return None
def _set_cache(self, key, data, ttl):
""" Store an item in the cache """
filename = '.'.join(key) + '.json'
fullpath = self._cache_path + filename
if not os.path.exists(self._cache_path):
os.mkdir(self._cache_path)
with open(fullpath, 'w') as fdesc:
_LOGGER.debug('Storing to cache as %s', filename)
json.dump(data, fdesc)
# Set TTL by modifying modification date
deadline = int(time.time()) + ttl
os.utime(fullpath, (deadline, deadline))

View File

@ -10,6 +10,5 @@
<setting label="30827" type="lsep"/> <!-- Metadata --> <setting label="30827" type="lsep"/> <!-- Metadata -->
<setting label="30829" type="bool" id="metadata_update" default="true" subsetting="true"/> <setting label="30829" type="bool" id="metadata_update" default="true" subsetting="true"/>
<setting label="30831" type="action" action="RunPlugin(plugin://plugin.video.viervijfzes/metadata/update)"/> <setting label="30831" type="action" action="RunPlugin(plugin://plugin.video.viervijfzes/metadata/update)"/>
<setting label="30833" type="action" action="RunPlugin(plugin://plugin.video.viervijfzes/metadata/clean)"/>
</category> </category>
</settings> </settings>

View File

@ -18,8 +18,8 @@ _LOGGER = logging.getLogger('test-api')
class TestApi(unittest.TestCase): class TestApi(unittest.TestCase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TestApi, self).__init__(*args, **kwargs) super(TestApi, self).__init__(*args, **kwargs)
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password')) auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
self._api = ContentApi(auth) self._api = ContentApi(auth, cache_path=kodiutils.get_cache_path())
def test_programs(self): def test_programs(self):
for channel in ['vier', 'vijf', 'zes']: for channel in ['vier', 'vijf', 'zes']:
@ -40,9 +40,6 @@ class TestApi(unittest.TestCase):
program = self._api.get_program('vier', 'auwch') program = self._api.get_program('vier', 'auwch')
self.assertIsInstance(program, Program) self.assertIsInstance(program, Program)
program_by_uuid = self._api.get_program_by_uuid(program.uuid)
self.assertIsInstance(program_by_uuid, Program)
episode = program.episodes[0] episode = program.episodes[0]
video = self._api.get_stream_by_uuid(episode.uuid) video = self._api.get_stream_by_uuid(episode.uuid)
self.assertTrue(video) self.assertTrue(video)

View File

@ -20,16 +20,16 @@ class TestAuth(unittest.TestCase):
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.') @unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
def test_login(self): def test_login(self):
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path())
# Clear any cache we have # Clear any cache we have
AuthApi.clear_tokens() auth.clear_tokens()
# We should get a token by logging in # We should get a token by logging in
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
token = auth.get_token() token = auth.get_token()
self.assertTrue(token) self.assertTrue(token)
# Test it a second time, it should go from memory now # Test it a second time, it should go from memory now
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'))
token = auth.get_token() token = auth.get_token()
self.assertTrue(token) self.assertTrue(token)

View File

@ -9,6 +9,7 @@ import logging
import unittest import unittest
from datetime import date from datetime import date
from resources.lib import kodiutils
from resources.lib.viervijfzes.content import ContentApi, Episode from resources.lib.viervijfzes.content import ContentApi, Episode
from resources.lib.viervijfzes.epg import EpgApi, EpgProgram from resources.lib.viervijfzes.epg import EpgApi, EpgProgram
@ -48,7 +49,7 @@ class TestEpg(unittest.TestCase):
epg_program = [program for program in epg_programs if program.video_url][0] epg_program = [program for program in epg_programs if program.video_url][0]
# Lookup the Episode data since we don't have an UUID # Lookup the Episode data since we don't have an UUID
api = ContentApi() api = ContentApi(cache_path=kodiutils.get_cache_path())
episode = api.get_episode(epg_program.channel, epg_program.video_url) episode = api.get_episode(epg_program.channel, epg_program.video_url)
self.assertIsInstance(episode, Episode) self.assertIsInstance(episode, Episode)

View File

@ -57,7 +57,7 @@ class TestRouting(unittest.TestCase):
routing.run([routing.url_for(addon.show_tvguide_detail, channel='vier', date='today'), '0', '']) routing.run([routing.url_for(addon.show_tvguide_detail, channel='vier', date='today'), '0', ''])
def test_metadata_update(self): def test_metadata_update(self):
routing.run([routing.url_for(addon.metadata_clean), '0', '']) routing.run([routing.url_for(addon.metadata_update), '0', ''])
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -129,9 +129,9 @@ class DialogProgress:
print('\033[37;44;1mPROGRESS:\033[35;49;1m [%s] \033[37;1m%s\033[39;0m' % (heading, line1)) print('\033[37;44;1mPROGRESS:\033[35;49;1m [%s] \033[37;1m%s\033[39;0m' % (heading, line1))
sys.stdout.flush() sys.stdout.flush()
@staticmethod def iscanceled(self):
def iscanceled():
"""A stub implementation for the xbmcgui DialogProgress class iscanceled() method""" """A stub implementation for the xbmcgui DialogProgress class iscanceled() method"""
return self.percentage > 5 # Cancel at 5%
def update(self, percentage, line1=None, line2=None, line3=None): def update(self, percentage, line1=None, line2=None, line3=None):
"""A stub implementation for the xbmcgui DialogProgress class update() method""" """A stub implementation for the xbmcgui DialogProgress class update() method"""