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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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(addon_id) if addon_id else 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. self.load_addon_settings()
  91. @property
  92. def info_type(self):
  93. return self._info_type
  94. @property
  95. def log(self):
  96. """The log instance for the plugin.
  97. Returns an instance of the stdlib's ``logging.Logger``.
  98. This log will print to STDOUT when running in CLI mode and will
  99. forward messages to Kodi's log when running in Kodi.
  100. Examples:
  101. ``plugin.log.debug('Debug message')``
  102. ``plugin.log.warning('Warning message')``
  103. ``plugin.log.error('Error message')``
  104. Returns:
  105. logging.Logger:
  106. """
  107. return self._log
  108. @property
  109. def id(self):
  110. """The id for the addon instance.
  111. """
  112. return self._addon_id
  113. @property
  114. def storage_path(self):
  115. """A full path to the storage folder for this plugin's addon data.
  116. """
  117. return self._storage_path
  118. @property
  119. def addon(self):
  120. """This addon's wrapped instance of xbmcaddon.Plugin.
  121. """
  122. return self._addon
  123. @property
  124. def added_items(self):
  125. """The list of currently added items.
  126. Even after repeated calls to :meth:`~kodiswift.Plugin.add_items`, this
  127. property will contain the complete list of added items.
  128. """
  129. return self._current_items
  130. @property
  131. def handle(self):
  132. """The current plugin's handle. Equal to ``plugin.request.handle``.
  133. """
  134. return self.request.handle
  135. @property
  136. def request(self):
  137. """The current :class:`~kodiswift.Request`.
  138. Raises:
  139. Exception: if the request hasn't been initialized yet via
  140. :meth:`~kodiswift.Plugin.run()`.
  141. Returns:
  142. kodiswift.Request:
  143. """
  144. if self._request is None:
  145. raise Exception('It seems the current request has not been '
  146. 'initialized yet. Please ensure that '
  147. '`plugin.run()` has been called before attempting '
  148. 'to access the current request.')
  149. return self._request
  150. @property
  151. def name(self):
  152. """The addon's name.
  153. Returns:
  154. str:
  155. """
  156. return self._name
  157. def clear_added_items(self):
  158. self._current_items = []
  159. def register_module(self, module, url_prefix):
  160. """Registers a module with a plugin. Requires a url_prefix that will
  161. then enable calls to url_for.
  162. Args:
  163. module (kodiswift.Module):
  164. url_prefix (str): A url prefix to use for all module urls,
  165. e.g. '/mymodule'
  166. """
  167. module.plugin = self
  168. module.url_prefix = url_prefix
  169. for func in module.register_funcs:
  170. func(self, url_prefix)
  171. def cached_route(self, url_rule, name=None, options=None, ttl=None):
  172. """A decorator to add a route to a view and also apply caching. The
  173. url_rule, name and options arguments are the same arguments for the
  174. route function. The TTL argument if given will passed along to the
  175. caching decorator.
  176. """
  177. route_decorator = self.route(url_rule, name=name, options=options)
  178. if ttl:
  179. cache_decorator = self.cached(ttl)
  180. else:
  181. cache_decorator = self.cached()
  182. def new_decorator(func):
  183. return route_decorator(cache_decorator(func))
  184. return new_decorator
  185. def route(self, url_rule=None, name=None, root=False, options=None):
  186. """A decorator to add a route to a view. name is used to
  187. differentiate when there are multiple routes for a given view."""
  188. def decorator(f):
  189. view_name = name or f.__name__
  190. if root:
  191. url = '/'
  192. elif not url_rule:
  193. url = '/' + view_name + '/'
  194. args = inspect.getargspec(f)[0]
  195. if args:
  196. url += '/'.join('%s/<%s>' % (p, p) for p in args)
  197. else:
  198. url = url_rule
  199. self.add_url_rule(url, f, name=view_name, options=options)
  200. return f
  201. return decorator
  202. def add_url_rule(self, url_rule, view_func, name, options=None):
  203. """This method adds a URL rule for routing purposes. The
  204. provided name can be different from the view function name if
  205. desired. The provided name is what is used in url_for to build
  206. a URL.
  207. The route decorator provides the same functionality.
  208. """
  209. rule = UrlRule(url_rule, view_func, name, options)
  210. if name in self._view_functions.keys():
  211. # TODO: Raise exception for ambiguous views during registration
  212. log.warning('Cannot add url rule "%s" with name "%s". There is '
  213. 'already a view with that name', url_rule, name)
  214. self._view_functions[name] = None
  215. else:
  216. log.debug('Adding url rule "%s" named "%s" pointing to function '
  217. '"%s"', url_rule, name, view_func.__name__)
  218. self._view_functions[name] = rule
  219. self._routes.append(rule)
  220. def url_for(self, endpoint, **items):
  221. """Returns a valid Kodi plugin URL for the given endpoint name.
  222. endpoint can be the literal name of a function, or it can
  223. correspond to the name keyword arguments passed to the route
  224. decorator.
  225. Raises AmbiguousUrlException if there is more than one possible
  226. view for the given endpoint name.
  227. """
  228. try:
  229. rule = self._view_functions[endpoint]
  230. except KeyError:
  231. try:
  232. rule = (rule for rule in self._view_functions.values()
  233. if rule.view_func == endpoint).next()
  234. except StopIteration:
  235. raise NotFoundException(
  236. '%s does not match any known patterns.' % endpoint)
  237. # rule can be None since values of None are allowed in the
  238. # _view_functions dict. This signifies more than one view function is
  239. # tied to the same name.
  240. if not rule:
  241. # TODO: Make this a regular exception
  242. raise AmbiguousUrlException
  243. path_qs = rule.make_path_qs(items)
  244. return 'plugin://%s%s' % (self._addon_id, path_qs)
  245. def redirect(self, url):
  246. """Used when you need to redirect to another view, and you only
  247. have the final plugin:// url."""
  248. # TODO: Should we be overriding self.request with the new request?
  249. new_request = self._parse_request(url=url, handle=self.request.handle)
  250. log.debug('Redirecting %s to %s', self.request.path, new_request.path)
  251. return self._dispatch(new_request.path)
  252. def run(self):
  253. """The main entry point for a plugin."""
  254. self._request = self._parse_request()
  255. log.debug('Handling incoming request for %s', self.request.path)
  256. items = self._dispatch(self.request.path)
  257. # Close any open storages which will persist them to disk
  258. if hasattr(self, '_unsynced_storage'):
  259. for storage in self._unsynced_storage.values():
  260. log.debug('Saving a %s storage to disk at "%s"',
  261. storage.file_format, storage.file_path)
  262. storage.close()
  263. return items
  264. def _dispatch(self, path):
  265. for rule in self._routes:
  266. try:
  267. view_func, items = rule.match(path)
  268. except NotFoundException:
  269. continue
  270. log.info('Request for "%s" matches rule for function "%s"',
  271. path, view_func.__name__)
  272. resp = view_func(**items)
  273. # Only call self.finish() for UI container listing calls to plugin
  274. # (handle will be >= 0). Do not call self.finish() when called via
  275. # RunPlugin() (handle will be -1).
  276. if not self._end_of_directory and self.handle >= 0:
  277. if isinstance(resp, dict):
  278. resp['items'] = self.finish(**resp)
  279. elif isinstance(resp, collections.Iterable):
  280. resp = self.finish(items=resp)
  281. return resp
  282. raise NotFoundException('No matching view found for %s' % path)
  283. @staticmethod
  284. def _parse_request(url=None, handle=None):
  285. """Handles setup of the plugin state, including request
  286. arguments, handle, mode.
  287. This method never needs to be called directly. For testing, see
  288. plugin.test()
  289. """
  290. # To accommodate self.redirect, we need to be able to parse a full
  291. # url as well
  292. if url is None:
  293. url = sys.argv[0]
  294. if len(sys.argv) == 3:
  295. url += sys.argv[2]
  296. if handle is None:
  297. handle = sys.argv[1]
  298. return Request(url, handle)