123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- # -*- coding: utf-8 -*-
- """
- kodiswift.cli.app
- ----------------
-
- This package contains the code which runs plugins from the command line.
-
- :copyright: (c) 2012 by Jonathan Beluch
- :license: GPLv3, see LICENSE for more details.
- """
- from __future__ import absolute_import
-
- import logging
- import os
- import sys
- from xml.etree import ElementTree as Et
-
- from kodiswift import Plugin, ListItem, logger
- from kodiswift.cli import Option
- from kodiswift.cli.console import (display_listitems, continue_or_quit,
- get_user_choice)
- from kodiswift.common import Modes
-
- __all__ = ['get_addon_module_name', 'crawl', 'RunCommand', 'PluginManager',
- 'setup_options', 'patch_sysargv', 'patch_plugin', 'once',
- 'interactive']
-
-
- class RunCommand(object):
- """A CLI command to run a plugin."""
-
- command = 'run'
- usage = '%prog run [once|interactive|crawl] [url]'
- option_list = (
- Option('-q', '--quiet', action='store_true',
- help='set logging level to quiet'),
- Option('-v', '--verbose', action='store_true',
- help='set logging level to verbose'),
- )
-
- @staticmethod
- def run(opts, args):
- """The run method for the 'run' command. Executes a plugin from the
- command line.
- """
- setup_options(opts)
-
- mode = Modes.ONCE
- if len(args) > 0 and hasattr(Modes, args[0].upper()):
- _mode = args.pop(0).upper()
- mode = getattr(Modes, _mode)
-
- url = None
- if len(args) > 0:
- # A url was specified
- url = args.pop(0)
-
- plugin_mgr = PluginManager.load_plugin_from_addon_xml(mode, url)
- plugin_mgr.run()
-
-
- def setup_options(opts):
- """Takes any actions necessary based on command line options"""
- if opts.quiet:
- logger.log.setLevel(logging.WARNING)
- logger.GLOBAL_LOG_LEVEL = logging.WARNING
-
- if opts.verbose:
- logger.log.setLevel(logging.DEBUG)
- logger.GLOBAL_LOG_LEVEL = logging.DEBUG
-
-
- def get_addon_module_name(addon_xml_filename):
- """Attempts to extract a module name for the given addon's addon.xml file.
- Looks for the 'xbmc.python.pluginsource' extension node and returns the
- addon's filename without the .py suffix.
- """
- try:
- xml = Et.parse(addon_xml_filename).getroot()
- except IOError:
- sys.exit('Cannot find an addon.xml file in the current working '
- 'directory. Please run this command from the root directory '
- 'of an addon.')
-
- try:
- plugin_source = (ext for ext in xml.findall('extension') if
- ext.get('point') == 'xbmc.python.pluginsource').next()
- except StopIteration:
- sys.exit('ERROR, no pluginsource in addonxml')
-
- return plugin_source.get('library').split('.')[0]
-
-
- class PluginManager(object):
- """A class to handle running a plugin in CLI mode. Handles setup state
- before calling plugin.run().
- """
-
- @classmethod
- def load_plugin_from_addon_xml(cls, mode, url):
- """Attempts to import a plugin's source code and find an instance of
- :class:`~kodiswift.Plugin`. Returns an instance of PluginManager if
- successful.
- """
- cwd = os.getcwd()
- sys.path.insert(0, cwd)
- module_name = get_addon_module_name(os.path.join(cwd, 'addon.xml'))
- addon = __import__(module_name)
-
- # Find the first instance of kodiswift.Plugin
- try:
- plugin = (attr_value for attr_value in vars(addon).values()
- if isinstance(attr_value, Plugin)).next()
- except StopIteration:
- sys.exit('Could not find a Plugin instance in %s.py' % module_name)
-
- return cls(plugin, mode, url)
-
- def __init__(self, plugin, mode, url):
- self.plugin = plugin
- self.mode = mode
- self.url = url
-
- def run(self):
- """This method runs the the plugin in the appropriate mode parsed from
- the command line options.
- """
- handle = 0
- handlers = {
- Modes.ONCE: once,
- Modes.CRAWL: crawl,
- Modes.INTERACTIVE: interactive,
- }
- handler = handlers[self.mode]
- patch_sysargv(self.url or 'plugin://%s/' % self.plugin.id, handle)
- return handler(self.plugin)
-
-
- def patch_sysargv(*args):
- """Patches sys.argv with the provided args"""
- sys.argv = args[:]
-
-
- def patch_plugin(plugin, path, handle=None):
- """Patches a few attributes of a plugin instance to enable a new call to
- plugin.run()
- """
- if handle is None:
- handle = plugin.request.handle
- patch_sysargv(path, handle)
- plugin._end_of_directory = False
-
-
- def once(plugin, parent_stack=None):
- """A run mode for the CLI that runs the plugin once and exits."""
- plugin.clear_added_items()
- items = plugin.run()
-
- # if update_listing=True, we need to remove the last url from the parent
- # stack
- if parent_stack and plugin._update_listing:
- del parent_stack[-1]
-
- # if we have parent items, include the most recent in the display
- if parent_stack:
- items.insert(0, parent_stack[-1])
-
- display_listitems(items, plugin.request.url)
- return items
-
-
- def interactive(plugin):
- """A run mode for the CLI that runs the plugin in a loop based on user
- input.
- """
- items = [item for item in once(plugin)]
- #items = [item for item in once(plugin) if not item.get_played()]
- parent_stack = [] # Keep track of parents so we can have a '..' option
-
- selected_item = get_user_choice(items)
- while selected_item is not None:
- #if parent_stack and selected_item == parent_stack[-1]:
- if selected_item.path.startswith("http"):
- plugin.play_video(selected_item)
- continue
-
- if selected_item.label == "..":
- # User selected the parent item, remove from list
- parent_stack.pop()
- else:
- # User selected non parent item, add current url to parent stack
- parent_stack.append(ListItem.from_dict(label='..', path=plugin.request.url))
- patch_plugin(plugin, selected_item.path)
-
-
- items = [item for item in once(plugin, parent_stack=parent_stack)]
- #if not item.get_played()]
- selected_item = get_user_choice(items)
-
-
- def crawl(plugin):
- """Performs a breadth-first crawl of all possible routes from the
- starting path. Will only visit a URL once, even if it is referenced
- multiple times in a plugin. Requires user interaction in between each
- fetch.
- """
- # TODO: use OrderedSet?
- paths_visited = set()
- paths_to_visit = set(item.get_path() for item in once(plugin))
-
- while paths_to_visit and continue_or_quit():
- path = paths_to_visit.pop()
- paths_visited.add(path)
-
- # Run the new listitem
- patch_plugin(plugin, path)
- new_paths = set(item.get_path() for item in once(plugin))
-
- # Filter new items by checking against urls_visited and
- # urls_tovisit
- paths_to_visit.update(path for path in new_paths
- if path not in paths_visited)
|