From 20c66bb68bf9ae066fe317c739abbcfd82087472 Mon Sep 17 00:00:00 2001 From: Jonathan Beluch Date: Wed, 20 Feb 2013 22:34:45 -0700 Subject: [PATCH] [xbmcswift2-release-script] prepare release 2.4.0 --- addon.xml | 2 +- changelog.txt | 32 ++++++ lib/xbmcswift2/__init__.py | 4 +- lib/xbmcswift2/actions.py | 27 +++++ lib/xbmcswift2/listitem.py | 19 +++- lib/xbmcswift2/plugin.py | 35 ++++--- lib/xbmcswift2/storage.py | 4 + lib/xbmcswift2/urls.py | 13 ++- lib/xbmcswift2/xbmcmixin.py | 197 +++++++++++++++++++++++++++++++----- xbmcswift2_version | 2 +- 10 files changed, 285 insertions(+), 50 deletions(-) create mode 100644 lib/xbmcswift2/actions.py diff --git a/addon.xml b/addon.xml index 5bff450..3d5d47c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,4 +1,4 @@ - + diff --git a/changelog.txt b/changelog.txt index 1524744..a1a092c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,38 @@ CHANGES ======= + +Version 2.4.0 +------------- +- Allow ints to be passed to plugin.url_for() and str() them. +- plugin.set_resolved_url() now takes an item dict instead of a URL. (The URL + is still allowed for backwards compatibility but is decprecated). +- Ability to replace the context menu items by setting 'replace_context_menu' + to True in an item dict. +- pluign.get_setting() now requires an extra paramter, *converter*, which + converts the item from XML to a python type specified by converter. +- Major bug fix to accomodate handles > 0 in XBMC Frodo. +- Auto-call plugin.finish(succeeded=False) if a view returns None. +- XBMC now decides the default player when using plugin.play_video(). +- Storages now call sync() when clear() is called. +- Added plugin.clear_function_cache() which clears the storage behind the + @cached and @cached_route decorators. +- Add ability to set stream info for list items. Also, devs can now add + stream_info to item dicts. +- Added the actions module. Contains actions.background() and + actions.update_view() which are convenience wrappers for RunPlugin() and + Container.Update(). +- Allow passing of functions to url_for. +- 'properties' key in an item dict should now be a dictionary. A list of tuples + is still allowed for backwards compatibility. +- Addon user is now prompted and storage automatically cleared if it becomes + corrupted. +- Added subtitles support. A 'subtitles' parameter was added to + plugin.set_resolved_url(). +- xbmcswift2 URLs and routing code is now indifferent to a trailing slash. +- Bug fixes. See https://github.com/jbeluch/xbmcswift2/issues?milestone=3. + + Version 2.3.2 ------------- diff --git a/lib/xbmcswift2/__init__.py b/lib/xbmcswift2/__init__.py index f7a9ca0..5a3634a 100644 --- a/lib/xbmcswift2/__init__.py +++ b/lib/xbmcswift2/__init__.py @@ -54,12 +54,12 @@ def func(*args, **kwargs): from logger import log # Mock the XBMC modules - from mockxbmc import xbmc, xbmcgui, xbmcplugin, xbmcaddon + from mockxbmc import xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs xbmc = module(xbmc) xbmcgui = module(xbmcgui) xbmcplugin = module(xbmcplugin) xbmcaddon = module(xbmcaddon) - xbmcvfs = module() + xbmcvfs = module(xbmcvfs) from xbmcswift2.storage import TimedStorage diff --git a/lib/xbmcswift2/actions.py b/lib/xbmcswift2/actions.py new file mode 100644 index 0000000..d42ff7f --- /dev/null +++ b/lib/xbmcswift2/actions.py @@ -0,0 +1,27 @@ +''' + xbmcswift2.actions + ------------------ + + This module contains wrapper functions for XBMC built-in functions. + + :copyright: (c) 2012 by Jonathan Beluch + :license: GPLv3, see LICENSE for more details. +''' + + +def background(url): + '''This action will run an addon in the background for the provided URL. + + See 'XBMC.RunPlugin()' at + http://wiki.xbmc.org/index.php?title=List_of_built-in_functions. + ''' + return 'XBMC.RunPlugin(%s)' % url + + +def update_view(url): + '''This action will update the current container view with provided url. + + See 'XBMC.Container.Update()' at + http://wiki.xbmc.org/index.php?title=List_of_built-in_functions. + ''' + return 'XBMC.Container.Update(%s)' % url diff --git a/lib/xbmcswift2/listitem.py b/lib/xbmcswift2/listitem.py index fd6db23..ffe7f97 100644 --- a/lib/xbmcswift2/listitem.py +++ b/lib/xbmcswift2/listitem.py @@ -108,6 +108,10 @@ def set_property(self, key, value): '''Sets a property for the given key and value''' return self._listitem.setProperty(key, value) + def add_stream_info(self, stream_type, stream_values): + '''Adds stream details''' + return self._listitem.addStreamInfo(stream_type, stream_values) + def get_icon(self): '''Returns the listitem's icon image''' return self._icon @@ -181,7 +185,8 @@ def as_xbmc_listitem(self): @classmethod def from_dict(cls, label=None, label2=None, icon=None, thumbnail=None, path=None, selected=None, info=None, properties=None, - context_menu=None, is_playable=None, info_type='video'): + context_menu=None, replace_context_menu=False, + is_playable=None, info_type='video', stream_info=None): '''A ListItem constructor for setting a lot of properties not available in the regular __init__ method. Useful to collect all the properties in a dict and then use the **dct to call this @@ -199,12 +204,18 @@ def from_dict(cls, label=None, label2=None, icon=None, thumbnail=None, listitem.set_is_playable(True) if properties: + # Need to support existing tuples, but prefer to have a dict for + # properties. + if hasattr(properties, 'items'): + properties = properties.items() for key, val in properties: listitem.set_property(key, val) - # By default doesn't replace, use .add_context_menu_items if you wish - # to set replace_items=True + if stream_info: + for stream_type, stream_values in stream_info.items(): + listitem.add_stream_info(stream_type, stream_values) + if context_menu: - listitem.add_context_menu_items(context_menu) + listitem.add_context_menu_items(context_menu, replace_context_menu) return listitem diff --git a/lib/xbmcswift2/plugin.py b/lib/xbmcswift2/plugin.py index 6e66c7b..7b635aa 100644 --- a/lib/xbmcswift2/plugin.py +++ b/lib/xbmcswift2/plugin.py @@ -96,6 +96,10 @@ def __init__(self, name=None, addon_id=None, filepath=None, info_type=None): # A flag to keep track of a call to xbmcplugin.endOfDirectory() self._end_of_directory = False + # Keep track of the update_listing flag passed to + # xbmcplugin.endOfDirectory() + self._update_listing = False + # The plugin's named logger self._log = setup_log(self._addon_id) @@ -272,11 +276,18 @@ def url_for(self, endpoint, **items): Raises AmbiguousUrlException if there is more than one possible view for the given endpoint name. ''' - if endpoint not in self._view_functions.keys(): - raise NotFoundException, ('%s doesn\'t match any known patterns.' % - endpoint) - - rule = self._view_functions[endpoint] + try: + rule = self._view_functions[endpoint] + except KeyError: + try: + rule = (rule for rule in self._view_functions.values() if rule.view_func == endpoint).next() + except StopIteration: + raise NotFoundException( + '%s doesn\'t match any known patterns.' % endpoint) + + # rule can be None since values of None are allowed in the + # _view_functions dict. This signifies more than one view function is + # tied to the same name. if not rule: # TODO: Make this a regular exception raise AmbiguousUrlException @@ -294,14 +305,14 @@ def _dispatch(self, path): path, view_func.__name__) listitems = view_func(**items) - # XXX: The UI Container listing call to plugin or the resolving - # url call always has a handle greater or equals 0. RunPlugin() - # call using a handle -1. we only auto-call endOfDirectory for - # the UI Container listing call. and set_resolve_url() also - # set the _end_of_directory flag so we do not call finish() for it. - # Allow the returning of bare dictionaries so we can cache view + # Only call self.finish() for UI container listing calls to plugin + # (handle will be >= 0). Do not call self.finish() when called via + # RunPlugin() (handle will be -1). if not self._end_of_directory and self.handle >= 0: - listitems = self.finish(listitems) + if listitems is None: + self.finish(succeeded=False) + else: + listitems = self.finish(listitems) return listitems raise NotFoundException, 'No matching view found for %s' % path diff --git a/lib/xbmcswift2/storage.py b/lib/xbmcswift2/storage.py index 908a64f..92a2b11 100644 --- a/lib/xbmcswift2/storage.py +++ b/lib/xbmcswift2/storage.py @@ -143,6 +143,10 @@ def raw_dict(self): initial_update = collections.MutableMapping.update + def clear(self): + super(_Storage, self).clear() + self.sync() + class TimedStorage(_Storage): '''A dict with the ability to persist to disk and TTL for items.''' diff --git a/lib/xbmcswift2/urls.py b/lib/xbmcswift2/urls.py index ca19bdd..472d7a2 100644 --- a/lib/xbmcswift2/urls.py +++ b/lib/xbmcswift2/urls.py @@ -50,7 +50,11 @@ def __init__(self, url_rule, view_func, name, options): self._url_format = self._url_rule.replace('<', '{').replace('>', '}') # Make a regex pattern for matching incoming URLs - p = self._url_rule.replace('<', '(?P<').replace('>', '>[^/]+?)') + rule = self._url_rule + if rule != '/': + # Except for a path of '/', the trailing slash is optional. + rule = self._url_rule.rstrip('/') + '/?' + p = rule.replace('<', '(?P<').replace('>', '>[^/]+?)') try: self._regex = re.compile('^' + p + '$') @@ -130,7 +134,7 @@ def make_path_qs(self, items): parameters. All items will be urlencoded. Any items which are not instances of - basestring will be pickled before being urlencoded. + basestring, or int/long will be pickled before being urlencoded. .. warning:: The pickling of items only works for key/value pairs which will be in the query string. This behavior should only be @@ -139,6 +143,11 @@ def make_path_qs(self, items): hard limit on URL length. See the caching section if you need to persist a large amount of data between requests. ''' + # Convert any ints and longs to strings + for key, val in items.items(): + if isinstance(val, (int, long)): + items[key] = str(val) + # First use our defaults passed when registering the rule url_items = dict((key, val) for key, val in self._options.items() if key in self._keywords) diff --git a/lib/xbmcswift2/xbmcmixin.py b/lib/xbmcswift2/xbmcmixin.py index 9af96f6..f976c05 100644 --- a/lib/xbmcswift2/xbmcmixin.py +++ b/lib/xbmcswift2/xbmcmixin.py @@ -2,11 +2,12 @@ import sys import time import shelve +import urllib from datetime import timedelta from functools import wraps import xbmcswift2 -from xbmcswift2 import xbmc, xbmcaddon, xbmcplugin +from xbmcswift2 import xbmc, xbmcaddon, xbmcplugin, xbmcgui from xbmcswift2.storage import TimedStorage from xbmcswift2.logger import log from xbmcswift2.constants import VIEW_MODES, SortMethod @@ -14,6 +15,8 @@ from request import Request + + class XBMCMixin(object): '''A mixin to add XBMC helper methods. In order to use this mixin, the child class must implement the following methods and @@ -31,6 +34,8 @@ class XBMCMixin(object): _end_of_directory = False + _update_listing + self.handle # optional @@ -39,6 +44,9 @@ class XBMCMixin(object): _unsynced_storages = None # TODO: Ensure above is implemented ''' + + _function_cache_name = '.functions' + def cached(self, TTL=60 * 24): '''A decorator that will cache the output of the wrapped function. The key used for the cache is the function name as well as the `*args` and @@ -51,7 +59,7 @@ def cached(self, TTL=60 * 24): ''' def decorating_function(function): # TODO test this method - storage = self.get_storage('.functions', file_format='pickle', + storage = self.get_storage(self._function_cache_name, file_format='pickle', TTL=TTL) kwd_mark = 'f35c2d973e1bbbc61ca60fc6d7ae4eb3' @@ -77,6 +85,13 @@ def wrapper(*args, **kwargs): return wrapper return decorating_function + def clear_function_cache(self): + '''Clears the storage that caches results when using + :meth:`xbmcswift2.Plugin.cached_route` or + :meth:`xbmcswift2.Plugin.cached`. + ''' + self.get_storage(self._function_cache_name).clear() + def list_storages(self): '''Returns a list of existing stores. The returned names can then be used to call get_storage(). @@ -119,7 +134,22 @@ def get_storage(self, name='main', file_format='pickle', TTL=None): except KeyError: if TTL: TTL = timedelta(minutes=TTL) - storage = TimedStorage(filename, file_format, TTL) + + try: + storage = TimedStorage(filename, file_format, TTL) + except ValueError: + # Thrown when the storage file is corrupted and can't be read. + # Prompt user to delete storage. + choices = ['Clear storage', 'Cancel'] + ret = xbmcgui.Dialog().select('A storage file is corrupted. It' + ' is recommended to clear it.', + choices) + if ret == 0: + os.remove(filename) + storage = TimedStorage(filename, file_format, TTL) + else: + raise Exception('Corrupted storage file at %s' % filename) + self._unsynced_storages[filename] = storage log.debug('Loaded storage "%s" from disk', name) return storage @@ -131,7 +161,12 @@ def get_string(self, stringid): '''Returns the localized string from strings.xml for the given stringid. ''' - return self.addon.getLocalizedString(stringid) + stringid = int(stringid) + if not hasattr(self, '_strings'): + self._strings = {} + if not stringid in self._strings: + self._strings[stringid] = self.addon.getLocalizedString(stringid) + return self._strings[stringid] def set_content(self, content): '''Sets the content type for the plugin.''' @@ -143,10 +178,46 @@ def set_content(self, content): #assert content in contents, 'Content type "%s" is not valid' % content xbmcplugin.setContent(self.handle, content) - def get_setting(self, key): + def get_setting(self, key, converter=None, choices=None): + '''Returns the settings value for the provided key. + If converter is str, unicode, bool or int the settings value will be + returned converted to the provided type. + If choices is an instance of list or tuple its item at position of the + settings value be returned. + .. note:: It is suggested to always use unicode for text-settings + because else xbmc returns utf-8 encoded strings. + + :param key: The id of the setting defined in settings.xml. + :param converter: (Optional) Choices are str, unicode, bool and int. + :param converter: (Optional) Choices are instances of list or tuple. + + Examples: + * ``plugin.get_setting('per_page', int)`` + * ``plugin.get_setting('password', unicode)`` + * ``plugin.get_setting('force_viewmode', bool)`` + * ``plugin.get_setting('content', choices=('videos', 'movies'))`` + ''' #TODO: allow pickling of settings items? # TODO: STUB THIS OUT ON CLI - return self.addon.getSetting(id=key) + value = self.addon.getSetting(id=key) + if converter is str: + return value + elif converter is unicode: + return value.decode('utf-8') + elif converter is bool: + return value == 'true' + elif converter is int: + return int(value) + elif isinstance(choices, (list, tuple)): + return choices[int(value)] + elif converter is None: + log.warning('No converter provided, unicode should be used, ' + 'but returning str value') + return value + else: + raise TypeError('Acceptable converters are str, unicode, bool and ' + 'int. Acceptable choices are instances of list ' + ' or tuple.') def set_setting(self, key, val): # TODO: STUB THIS OUT ON CLI @@ -228,19 +299,100 @@ def notify(self, msg='', title=None, delay=5000, image=''): xbmc.executebuiltin('XBMC.Notification("%s", "%s", "%s", "%s")' % (msg, title, delay, image)) - def set_resolved_url(self, url): - item = xbmcswift2.ListItem(path=url) - item.set_played(True) - xbmcplugin.setResolvedUrl(self.handle, True, item.as_xbmc_listitem()) - return [item] + def _listitemify(self, item): + '''Creates an xbmcswift2.ListItem if the provided value for item is a + dict. If item is already a valid xbmcswift2.ListItem, the item is + returned unmodified. + ''' + info_type = self.info_type if hasattr(self, 'info_type') else 'video' - def play_video(self, item, player=xbmc.PLAYER_CORE_DVDPLAYER): - if not hasattr(item, 'as_xbmc_listitem'): + # Create ListItems for anything that is not already an instance of + # ListItem + if not hasattr(item, 'as_tuple'): if 'info_type' not in item.keys(): - item['info_type'] = 'video' + item['info_type'] = info_type item = xbmcswift2.ListItem.from_dict(**item) + return item + + def _add_subtitles(self, subtitles): + '''Adds subtitles to playing video. + + :param subtitles: A URL to a remote subtitles file or a local filename + for a subtitles file. + + .. warning:: You must start playing a video before calling this method + or it will loop for an indefinite length. + ''' + # This method is named with an underscore to suggest that callers pass + # the subtitles argument to set_resolved_url instead of calling this + # method directly. This is to ensure a video is played before calling + # this method. + player = xbmc.Player() + for _ in xrange(30): + if player.isPlaying(): + break + time.sleep(1) + else: + raise Exception('No video playing. Aborted after 30 seconds.') + + player.setSubtitles(subtitles) + + def set_resolved_url(self, item=None, subtitles=None): + '''Takes a url or a listitem to be played. Used in conjunction with a + playable list item with a path that calls back into your addon. + + :param item: A playable list item or url. Pass None to alert XBMC of a + failure to resolve the item. + + .. warning:: When using set_resolved_url you should ensure + the initial playable item (which calls back + into your addon) doesn't have a trailing + slash in the URL. Otherwise it won't work + reliably with XBMC's PlayMedia(). + :param subtitles: A URL to a remote subtitles file or a local filename + for a subtitles file to be played along with the + item. + ''' + if self._end_of_directory: + raise Exception('Current XBMC handle has been removed. Either ' + 'set_resolved_url(), end_of_directory(), or ' + 'finish() has already been called.') + self._end_of_directory = True + + succeeded = True + if item is None: + # None item indicates the resolve url failed. + item = {} + succeeded = False + + if isinstance(item, basestring): + # caller is passing a url instead of an item dict + item = {'path': item} + + item = self._listitemify(item) item.set_played(True) - xbmc.Player(player).play(item.get_path(), item.as_xbmc_listitem()) + xbmcplugin.setResolvedUrl(self.handle, succeeded, + item.as_xbmc_listitem()) + + # call to _add_subtitles must be after setResolvedUrl + if subtitles: + self._add_subtitles(subtitles) + return [item] + + def play_video(self, item, player=None): + try: + # videos are always type video + item['info_type'] = 'video' + except TypeError: + pass # not a dict + + item = self._listitemify(item) + item.set_played(True) + if player: + _player = xbmc.Player(player) + else: + _player = xbmc.Player() + _player.play(item.get_path(), item.as_xbmc_listitem()) return [item] def add_items(self, items): @@ -254,19 +406,7 @@ def add_items(self, items): :meth:`xbmcswift2.ListItem.from_dict` or an instance of :class:`xbmcswift2.ListItem`. ''' - # For each item if it is not already a list item, we need to create one - _items = [] - info_type = self.info_type if hasattr(self, 'info_type') else 'video' - - # Create ListItems for anything that is not already an instance of - # ListItem - for item in items: - if not isinstance(item, xbmcswift2.ListItem): - if 'info_type' not in item.keys(): - item['info_type'] = info_type - item = xbmcswift2.ListItem.from_dict(**item) - _items.append(item) - + _items = [self._listitemify(item) for item in items] tuples = [item.as_tuple() for item in _items] xbmcplugin.addDirectoryItems(self.handle, tuples, len(tuples)) @@ -285,6 +425,7 @@ def end_of_directory(self, succeeded=True, update_listing=False, Typically it is not necessary to call this method directly, as calling :meth:`~xbmcswift2.Plugin.finish` will call this method. ''' + self._update_listing = update_listing if not self._end_of_directory: self._end_of_directory = True # Finalize the directory items diff --git a/xbmcswift2_version b/xbmcswift2_version index 4fd6b68..c062afb 100644 --- a/xbmcswift2_version +++ b/xbmcswift2_version @@ -1 +1 @@ -8fc7013db0baf89f258ae3ee158914390295ddc8 \ No newline at end of file +0e7a3642499554edc8265fdf1ba6c5ee567daa78 \ No newline at end of file