123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- # -*- coding: utf-8 -*-
- """
- kodiswift.plugin
- -----------------
-
- This module contains the Plugin class. This class handles all of the url
- routing and interaction with Kodi for a plugin.
-
- :copyright: (c) 2012 by Jonathan Beluch
- :license: GPLv3, see LICENSE for more details.
- """
- from __future__ import absolute_import
-
- import collections
- import inspect
- import os
- import sys
-
- import kodiswift
- from kodiswift import xbmc, xbmcaddon, Request
- from kodiswift.logger import log, setup_log
- from kodiswift.urls import UrlRule, NotFoundException, AmbiguousUrlException
- from kodiswift.xbmcmixin import XBMCMixin
-
- __all__ = ['Plugin']
-
-
- class Plugin(XBMCMixin):
- """The Plugin objects encapsulates all the properties and methods necessary
- for running an Kodi plugin. The plugin instance is a central place for
- registering view functions and keeping track of plugin state.
-
- Usually the plugin instance is created in the main plugin.py file for the
- plugin. Typical creation looks like this::
-
- >>> from kodiswift import Plugin
- >>> plugin = Plugin('Hello Kodi')
- """
-
- def __init__(self, name=None, addon_id=None, plugin_file=None,
- info_type=None):
- """
- Args:
- name (Optional[str]): The name of the plugin, e.g. 'Hello Kodi'.
- addon_id (Optional[str): The Kodi addon ID for the plugin,
- e.g. 'plugin.video.hellokodi'. This parameter is now optional
- and is really only useful for testing purposes. If it is not
- provided, the correct value will be parsed from the
- addon.xml file.
- plugin_file (Optional[str]): If provided, it should be the path
- to the plugin.py file in the root of the addon directory.
- This only has an effect when kodiswift is running on the
- command line. Will default to the current working directory
- since kodiswift requires execution in the root addon directory
- anyway. The parameter still exists to ease testing.
- info_type (Optional[str):
- """
- self._name = name
- self._routes = []
- self._view_functions = {}
- self._addon = xbmcaddon.Addon(addon_id) if addon_id else xbmcaddon.Addon()
-
- self._addon_id = addon_id or self._addon.getAddonInfo('id')
- self._name = name or self._addon.getAddonInfo('name')
-
- self._info_type = info_type
- if not self._info_type:
- types = {
- 'video': 'video',
- 'audio': 'music',
- 'image': 'pictures',
- }
- self._info_type = types.get(self._addon_id.split('.')[1], 'video')
-
- # Keeps track of the added list items
- self._current_items = []
-
- # Gets initialized when self.run() is called
- self._request = 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)
-
- # The path to the storage directory for the addon
- self._storage_path = xbmc.translatePath(
- 'special://profile/addon_data/%s/.storage/' % self._addon_id)
- if not os.path.isdir(self._storage_path):
- os.makedirs(self._storage_path)
-
- # If we are running in CLI, we need to load the strings.xml manually
- # Since kodiswift currently relies on execution from an addon's root
- # directly, we can rely on cwd for now...
- if kodiswift.CLI_MODE:
- from kodiswift.mockxbmc import utils
- if plugin_file:
- plugin_dir = os.path.dirname(plugin_file)
- else:
- plugin_dir = os.getcwd()
- strings_fn = os.path.join(
- plugin_dir, 'resources', 'language', 'English', 'strings.po')
- utils.load_addon_strings(self._addon, strings_fn)
- self.load_addon_settings()
-
- @property
- def info_type(self):
- return self._info_type
-
- @property
- def log(self):
- """The log instance for the plugin.
-
- Returns an instance of the stdlib's ``logging.Logger``.
- This log will print to STDOUT when running in CLI mode and will
- forward messages to Kodi's log when running in Kodi.
-
- Examples:
- ``plugin.log.debug('Debug message')``
- ``plugin.log.warning('Warning message')``
- ``plugin.log.error('Error message')``
-
- Returns:
- logging.Logger:
- """
- return self._log
-
- @property
- def id(self):
- """The id for the addon instance.
- """
- return self._addon_id
-
- @property
- def storage_path(self):
- """A full path to the storage folder for this plugin's addon data.
- """
- return self._storage_path
-
- @property
- def addon(self):
- """This addon's wrapped instance of xbmcaddon.Plugin.
- """
- return self._addon
-
- @property
- def added_items(self):
- """The list of currently added items.
-
- Even after repeated calls to :meth:`~kodiswift.Plugin.add_items`, this
- property will contain the complete list of added items.
- """
- return self._current_items
-
- @property
- def handle(self):
- """The current plugin's handle. Equal to ``plugin.request.handle``.
- """
- return self.request.handle
-
- @property
- def request(self):
- """The current :class:`~kodiswift.Request`.
-
- Raises:
- Exception: if the request hasn't been initialized yet via
- :meth:`~kodiswift.Plugin.run()`.
-
- Returns:
- kodiswift.Request:
- """
- if self._request is None:
- raise Exception('It seems the current request has not been '
- 'initialized yet. Please ensure that '
- '`plugin.run()` has been called before attempting '
- 'to access the current request.')
- return self._request
-
- @property
- def name(self):
- """The addon's name.
-
- Returns:
- str:
- """
- return self._name
-
- def clear_added_items(self):
- self._current_items = []
-
- def register_module(self, module, url_prefix):
- """Registers a module with a plugin. Requires a url_prefix that will
- then enable calls to url_for.
-
- Args:
- module (kodiswift.Module):
- url_prefix (str): A url prefix to use for all module urls,
- e.g. '/mymodule'
- """
- module.plugin = self
- module.url_prefix = url_prefix
- for func in module.register_funcs:
- func(self, url_prefix)
-
- def cached_route(self, url_rule, name=None, options=None, ttl=None):
- """A decorator to add a route to a view and also apply caching. The
- url_rule, name and options arguments are the same arguments for the
- route function. The TTL argument if given will passed along to the
- caching decorator.
- """
- route_decorator = self.route(url_rule, name=name, options=options)
- if ttl:
- cache_decorator = self.cached(ttl)
- else:
- cache_decorator = self.cached()
-
- def new_decorator(func):
- return route_decorator(cache_decorator(func))
-
- return new_decorator
-
- def route(self, url_rule=None, name=None, root=False, options=None):
- """A decorator to add a route to a view. name is used to
- differentiate when there are multiple routes for a given view."""
-
- def decorator(f):
- view_name = name or f.__name__
- if root:
- url = '/'
- elif not url_rule:
- url = '/' + view_name + '/'
- args = inspect.getargspec(f)[0]
- if args:
- url += '/'.join('%s/<%s>' % (p, p) for p in args)
- else:
- url = url_rule
- self.add_url_rule(url, f, name=view_name, options=options)
- return f
-
- return decorator
-
- def add_url_rule(self, url_rule, view_func, name, options=None):
- """This method adds a URL rule for routing purposes. The
- provided name can be different from the view function name if
- desired. The provided name is what is used in url_for to build
- a URL.
-
- The route decorator provides the same functionality.
- """
- rule = UrlRule(url_rule, view_func, name, options)
- if name in self._view_functions.keys():
- # TODO: Raise exception for ambiguous views during registration
- log.warning('Cannot add url rule "%s" with name "%s". There is '
- 'already a view with that name', url_rule, name)
- self._view_functions[name] = None
- else:
- log.debug('Adding url rule "%s" named "%s" pointing to function '
- '"%s"', url_rule, name, view_func.__name__)
- self._view_functions[name] = rule
- self._routes.append(rule)
-
- def url_for(self, endpoint, **items):
- """Returns a valid Kodi plugin URL for the given endpoint name.
- endpoint can be the literal name of a function, or it can
- correspond to the name keyword arguments passed to the route
- decorator.
-
- Raises AmbiguousUrlException if there is more than one possible
- view for the given endpoint name.
- """
- 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 does not 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
-
- path_qs = rule.make_path_qs(items)
- return 'plugin://%s%s' % (self._addon_id, path_qs)
-
- def redirect(self, url):
- """Used when you need to redirect to another view, and you only
- have the final plugin:// url."""
- # TODO: Should we be overriding self.request with the new request?
- new_request = self._parse_request(url=url, handle=self.request.handle)
- log.debug('Redirecting %s to %s', self.request.path, new_request.path)
- return self._dispatch(new_request.path)
-
- def run(self):
- """The main entry point for a plugin."""
- self._request = self._parse_request()
- log.debug('Handling incoming request for %s', self.request.path)
- items = self._dispatch(self.request.path)
-
- # Close any open storages which will persist them to disk
- if hasattr(self, '_unsynced_storage'):
- for storage in self._unsynced_storage.values():
- log.debug('Saving a %s storage to disk at "%s"',
- storage.file_format, storage.file_path)
- storage.close()
-
- return items
-
- def _dispatch(self, path):
- for rule in self._routes:
- try:
- view_func, items = rule.match(path)
- except NotFoundException:
- continue
- log.info('Request for "%s" matches rule for function "%s"',
- path, view_func.__name__)
- resp = view_func(**items)
-
- # 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:
- if isinstance(resp, dict):
- resp['items'] = self.finish(**resp)
- elif isinstance(resp, collections.Iterable):
- resp = self.finish(items=resp)
- return resp
-
- raise NotFoundException('No matching view found for %s' % path)
-
- @staticmethod
- def _parse_request(url=None, handle=None):
- """Handles setup of the plugin state, including request
- arguments, handle, mode.
-
- This method never needs to be called directly. For testing, see
- plugin.test()
- """
- # To accommodate self.redirect, we need to be able to parse a full
- # url as well
- if url is None:
- url = sys.argv[0]
- if len(sys.argv) == 3:
- url += sys.argv[2]
- if handle is None:
- handle = sys.argv[1]
- return Request(url, handle)
|