Kodi plugin to to play various online streams (mostly Latvian)

plugin.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. # -*- coding: utf-8 -*-
  2. """
  3. kodiswift.plugin
  4. -----------------
  5. This module contains the Plugin class. This class handles all of the url
  6. routing and interaction with Kodi for a plugin.
  7. :copyright: (c) 2012 by Jonathan Beluch
  8. :license: GPLv3, see LICENSE for more details.
  9. """
  10. from __future__ import absolute_import
  11. import collections
  12. import inspect
  13. import os
  14. import sys
  15. import kodiswift
  16. from kodiswift import xbmc, xbmcaddon, Request
  17. from kodiswift.logger import log, setup_log
  18. from kodiswift.urls import UrlRule, NotFoundException, AmbiguousUrlException
  19. from kodiswift.xbmcmixin import XBMCMixin
  20. __all__ = ['Plugin']
  21. class Plugin(XBMCMixin):
  22. """The Plugin objects encapsulates all the properties and methods necessary
  23. for running an Kodi plugin. The plugin instance is a central place for
  24. registering view functions and keeping track of plugin state.
  25. Usually the plugin instance is created in the main plugin.py file for the
  26. plugin. Typical creation looks like this::
  27. >>> from kodiswift import Plugin
  28. >>> plugin = Plugin('Hello Kodi')
  29. """
  30. def __init__(self, name=None, addon_id=None, plugin_file=None,
  31. info_type=None):
  32. """
  33. Args:
  34. name (Optional[str]): The name of the plugin, e.g. 'Hello Kodi'.
  35. addon_id (Optional[str): The Kodi addon ID for the plugin,
  36. e.g. 'plugin.video.hellokodi'. This parameter is now optional
  37. and is really only useful for testing purposes. If it is not
  38. provided, the correct value will be parsed from the
  39. addon.xml file.
  40. plugin_file (Optional[str]): If provided, it should be the path
  41. to the plugin.py file in the root of the addon directory.
  42. This only has an effect when kodiswift is running on the
  43. command line. Will default to the current working directory
  44. since kodiswift requires execution in the root addon directory
  45. anyway. The parameter still exists to ease testing.
  46. info_type (Optional[str):
  47. """
  48. self._name = name
  49. self._routes = []
  50. self._view_functions = {}
  51. self._addon = xbmcaddon.Addon()
  52. self._addon_id = addon_id or self._addon.getAddonInfo('id')
  53. self._name = name or self._addon.getAddonInfo('name')
  54. self._info_type = info_type
  55. if not self._info_type:
  56. types = {
  57. 'video': 'video',
  58. 'audio': 'music',
  59. 'image': 'pictures',
  60. }
  61. self._info_type = types.get(self._addon_id.split('.')[1], 'video')
  62. # Keeps track of the added list items
  63. self._current_items = []
  64. # Gets initialized when self.run() is called
  65. self._request = None
  66. # A flag to keep track of a call to xbmcplugin.endOfDirectory()
  67. self._end_of_directory = False
  68. # Keep track of the update_listing flag passed to
  69. # xbmcplugin.endOfDirectory()
  70. self._update_listing = False
  71. # The plugin's named logger
  72. self._log = setup_log(self._addon_id)
  73. # The path to the storage directory for the addon
  74. self._storage_path = xbmc.translatePath(
  75. 'special://profile/addon_data/%s/.storage/' % self._addon_id)
  76. if not os.path.isdir(self._storage_path):
  77. os.makedirs(self._storage_path)
  78. # If we are running in CLI, we need to load the strings.xml manually
  79. # Since kodiswift currently relies on execution from an addon's root
  80. # directly, we can rely on cwd for now...
  81. if kodiswift.CLI_MODE:
  82. from kodiswift.mockxbmc import utils
  83. if plugin_file:
  84. plugin_dir = os.path.dirname(plugin_file)
  85. else:
  86. plugin_dir = os.getcwd()
  87. strings_fn = os.path.join(
  88. plugin_dir, 'resources', 'language', 'English', 'strings.po')
  89. utils.load_addon_strings(self._addon, strings_fn)
  90. @property
  91. def info_type(self):
  92. return self._info_type
  93. @property
  94. def log(self):
  95. """The log instance for the plugin.
  96. Returns an instance of the stdlib's ``logging.Logger``.
  97. This log will print to STDOUT when running in CLI mode and will
  98. forward messages to Kodi's log when running in Kodi.
  99. Examples:
  100. ``plugin.log.debug('Debug message')``
  101. ``plugin.log.warning('Warning message')``
  102. ``plugin.log.error('Error message')``
  103. Returns:
  104. logging.Logger:
  105. """
  106. return self._log
  107. @property
  108. def id(self):
  109. """The id for the addon instance.
  110. """
  111. return self._addon_id
  112. @property
  113. def storage_path(self):
  114. """A full path to the storage folder for this plugin's addon data.
  115. """
  116. return self._storage_path
  117. @property
  118. def addon(self):
  119. """This addon's wrapped instance of xbmcaddon.Plugin.
  120. """
  121. return self._addon
  122. @property
  123. def added_items(self):
  124. """The list of currently added items.
  125. Even after repeated calls to :meth:`~kodiswift.Plugin.add_items`, this
  126. property will contain the complete list of added items.
  127. """
  128. return self._current_items
  129. @property
  130. def handle(self):
  131. """The current plugin's handle. Equal to ``plugin.request.handle``.
  132. """
  133. return self.request.handle
  134. @property
  135. def request(self):
  136. """The current :class:`~kodiswift.Request`.
  137. Raises:
  138. Exception: if the request hasn't been initialized yet via
  139. :meth:`~kodiswift.Plugin.run()`.
  140. Returns:
  141. kodiswift.Request:
  142. """
  143. if self._request is None:
  144. raise Exception('It seems the current request has not been '
  145. 'initialized yet. Please ensure that '
  146. '`plugin.run()` has been called before attempting '
  147. 'to access the current request.')
  148. return self._request
  149. @property
  150. def name(self):
  151. """The addon's name.
  152. Returns:
  153. str:
  154. """
  155. return self._name
  156. def clear_added_items(self):
  157. self._current_items = []
  158. def register_module(self, module, url_prefix):
  159. """Registers a module with a plugin. Requires a url_prefix that will
  160. then enable calls to url_for.
  161. Args:
  162. module (kodiswift.Module):
  163. url_prefix (str): A url prefix to use for all module urls,
  164. e.g. '/mymodule'
  165. """
  166. module.plugin = self
  167. module.url_prefix = url_prefix
  168. for func in module.register_funcs:
  169. func(self, url_prefix)
  170. def cached_route(self, url_rule, name=None, options=None, ttl=None):
  171. """A decorator to add a route to a view and also apply caching. The
  172. url_rule, name and options arguments are the same arguments for the
  173. route function. The TTL argument if given will passed along to the
  174. caching decorator.
  175. """
  176. route_decorator = self.route(url_rule, name=name, options=options)
  177. if ttl:
  178. cache_decorator = self.cached(ttl)
  179. else:
  180. cache_decorator = self.cached()
  181. def new_decorator(func):
  182. return route_decorator(cache_decorator(func))
  183. return new_decorator
  184. def route(self, url_rule=None, name=None, root=False, options=None):
  185. """A decorator to add a route to a view. name is used to
  186. differentiate when there are multiple routes for a given view."""
  187. def decorator(f):
  188. view_name = name or f.__name__
  189. if root:
  190. url = '/'
  191. elif not url_rule:
  192. url = '/' + view_name + '/'
  193. args = inspect.getargspec(f)[0]
  194. if args:
  195. url += '/'.join('%s/<%s>' % (p, p) for p in args)
  196. else:
  197. url = url_rule
  198. self.add_url_rule(url, f, name=view_name, options=options)
  199. return f
  200. return decorator
  201. def add_url_rule(self, url_rule, view_func, name, options=None):
  202. """This method adds a URL rule for routing purposes. The
  203. provided name can be different from the view function name if
  204. desired. The provided name is what is used in url_for to build
  205. a URL.
  206. The route decorator provides the same functionality.
  207. """
  208. rule = UrlRule(url_rule, view_func, name, options)
  209. if name in self._view_functions.keys():
  210. # TODO: Raise exception for ambiguous views during registration
  211. log.warning('Cannot add url rule "%s" with name "%s". There is '
  212. 'already a view with that name', url_rule, name)
  213. self._view_functions[name] = None
  214. else:
  215. log.debug('Adding url rule "%s" named "%s" pointing to function '
  216. '"%s"', url_rule, name, view_func.__name__)
  217. self._view_functions[name] = rule
  218. self._routes.append(rule)
  219. def url_for(self, endpoint, **items):
  220. """Returns a valid Kodi plugin URL for the given endpoint name.
  221. endpoint can be the literal name of a function, or it can
  222. correspond to the name keyword arguments passed to the route
  223. decorator.
  224. Raises AmbiguousUrlException if there is more than one possible
  225. view for the given endpoint name.
  226. """
  227. try:
  228. rule = self._view_functions[endpoint]
  229. except KeyError:
  230. try:
  231. rule = (rule for rule in self._view_functions.values()
  232. if rule.view_func == endpoint).next()
  233. except StopIteration:
  234. raise NotFoundException(
  235. '%s does not match any known patterns.' % endpoint)
  236. # rule can be None since values of None are allowed in the
  237. # _view_functions dict. This signifies more than one view function is
  238. # tied to the same name.
  239. if not rule:
  240. # TODO: Make this a regular exception
  241. raise AmbiguousUrlException
  242. path_qs = rule.make_path_qs(items)
  243. return 'plugin://%s%s' % (self._addon_id, path_qs)
  244. def redirect(self, url):
  245. """Used when you need to redirect to another view, and you only
  246. have the final plugin:// url."""
  247. # TODO: Should we be overriding self.request with the new request?
  248. new_request = self._parse_request(url=url, handle=self.request.handle)
  249. log.debug('Redirecting %s to %s', self.request.path, new_request.path)
  250. return self._dispatch(new_request.path)
  251. def run(self):
  252. """The main entry point for a plugin."""
  253. self._request = self._parse_request()
  254. log.debug('Handling incoming request for %s', self.request.path)
  255. items = self._dispatch(self.request.path)
  256. # Close any open storages which will persist them to disk
  257. if hasattr(self, '_unsynced_storage'):
  258. for storage in self._unsynced_storage.values():
  259. log.debug('Saving a %s storage to disk at "%s"',
  260. storage.file_format, storage.file_path)
  261. storage.close()
  262. return items
  263. def _dispatch(self, path):
  264. for rule in self._routes:
  265. try:
  266. view_func, items = rule.match(path)
  267. except NotFoundException:
  268. continue
  269. log.info('Request for "%s" matches rule for function "%s"',
  270. path, view_func.__name__)
  271. resp = view_func(**items)
  272. # Only call self.finish() for UI container listing calls to plugin
  273. # (handle will be >= 0). Do not call self.finish() when called via
  274. # RunPlugin() (handle will be -1).
  275. if not self._end_of_directory and self.handle >= 0:
  276. if isinstance(resp, dict):
  277. resp['items'] = self.finish(**resp)
  278. elif isinstance(resp, collections.Iterable):
  279. resp = self.finish(items=resp)
  280. return resp
  281. raise NotFoundException('No matching view found for %s' % path)
  282. @staticmethod
  283. def _parse_request(url=None, handle=None):
  284. """Handles setup of the plugin state, including request
  285. arguments, handle, mode.
  286. This method never needs to be called directly. For testing, see
  287. plugin.test()
  288. """
  289. # To accommodate self.redirect, we need to be able to parse a full
  290. # url as well
  291. if url is None:
  292. url = sys.argv[0]
  293. if len(sys.argv) == 3:
  294. url += sys.argv[2]
  295. if handle is None:
  296. handle = sys.argv[1]
  297. return Request(url, handle)