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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. # -*- coding: utf-8 -*-
  2. """
  3. kodiswift.cli.app
  4. ----------------
  5. This package contains the code which runs plugins from the command line.
  6. :copyright: (c) 2012 by Jonathan Beluch
  7. :license: GPLv3, see LICENSE for more details.
  8. """
  9. from __future__ import absolute_import
  10. import logging
  11. import os
  12. import sys
  13. from xml.etree import ElementTree as Et
  14. from kodiswift import Plugin, ListItem, logger
  15. from kodiswift.cli import Option
  16. from kodiswift.cli.console import (display_listitems, continue_or_quit,
  17. get_user_choice)
  18. from kodiswift.common import Modes
  19. __all__ = ['get_addon_module_name', 'crawl', 'RunCommand', 'PluginManager',
  20. 'setup_options', 'patch_sysargv', 'patch_plugin', 'once',
  21. 'interactive']
  22. class RunCommand(object):
  23. """A CLI command to run a plugin."""
  24. command = 'run'
  25. usage = '%prog run [once|interactive|crawl] [url]'
  26. option_list = (
  27. Option('-q', '--quiet', action='store_true',
  28. help='set logging level to quiet'),
  29. Option('-v', '--verbose', action='store_true',
  30. help='set logging level to verbose'),
  31. )
  32. @staticmethod
  33. def run(opts, args):
  34. """The run method for the 'run' command. Executes a plugin from the
  35. command line.
  36. """
  37. setup_options(opts)
  38. mode = Modes.ONCE
  39. if len(args) > 0 and hasattr(Modes, args[0].upper()):
  40. _mode = args.pop(0).upper()
  41. mode = getattr(Modes, _mode)
  42. url = None
  43. if len(args) > 0:
  44. # A url was specified
  45. url = args.pop(0)
  46. plugin_mgr = PluginManager.load_plugin_from_addon_xml(mode, url)
  47. plugin_mgr.run()
  48. def setup_options(opts):
  49. """Takes any actions necessary based on command line options"""
  50. if opts.quiet:
  51. logger.log.setLevel(logging.WARNING)
  52. logger.GLOBAL_LOG_LEVEL = logging.WARNING
  53. if opts.verbose:
  54. logger.log.setLevel(logging.DEBUG)
  55. logger.GLOBAL_LOG_LEVEL = logging.DEBUG
  56. def get_addon_module_name(addon_xml_filename):
  57. """Attempts to extract a module name for the given addon's addon.xml file.
  58. Looks for the 'xbmc.python.pluginsource' extension node and returns the
  59. addon's filename without the .py suffix.
  60. """
  61. try:
  62. xml = Et.parse(addon_xml_filename).getroot()
  63. except IOError:
  64. sys.exit('Cannot find an addon.xml file in the current working '
  65. 'directory. Please run this command from the root directory '
  66. 'of an addon.')
  67. try:
  68. plugin_source = (ext for ext in xml.findall('extension') if
  69. ext.get('point') == 'xbmc.python.pluginsource').next()
  70. except StopIteration:
  71. sys.exit('ERROR, no pluginsource in addonxml')
  72. return plugin_source.get('library').split('.')[0]
  73. class PluginManager(object):
  74. """A class to handle running a plugin in CLI mode. Handles setup state
  75. before calling plugin.run().
  76. """
  77. @classmethod
  78. def load_plugin_from_addon_xml(cls, mode, url):
  79. """Attempts to import a plugin's source code and find an instance of
  80. :class:`~kodiswift.Plugin`. Returns an instance of PluginManager if
  81. successful.
  82. """
  83. cwd = os.getcwd()
  84. sys.path.insert(0, cwd)
  85. module_name = get_addon_module_name(os.path.join(cwd, 'addon.xml'))
  86. addon = __import__(module_name)
  87. # Find the first instance of kodiswift.Plugin
  88. try:
  89. plugin = (attr_value for attr_value in vars(addon).values()
  90. if isinstance(attr_value, Plugin)).next()
  91. except StopIteration:
  92. sys.exit('Could not find a Plugin instance in %s.py' % module_name)
  93. return cls(plugin, mode, url)
  94. def __init__(self, plugin, mode, url):
  95. self.plugin = plugin
  96. self.mode = mode
  97. self.url = url
  98. def run(self):
  99. """This method runs the the plugin in the appropriate mode parsed from
  100. the command line options.
  101. """
  102. handle = 0
  103. handlers = {
  104. Modes.ONCE: once,
  105. Modes.CRAWL: crawl,
  106. Modes.INTERACTIVE: interactive,
  107. }
  108. handler = handlers[self.mode]
  109. patch_sysargv(self.url or 'plugin://%s/' % self.plugin.id, handle)
  110. return handler(self.plugin)
  111. def patch_sysargv(*args):
  112. """Patches sys.argv with the provided args"""
  113. sys.argv = args[:]
  114. def patch_plugin(plugin, path, handle=None):
  115. """Patches a few attributes of a plugin instance to enable a new call to
  116. plugin.run()
  117. """
  118. if handle is None:
  119. handle = plugin.request.handle
  120. patch_sysargv(path, handle)
  121. plugin._end_of_directory = False
  122. def once(plugin, parent_stack=None):
  123. """A run mode for the CLI that runs the plugin once and exits."""
  124. plugin.clear_added_items()
  125. items = plugin.run()
  126. # if update_listing=True, we need to remove the last url from the parent
  127. # stack
  128. if parent_stack and plugin._update_listing:
  129. del parent_stack[-1]
  130. # if we have parent items, include the most recent in the display
  131. if parent_stack:
  132. items.insert(0, parent_stack[-1])
  133. display_listitems(items, plugin.request.url)
  134. return items
  135. def interactive(plugin):
  136. """A run mode for the CLI that runs the plugin in a loop based on user
  137. input.
  138. """
  139. items = [item for item in once(plugin) if not item.get_played()]
  140. parent_stack = [] # Keep track of parents so we can have a '..' option
  141. selected_item = get_user_choice(items)
  142. while selected_item is not None:
  143. if parent_stack and selected_item == parent_stack[-1]:
  144. # User selected the parent item, remove from list
  145. parent_stack.pop()
  146. else:
  147. # User selected non parent item, add current url to parent stack
  148. parent_stack.append(ListItem.from_dict(label='..',
  149. path=plugin.request.url))
  150. patch_plugin(plugin, selected_item.get_path())
  151. items = [item for item in once(plugin, parent_stack=parent_stack)
  152. if not item.get_played()]
  153. selected_item = get_user_choice(items)
  154. def crawl(plugin):
  155. """Performs a breadth-first crawl of all possible routes from the
  156. starting path. Will only visit a URL once, even if it is referenced
  157. multiple times in a plugin. Requires user interaction in between each
  158. fetch.
  159. """
  160. # TODO: use OrderedSet?
  161. paths_visited = set()
  162. paths_to_visit = set(item.get_path() for item in once(plugin))
  163. while paths_to_visit and continue_or_quit():
  164. path = paths_to_visit.pop()
  165. paths_visited.add(path)
  166. # Run the new listitem
  167. patch_plugin(plugin, path)
  168. new_paths = set(item.get_path() for item in once(plugin))
  169. # Filter new items by checking against urls_visited and
  170. # urls_tovisit
  171. paths_to_visit.update(path for path in new_paths
  172. if path not in paths_visited)