Browse Source

trukstosie faili

Ivars 6 years ago
parent
commit
46c3c2845f
69 changed files with 5180 additions and 0 deletions
  1. BIN
      icon.png
  2. 83
    0
      kodiswift/__init__.py
  3. BIN
      kodiswift/__init__.pyc
  4. BIN
      kodiswift/__init__.pyo
  5. 37
    0
      kodiswift/actions.py
  6. 18
    0
      kodiswift/cli/__init__.py
  7. BIN
      kodiswift/cli/__init__.pyc
  8. 216
    0
      kodiswift/cli/app.py
  9. BIN
      kodiswift/cli/app.pyc
  10. 78
    0
      kodiswift/cli/cli.py
  11. BIN
      kodiswift/cli/cli.pyc
  12. 101
    0
      kodiswift/cli/console.py
  13. BIN
      kodiswift/cli/console.pyc
  14. 192
    0
      kodiswift/cli/create.py
  15. BIN
      kodiswift/cli/create.pyc
  16. 19
    0
      kodiswift/cli/data/addon.py
  17. BIN
      kodiswift/cli/data/addon.pyc
  18. 17
    0
      kodiswift/cli/data/addon.xml
  19. 1
    0
      kodiswift/cli/data/resources/__init__.py
  20. BIN
      kodiswift/cli/data/resources/__init__.pyc
  21. 26
    0
      kodiswift/cli/data/resources/language/English/strings.po
  22. 1
    0
      kodiswift/cli/data/resources/lib/__init__.py
  23. BIN
      kodiswift/cli/data/resources/lib/__init__.pyc
  24. 147
    0
      kodiswift/common.py
  25. BIN
      kodiswift/common.pyc
  26. BIN
      kodiswift/common.pyo
  27. 76
    0
      kodiswift/constants.py
  28. BIN
      kodiswift/constants.pyc
  29. BIN
      kodiswift/constants.pyo
  30. 339
    0
      kodiswift/listitem.py
  31. BIN
      kodiswift/listitem.pyc
  32. BIN
      kodiswift/listitem.pyo
  33. 104
    0
      kodiswift/logger.py
  34. BIN
      kodiswift/logger.pyc
  35. BIN
      kodiswift/logger.pyo
  36. 1
    0
      kodiswift/mockxbmc/__init__.py
  37. BIN
      kodiswift/mockxbmc/__init__.pyc
  38. 1835
    0
      kodiswift/mockxbmc/polib.py
  39. BIN
      kodiswift/mockxbmc/polib.pyc
  40. 37
    0
      kodiswift/mockxbmc/utils.py
  41. BIN
      kodiswift/mockxbmc/utils.pyc
  42. 97
    0
      kodiswift/mockxbmc/xbmc.py
  43. BIN
      kodiswift/mockxbmc/xbmc.pyc
  44. 70
    0
      kodiswift/mockxbmc/xbmcaddon.py
  45. BIN
      kodiswift/mockxbmc/xbmcaddon.pyc
  46. 67
    0
      kodiswift/mockxbmc/xbmcgui.py
  47. BIN
      kodiswift/mockxbmc/xbmcgui.pyc
  48. 88
    0
      kodiswift/mockxbmc/xbmcplugin.py
  49. BIN
      kodiswift/mockxbmc/xbmcplugin.pyc
  50. 25
    0
      kodiswift/mockxbmc/xbmcvfs.py
  51. BIN
      kodiswift/mockxbmc/xbmcvfs.pyc
  52. 155
    0
      kodiswift/module.py
  53. BIN
      kodiswift/module.pyc
  54. BIN
      kodiswift/module.pyo
  55. 357
    0
      kodiswift/plugin.py
  56. BIN
      kodiswift/plugin.pyc
  57. BIN
      kodiswift/plugin.pyo
  58. 46
    0
      kodiswift/request.py
  59. BIN
      kodiswift/request.pyc
  60. BIN
      kodiswift/request.pyo
  61. 163
    0
      kodiswift/storage.py
  62. BIN
      kodiswift/storage.pyc
  63. BIN
      kodiswift/storage.pyo
  64. 212
    0
      kodiswift/urls.py
  65. BIN
      kodiswift/urls.pyc
  66. BIN
      kodiswift/urls.pyo
  67. 572
    0
      kodiswift/xbmcmixin.py
  68. BIN
      kodiswift/xbmcmixin.pyc
  69. BIN
      kodiswift/xbmcmixin.pyo

BIN
icon.png View File


+ 83
- 0
kodiswift/__init__.py View File

@@ -0,0 +1,83 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift
4
+----------
5
+
6
+A micro framework to enable rapid development of Kodi plugins.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+from types import ModuleType
14
+
15
+try:
16
+    import xbmc
17
+    import xbmcgui
18
+    import xbmcplugin
19
+    import xbmcaddon
20
+    import xbmcvfs
21
+
22
+    CLI_MODE = False
23
+except ImportError:
24
+    CLI_MODE = True
25
+    import sys
26
+    from kodiswift.logger import log
27
+
28
+    # Mock the Kodi modules
29
+    from kodiswift.mockxbmc import xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs
30
+
31
+    class _Module(ModuleType):
32
+        """A wrapper class for a module used to override __getattr__.
33
+        This class will behave normally for any existing module attributes.
34
+        For any attributes which do not exist in the wrapped module, a mock
35
+        function will be returned. This function will also return itself
36
+        enabling multiple mock function calls.
37
+        """
38
+
39
+        def __init__(self, wrapped=None):
40
+            self.wrapped = wrapped
41
+            if wrapped:
42
+                self.__dict__.update(wrapped.__dict__)
43
+
44
+        def __getattr__(self, name):
45
+            """Returns any existing attr for the wrapped module or returns a
46
+            mock function for anything else. Never raises an AttributeError.
47
+            """
48
+            try:
49
+                return getattr(self.wrapped, name)
50
+            except AttributeError:
51
+                # noinspection PyUnusedLocal
52
+                # pylint disable=unused-argument
53
+                def func(*args, **kwargs):
54
+                    """A mock function which returns itself, enabling chainable
55
+                    function calls.
56
+                    """
57
+                    log.warning('The %s method has not been implemented on '
58
+                                'the CLI. Your code might not work properly '
59
+                                'when calling it.', name)
60
+                    return self
61
+
62
+                return func
63
+
64
+    xbmc = _Module(xbmc)
65
+    xbmcgui = _Module(xbmcgui)
66
+    xbmcplugin = _Module(xbmcplugin)
67
+    xbmcaddon = _Module(xbmcaddon)
68
+    xbmcvfs = _Module(xbmcvfs)
69
+    for m in (xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs):
70
+        name = reversed(m.__name__.rsplit('.', 1)).next()
71
+        sys.modules[name] = m
72
+
73
+from kodiswift.storage import TimedStorage
74
+from kodiswift.request import Request
75
+from kodiswift.common import (kodi_url, clean_dict, pickle_dict, unpickle_args,
76
+                              unpickle_dict, download_page)
77
+from kodiswift.constants import SortMethod
78
+from kodiswift.listitem import ListItem
79
+from kodiswift.logger import setup_log
80
+from kodiswift.module import Module
81
+from kodiswift.urls import AmbiguousUrlException, NotFoundException, UrlRule
82
+from kodiswift.xbmcmixin import XBMCMixin
83
+from kodiswift.plugin import Plugin

BIN
kodiswift/__init__.pyc View File


BIN
kodiswift/__init__.pyo View File


+ 37
- 0
kodiswift/actions.py View File

@@ -0,0 +1,37 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.actions
4
+------------------
5
+
6
+This module contains wrapper functions for Kodi built-in functions.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+
12
+__all__ = ['background', 'update_view']
13
+
14
+
15
+def background(url):
16
+    """This action will run an addon in the background for the provided URL.
17
+
18
+    See 'RunPlugin()' at
19
+    http://kodi.wiki/view/List_of_built-in_functions
20
+
21
+    Args:
22
+        url (str): Full path must be specified.
23
+            Does not work for folder plugins.
24
+
25
+    Returns:
26
+        str: String of the builtin command
27
+    """
28
+    return 'RunPlugin(%s)' % url
29
+
30
+
31
+def update_view(url):
32
+    """This action will update the current container view with provided url.
33
+
34
+    See 'Container.Update()' at
35
+    http://kodi.wiki/view/List_of_built-in_functions
36
+    """
37
+    return 'Container.Update(%s)' % url

+ 18
- 0
kodiswift/cli/__init__.py View File

@@ -0,0 +1,18 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+    kodiswift.cli
4
+    ----------------
5
+
6
+    This package contains modules that are used only when running kodiswift in
7
+    CLI mode. Nothing from this package should be called from addon code.
8
+
9
+    :copyright: (c) 2012 by Jonathan Beluch
10
+    :license: GPLv3, see LICENSE for more details.
11
+"""
12
+
13
+
14
+def Option(*args, **kwargs):
15
+    """Returns a tuple of args, kwargs passed to the function. Useful for
16
+    recording arguments for future function calls.
17
+    """
18
+    return args, kwargs

BIN
kodiswift/cli/__init__.pyc View File


+ 216
- 0
kodiswift/cli/app.py View File

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

BIN
kodiswift/cli/app.pyc View File


+ 78
- 0
kodiswift/cli/cli.py View File

@@ -0,0 +1,78 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.cli.cli
4
+------------------
5
+
6
+The main entry point for the kodiswift console script. CLI commands can be
7
+registered in this module.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import sys
15
+from optparse import OptionParser
16
+
17
+from kodiswift.cli.app import RunCommand
18
+from kodiswift.cli.create import CreateCommand
19
+
20
+# TODO: Make an ABC for Command
21
+COMMANDS = {
22
+    RunCommand.command: RunCommand,
23
+    CreateCommand.command: CreateCommand,
24
+}
25
+
26
+
27
+# TODO: Make this usage dynamic based on COMMANDS dict
28
+USAGE = """%prog <command>
29
+
30
+Commands:
31
+    create
32
+        Create a new plugin project.
33
+
34
+    run
35
+        Run an kodiswift plugin from the command line.
36
+
37
+Help:
38
+    To see options for a command, run `kodiswift <command> -h`
39
+"""
40
+
41
+
42
+def main():
43
+    """The entry point for the console script kodiswift.
44
+
45
+    The 'xbcmswift2' script is command bassed, so the second argument is always
46
+    the command to execute. Each command has its own parser options and usages.
47
+    If no command is provided or the -h flag is used without any other
48
+    commands, the general help message is shown.
49
+    """
50
+    parser = OptionParser()
51
+    if len(sys.argv) == 1:
52
+        parser.set_usage(USAGE)
53
+        parser.error('At least one command is required.')
54
+
55
+    # spy sys.argv[1] in order to use correct opts/args
56
+    command = sys.argv[1]
57
+
58
+    if command == '-h':
59
+        parser.set_usage(USAGE)
60
+        opts, args = parser.parse_args()
61
+
62
+    if command not in COMMANDS.keys():
63
+        parser.error('Invalid command')
64
+
65
+    # We have a proper command, set the usage and options list according to the
66
+    # specific command
67
+    manager = COMMANDS[command]
68
+    if hasattr(manager, 'option_list'):
69
+        for args, kwargs in manager.option_list:
70
+            parser.add_option(*args, **kwargs)
71
+    if hasattr(manager, 'usage'):
72
+        parser.set_usage(manager.usage)
73
+
74
+    opts, args = parser.parse_args()
75
+
76
+    # Since we are calling a specific comamnd's manager, we no longer need the
77
+    # actual command in sys.argv so we slice from position 1
78
+    manager.run(opts, args[1:])

BIN
kodiswift/cli/cli.pyc View File


+ 101
- 0
kodiswift/cli/console.py View File

@@ -0,0 +1,101 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+    kodiswift.cli.console
4
+    ----------------------
5
+
6
+    This module contains code to handle CLI interaction.
7
+
8
+    :copyright: (c) 2012 by Jonathan Beluch
9
+    :license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import print_function, absolute_import
12
+
13
+
14
+def get_max_len(items):
15
+    """Returns the max of the lengths for the provided items"""
16
+    try:
17
+        return max(len(item) for item in items)
18
+    except ValueError:
19
+        return 0
20
+
21
+
22
+def display_listitems(items, url):
23
+    """Displays a list of items along with the index to enable a user
24
+    to select an item.
25
+
26
+    Args:
27
+        items (list[kodiswift.ListItem]):
28
+        url (str):
29
+    """
30
+    if len(items) == 2 and items[0].label == '..' and items[1].played:
31
+        display_video(items)
32
+    else:
33
+        label_width = get_max_len(item.label for item in items)
34
+        num_width = len(str(len(items)))
35
+        output = []
36
+        for i, item in enumerate(items):
37
+            output.append('[%s] %s (%s)' % (
38
+                str(i).rjust(num_width),
39
+                item.label.ljust(label_width),
40
+                item.path))
41
+
42
+        line_width = get_max_len(output)
43
+        output.append('-' * line_width)
44
+
45
+        header = [
46
+            '',
47
+            '=' * line_width,
48
+            'Current URL: %s' % url,
49
+            '-' * line_width,
50
+            '%s %s Path' % ('#'.center(num_width + 2),
51
+                            'Label'.ljust(label_width)),
52
+            '-' * line_width,
53
+        ]
54
+        print('\n'.join(header + output))
55
+
56
+
57
+def display_video(items):
58
+    """Prints a message for a playing video and displays the parent
59
+    listitem.
60
+    """
61
+    parent_item, played_item = items
62
+
63
+    title_line = 'Playing Media %s (%s)' % (played_item.label, played_item.path.encode("utf8"))
64
+    parent_line = '[0] %s (%s)' % (parent_item.label, parent_item.path)
65
+    line_width = get_max_len([title_line, parent_line])
66
+
67
+    output = [
68
+        '-' * line_width,
69
+        title_line,
70
+        '-' * line_width,
71
+        parent_line.encode("utf8") if isinstance(parent_line, unicode) else parent_line ,
72
+    ]
73
+    print('\n'.join(output))
74
+
75
+
76
+def get_user_choice(items):
77
+    """Returns the selected item from provided items or None if 'q' was
78
+    entered for quit.
79
+    """
80
+    choice = raw_input('Choose an item or "q" to quit: ')
81
+    while choice != 'q':
82
+        try:
83
+            item = items[int(choice)]
84
+            print()  # Blank line for readability between interactive views
85
+            return item
86
+        except ValueError:
87
+            # Passed something that couldn't be converted with int()
88
+            choice = raw_input('You entered a non-integer. Choice must be an'
89
+                               ' integer or "q": ')
90
+        except IndexError:
91
+            # Passed an integer that was out of range of the list of urls
92
+            choice = raw_input('You entered an invalid integer. Choice must '
93
+                               'be from above url list or "q": ')
94
+    return None
95
+
96
+
97
+def continue_or_quit():
98
+    """Prints an exit message and returns False if the user wants to
99
+    quit.
100
+    """
101
+    return raw_input('Enter to continue or "q" to quit') != 'q'

BIN
kodiswift/cli/console.pyc View File


+ 192
- 0
kodiswift/cli/create.py View File

@@ -0,0 +1,192 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.cli.create
4
+---------------------
5
+
6
+This module contains the code to initialize a new Kodi addon project.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import print_function, absolute_import
12
+
13
+import os
14
+import readline
15
+import string
16
+from getpass import getpass
17
+from optparse import OptionParser
18
+from os import getcwd
19
+from shutil import copytree, ignore_patterns
20
+from xml.sax import saxutils
21
+
22
+
23
+class CreateCommand(object):
24
+    """A CLI command to initialize a new Kodi addon project."""
25
+    command = 'create'
26
+    usage = '%prog create'
27
+
28
+    # noinspection PyUnusedLocal
29
+    @staticmethod
30
+    def run(opts, args):
31
+        """Required run function for the 'create' CLI command."""
32
+        create_new_project()
33
+
34
+
35
+# Path to skeleton file templates dir
36
+SKEL = os.path.join(os.path.dirname(__file__), 'data')
37
+
38
+
39
+def error_msg(msg):
40
+    """A decorator that sets the error_message attribute of the decorated
41
+    function to the provided value.
42
+    """
43
+    def decorator(func):
44
+        """Sets the error_message attribute on the provided function"""
45
+        func.error_message = msg
46
+        return func
47
+    return decorator
48
+
49
+
50
+def parse_cli():
51
+    """Currently only one positional arg, create."""
52
+    parser = OptionParser()
53
+    return parser.parse_args()
54
+
55
+
56
+@error_msg('** Value must be non-blank.')
57
+def validate_nonblank(value):
58
+    """A callable that retunrs the value passed"""
59
+    return value
60
+
61
+
62
+@error_msg('** Value must contain only letters or underscores.')
63
+def validate_pluginid(value):
64
+    """Returns True if the provided value is a valid plugin id"""
65
+    valid = string.ascii_letters + string.digits + '.' + '_'
66
+    return all(c in valid for c in value)
67
+
68
+
69
+@error_msg('** The provided path must be an existing folder.')
70
+def validate_isfolder(value):
71
+    """Returns true if the provided path is an existing directory"""
72
+    return os.path.isdir(value)
73
+
74
+
75
+def get_valid_value(prompt, validator, default=None):
76
+    """Displays the provided prompt and gets input from the user. This behavior
77
+    loops indefinitely until the provided validator returns True for the user
78
+    input. If a default value is provided, it will be used only if the user
79
+    hits Enter and does not provide a value.
80
+
81
+    If the validator callable has an error_message attribute, it will be
82
+    displayed for an invalid value, otherwise a generic message is used.
83
+    """
84
+    ans = get_value(prompt, default)
85
+    while not validator(ans):
86
+        try:
87
+            print(validator.error_message)
88
+        except AttributeError:
89
+            print('Invalid value.')
90
+        ans = get_value(prompt, default)
91
+
92
+    return ans
93
+
94
+
95
+def get_value(prompt, default=None, hidden=False):
96
+    """Displays the provided prompt and returns the input from the user. If the
97
+    user hits Enter and there is a default value provided, the default is
98
+    returned.
99
+    """
100
+    _prompt = '%s : ' % prompt
101
+    if default:
102
+        _prompt = '%s [%s]: ' % (prompt, default)
103
+
104
+    if hidden:
105
+        ans = getpass(_prompt)
106
+    else:
107
+        ans = raw_input(_prompt)
108
+
109
+    # If user hit Enter and there is a default value
110
+    if not ans and default:
111
+        ans = default
112
+    return ans
113
+
114
+
115
+def update_file(filename, items):
116
+    """Edits the given file in place, replacing any instances of {key} with the
117
+    appropriate value from the provided items dict. If the given filename ends
118
+    with ".xml" values will be quoted and escaped for XML.
119
+    """
120
+    # TODO: Implement something in the templates to denote whether the value
121
+    # being replaced is an XML attribute or a value. Perhaps move to dyanmic
122
+    # XML tree building rather than string replacement.
123
+    should_escape = filename.endswith('addon.xml')
124
+
125
+    with open(filename, 'r') as inp:
126
+        text = inp.read()
127
+
128
+    for key, val in items.items():
129
+        if should_escape:
130
+            val = saxutils.quoteattr(val)
131
+        text = text.replace('{%s}' % key, val)
132
+    output = text
133
+
134
+    with open(filename, 'w') as out:
135
+        out.write(output)
136
+
137
+
138
+def create_new_project():
139
+    """Creates a new Kodi Plugin directory based on user input"""
140
+    readline.parse_and_bind('tab: complete')
141
+
142
+    print("""
143
+    kodiswift - A micro-framework for creating Kodi plugins.
144
+    xbmc@jonathanbeluch.com
145
+    --
146
+""")
147
+    print('I\'m going to ask you a few questions to get this project started.')
148
+
149
+    opts = {}
150
+
151
+    # Plugin Name
152
+    opts['plugin_name'] = get_valid_value(
153
+        'What is your plugin name?',
154
+        validate_nonblank
155
+    )
156
+
157
+    # Plugin ID
158
+    opts['plugin_id'] = get_valid_value(
159
+        'Enter your plugin id.',
160
+        validate_pluginid,
161
+        'plugin.video.%s' % (opts['plugin_name'].lower().replace(' ', ''))
162
+    )
163
+
164
+    # Parent Directory
165
+    opts['parent_dir'] = get_valid_value(
166
+        'Enter parent folder (where to create project)',
167
+        validate_isfolder,
168
+        getcwd()
169
+    )
170
+
171
+    # Parent Directory
172
+    opts['plugin_dir'] = os.path.join(opts['parent_dir'], opts['plugin_id'])
173
+    assert not os.path.isdir(opts['plugin_dir']), \
174
+        'A folder named %s already exists in %s.' % (opts['plugin_id'],
175
+                                                     opts['parent_dir'])
176
+
177
+    # Provider
178
+    opts['provider_name'] = get_valid_value(
179
+        'Enter provider name',
180
+        validate_nonblank,
181
+    )
182
+
183
+    # Create the project folder by copying over skel
184
+    copytree(SKEL, opts['plugin_dir'], ignore=ignore_patterns('*.pyc'))
185
+
186
+    # Walk through all the new files and fill in with out options
187
+    for root, _, files in os.walk(opts['plugin_dir']):
188
+        for filename in files:
189
+            update_file(os.path.join(root, filename), opts)
190
+
191
+    print('Projects successfully created in %s.' % opts['plugin_dir'])
192
+    print('Done.')

BIN
kodiswift/cli/create.pyc View File


+ 19
- 0
kodiswift/cli/data/addon.py View File

@@ -0,0 +1,19 @@
1
+# -*- coding: utf-8 -*-
2
+from kodiswift import Plugin
3
+
4
+
5
+plugin = Plugin()
6
+
7
+
8
+@plugin.route('/')
9
+def index():
10
+    item = {
11
+        'label': 'Hello Kodi!',
12
+        'path': 'http://example.com/video.mp4',
13
+        'is_playable': True
14
+    }
15
+    return [item]
16
+
17
+
18
+if __name__ == '__main__':
19
+    plugin.run()

BIN
kodiswift/cli/data/addon.pyc View File


+ 17
- 0
kodiswift/cli/data/addon.xml View File

@@ -0,0 +1,17 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+<addon id={plugin_id} name={plugin_name} version="0.0.1" provider-name={provider_name}>
3
+  <requires>
4
+    <import addon="xbmc.python" version="2.24.0" />
5
+    <import addon="script.module.kodiswift" version="0.0.1" />
6
+  </requires>
7
+  <extension point="xbmc.python.pluginsource" library="addon.py">
8
+    <provides>video</provides>
9
+  </extension>
10
+  <extension point="xbmc.addon.metadata">
11
+    <platform>all</platform>
12
+    <language>en</language>
13
+    <summary lang="en">Summary for {plugin_name}</summary>
14
+    <description lang="en">Description for {plugin_name}</description>
15
+    <disclaimer lang="en">Disclaimer for {plugin_name}</disclaimer>
16
+  </extension>
17
+</addon>

+ 1
- 0
kodiswift/cli/data/resources/__init__.py View File

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

BIN
kodiswift/cli/data/resources/__init__.pyc View File


+ 26
- 0
kodiswift/cli/data/resources/language/English/strings.po View File

@@ -0,0 +1,26 @@
1
+# Kodi Media Center language file
2
+# Addon Name: {plugin_name}
3
+# Addon id: {plugin_id}
4
+# Addon Provider: {provider_name}
5
+msgid ""
6
+msgstr ""
7
+"Project-Id-Version: Kodi Addons\n"
8
+"Report-Msgid-Bugs-To: \n"
9
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
10
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11
+"Last-Translator: \n"
12
+"Language-Team: \n"
13
+"MIME-Version: 1.0\n"
14
+"Content-Type: text/plain; charset=UTF-8\n"
15
+"Content-Transfer-Encoding: 8bit\n"
16
+"Language: en\n"
17
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
18
+
19
+# strings 30000 thru 30999 reserved for plugins and plugin settings
20
+# strings 31000 thru 31999 reserved for skins
21
+# strings 32000 thru 32999 reserved for scripts
22
+# strings 33000 thru 33999 reserved for common strings used in add-ons
23
+
24
+msgctxt "#33000"
25
+msgid "Hello Kodi"
26
+msgstr ""

+ 1
- 0
kodiswift/cli/data/resources/lib/__init__.py View File

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

BIN
kodiswift/cli/data/resources/lib/__init__.pyc View File


+ 147
- 0
kodiswift/common.py View File

@@ -0,0 +1,147 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.common
4
+-----------------
5
+
6
+This module contains some common helpful functions.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import urllib
14
+
15
+try:
16
+    import cPickle as pickle
17
+except ImportError:
18
+    import pickle
19
+
20
+__all__ = ['clean_dict', 'kodi_url', 'unpickle_args', 'pickle_dict',
21
+           'unpickle_dict', 'download_page', 'Modes']
22
+
23
+
24
+class Modes(object):
25
+    ONCE = 'ONCE'
26
+    CRAWL = 'CRAWL'
27
+    INTERACTIVE = 'INTERACTIVE'
28
+
29
+
30
+def kodi_url(url, **options):
31
+    """Appends key/val pairs to the end of a URL. Useful for passing arbitrary
32
+    HTTP headers to Kodi to be used when fetching a media resource, e.g.
33
+    cookies.
34
+
35
+    Args:
36
+        url (str):
37
+        **options (dict):
38
+
39
+    Returns:
40
+        str:
41
+    """
42
+    options = urllib.urlencode(options)
43
+    if options:
44
+        return url + '|' + options
45
+    return url
46
+
47
+
48
+def clean_dict(data):
49
+    """Remove keys with a value of None
50
+
51
+    Args:
52
+        data (dict):
53
+
54
+    Returns:
55
+        dict:
56
+    """
57
+    return dict((k, v) for k, v in data.items() if v is not None)
58
+
59
+
60
+def pickle_dict(items):
61
+    """Convert `items` values into pickled values.
62
+
63
+    Args:
64
+        items (dict): A dictionary
65
+
66
+    Returns:
67
+        dict: Values which aren't instances of basestring are pickled. Also,
68
+            a new key '_pickled' contains a comma separated list of keys
69
+            corresponding to the pickled values.
70
+    """
71
+    ret = {}
72
+    pickled_keys = []
73
+    for k, v in items.items():
74
+        if isinstance(v, basestring):
75
+            ret[k] = v
76
+        else:
77
+            pickled_keys.append(k)
78
+            ret[k] = pickle.dumps(v)
79
+    if pickled_keys:
80
+        ret['_pickled'] = ','.join(pickled_keys)
81
+    return ret
82
+
83
+
84
+def unpickle_args(items):
85
+    """Takes a dict and un-pickles values whose keys are found in a '_pickled'
86
+    key.
87
+
88
+    >>> unpickle_args({'_pickled': ['foo'], 'foo': ['I3%0A.']})
89
+    {'foo': 3}
90
+
91
+    Args:
92
+        items (dict): A pickled dictionary.
93
+
94
+    Returns:
95
+        dict: Dict with values un-pickled.
96
+    """
97
+    # Technically there can be more than one _pickled value. At this point
98
+    # we'll just use the first one
99
+    pickled = items.pop('_pickled', None)
100
+    if pickled is None:
101
+        return items
102
+
103
+    pickled_keys = pickled[0].split(',')
104
+    ret = {}
105
+    for k, v in items.items():
106
+        if k in pickled_keys:
107
+            ret[k] = [pickle.loads(val) for val in v]
108
+        else:
109
+            ret[k] = v
110
+    return ret
111
+
112
+
113
+def unpickle_dict(items):
114
+    """un-pickles a dictionary that was pickled with `pickle_dict`.
115
+
116
+    Args:
117
+        items (dict): A pickled dictionary.
118
+
119
+    Returns:
120
+        dict: An un-pickled dictionary.
121
+    """
122
+    pickled_keys = items.pop('_pickled', '').split(',')
123
+    ret = {}
124
+    for k, v in items.items():
125
+        if k in pickled_keys:
126
+            ret[k] = pickle.loads(v)
127
+        else:
128
+            ret[k] = v
129
+    return ret
130
+
131
+
132
+def download_page(url, data=None):
133
+    """Returns the response for the given url. The optional data argument is
134
+    passed directly to urlopen.
135
+
136
+    Args:
137
+        url (str): The URL to read.
138
+        data (Optional[any]): If given, a POST request will be made with
139
+            :param:`data` as the POST body.
140
+
141
+    Returns:
142
+        str: The results of requesting the URL.
143
+    """
144
+    conn = urllib.urlopen(url, data)
145
+    resp = conn.read()
146
+    conn.close()
147
+    return resp

BIN
kodiswift/common.pyc View File


BIN
kodiswift/common.pyo View File


+ 76
- 0
kodiswift/constants.py View File

@@ -0,0 +1,76 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.constants
4
+--------------------
5
+
6
+This module contains some helpful constants which ease interaction
7
+with Kodi.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+from kodiswift import xbmcplugin
15
+
16
+__all__ = ['SortMethod']
17
+
18
+
19
+class SortMethod(object):
20
+    """Static class to hold all of the available sort methods. The prefix
21
+    of 'SORT_METHOD_' is stripped.
22
+
23
+    e.g. SORT_METHOD_TITLE becomes SortMethod.TITLE
24
+    """
25
+    ALBUM = xbmcplugin.SORT_METHOD_ALBUM
26
+    ALBUM_IGNORE_THE = xbmcplugin.SORT_METHOD_ALBUM_IGNORE_THE
27
+    ARTIST = xbmcplugin.SORT_METHOD_ARTIST
28
+    ARTIST_IGNORE_THE = xbmcplugin.SORT_METHOD_ARTIST_IGNORE_THE
29
+    BITRATE = xbmcplugin.SORT_METHOD_BITRATE
30
+    CHANNEL = xbmcplugin.SORT_METHOD_CHANNEL
31
+    COUNTRY = xbmcplugin.SORT_METHOD_COUNTRY
32
+    DATE = xbmcplugin.SORT_METHOD_DATE
33
+    DATEADDED = xbmcplugin.SORT_METHOD_DATEADDED
34
+    DATE_TAKEN = xbmcplugin.SORT_METHOD_DATE_TAKEN
35
+    DRIVE_TYPE = xbmcplugin.SORT_METHOD_DRIVE_TYPE
36
+    DURATION = xbmcplugin.SORT_METHOD_DURATION
37
+    EPISODE = xbmcplugin.SORT_METHOD_EPISODE
38
+    FILE = xbmcplugin.SORT_METHOD_FILE
39
+    FULLPATH = xbmcplugin.SORT_METHOD_FULLPATH
40
+    GENRE = xbmcplugin.SORT_METHOD_GENRE
41
+    LABEL = xbmcplugin.SORT_METHOD_LABEL
42
+    LABEL_IGNORE_FOLDERS = xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS
43
+    LABEL_IGNORE_THE = xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE
44
+    LASTPLAYED = xbmcplugin.SORT_METHOD_LASTPLAYED
45
+    LISTENERS = xbmcplugin.SORT_METHOD_LISTENERS
46
+    MPAA_RATING = xbmcplugin.SORT_METHOD_MPAA_RATING
47
+    NONE = xbmcplugin.SORT_METHOD_NONE
48
+    PLAYCOUNT = xbmcplugin.SORT_METHOD_PLAYCOUNT
49
+    PLAYLIST_ORDER = xbmcplugin.SORT_METHOD_PLAYLIST_ORDER
50
+    PRODUCTIONCODE = xbmcplugin.SORT_METHOD_PRODUCTIONCODE
51
+    PROGRAM_COUNT = xbmcplugin.SORT_METHOD_PROGRAM_COUNT
52
+    SIZE = xbmcplugin.SORT_METHOD_SIZE
53
+    SONG_RATING = xbmcplugin.SORT_METHOD_SONG_RATING
54
+    STUDIO = xbmcplugin.SORT_METHOD_STUDIO
55
+    STUDIO_IGNORE_THE = xbmcplugin.SORT_METHOD_STUDIO_IGNORE_THE
56
+    TITLE = xbmcplugin.SORT_METHOD_TITLE
57
+    TITLE_IGNORE_THE = xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE
58
+    TRACKNUM = xbmcplugin.SORT_METHOD_TRACKNUM
59
+    UNSORTED = xbmcplugin.SORT_METHOD_UNSORTED
60
+    VIDEO_RATING = xbmcplugin.SORT_METHOD_VIDEO_RATING
61
+    VIDEO_RUNTIME = xbmcplugin.SORT_METHOD_VIDEO_RUNTIME
62
+    VIDEO_SORT_TITLE = xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE
63
+    VIDEO_SORT_TITLE_IGNORE_THE = xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE
64
+    VIDEO_TITLE = xbmcplugin.SORT_METHOD_VIDEO_TITLE
65
+    VIDEO_USER_RATING = xbmcplugin.SORT_METHOD_VIDEO_USER_RATING
66
+    VIDEO_YEAR = xbmcplugin.SORT_METHOD_VIDEO_YEAR
67
+
68
+    @classmethod
69
+    def from_string(cls, sort_method):
70
+        """Returns the sort method specified. sort_method is case insensitive.
71
+        Will raise an AttributeError if the provided sort_method does not
72
+        exist.
73
+
74
+        >>> SortMethod.from_string('title')
75
+        """
76
+        return getattr(cls, sort_method.upper())

BIN
kodiswift/constants.pyc View File


BIN
kodiswift/constants.pyo View File


+ 339
- 0
kodiswift/listitem.py View File

@@ -0,0 +1,339 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.listitem
4
+------------------
5
+
6
+This module contains the ListItem class, which acts as a wrapper
7
+for xbmcgui.ListItem.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import warnings
15
+
16
+from kodiswift import xbmcgui
17
+
18
+__all__ = ['ListItem']
19
+
20
+
21
+class ListItem(object):
22
+    """A wrapper for the xbmcgui.ListItem class. The class keeps track
23
+    of any set properties that xbmcgui doesn't expose getters for.
24
+    """
25
+
26
+    def __init__(self, label=None, label2=None, icon=None, thumbnail=None,
27
+                 path=None):
28
+        """Defaults are an emtpy string since xbmcgui.ListItem will not
29
+        accept None.
30
+        """
31
+        self._listitem = xbmcgui.ListItem(label=label, label2=label2, path=path)
32
+
33
+        # The docs have the thumbnail property set as thumb
34
+        # http://mirrors.kodi.tv/docs/python-docs/16.x-jarvis/xbmcgui.html#ListItem-setArt
35
+        self._art = {'icon': icon, 'thumb': thumbnail}
36
+        self._icon = icon
37
+        self._path = path
38
+        self._thumbnail = thumbnail
39
+        self._context_menu_items = []
40
+        self._played = False
41
+        self._playable = False
42
+        self.is_folder = True
43
+
44
+    def get_context_menu_items(self):
45
+        """Returns the list of currently set context_menu items."""
46
+        return self._context_menu_items
47
+
48
+    def add_context_menu_items(self, items, replace_items=False):
49
+        """Adds context menu items. If replace_items is True all
50
+        previous context menu items will be removed.
51
+        """
52
+        for label, action in items:
53
+            assert isinstance(label, basestring)
54
+            assert isinstance(action, basestring)
55
+        if replace_items:
56
+            self._context_menu_items = []
57
+        self._context_menu_items.extend(items)
58
+        self._listitem.addContextMenuItems(items, replace_items)
59
+
60
+    @property
61
+    def label(self):
62
+        """
63
+        Returns:
64
+            str:
65
+        """
66
+        return self._listitem.getLabel()
67
+
68
+    @label.setter
69
+    def label(self, value):
70
+        """
71
+        Args:
72
+            value (str):
73
+        """
74
+        self._listitem.setLabel(value)
75
+
76
+    def get_label(self):
77
+        warnings.warn('get_label is deprecated, use label property',
78
+                      DeprecationWarning)
79
+        return self.label
80
+
81
+    def set_label(self, value):
82
+        warnings.warn('set_label is deprecated, use label property',
83
+                      DeprecationWarning)
84
+        return self._listitem.setLabel(value)
85
+
86
+    @property
87
+    def label2(self):
88
+        return self._listitem.getLabel2()
89
+
90
+    @label2.setter
91
+    def label2(self, value):
92
+        self._listitem.setLabel2(value)
93
+
94
+    def get_label2(self):
95
+        warnings.warn('get_label2 is deprecated, use label2 property',
96
+                      DeprecationWarning)
97
+        return self.label2
98
+
99
+    def set_label2(self, value):
100
+        warnings.warn('set_label2 is deprecated, use label2 property',
101
+                      DeprecationWarning)
102
+        return self._listitem.setLabel2(value)
103
+
104
+    @property
105
+    def selected(self):
106
+        return self._listitem.isSelected()
107
+
108
+    @selected.setter
109
+    def selected(self, value):
110
+        self._listitem.select(value)
111
+
112
+    def is_selected(self):
113
+        warnings.warn('is_selected is deprecated, use selected property',
114
+                      DeprecationWarning)
115
+        return self._listitem.isSelected()
116
+
117
+    def select(self, selected_status=True):
118
+        warnings.warn('select is deprecated, use selected property',
119
+                      DeprecationWarning)
120
+        return self._listitem.select(selected_status)
121
+
122
+    @property
123
+    def icon(self):
124
+        return self._art.get('icon')
125
+
126
+    @icon.setter
127
+    def icon(self, value):
128
+        self._art['icon'] = value
129
+        self._listitem.setArt(self._art)
130
+
131
+    def get_icon(self):
132
+        warnings.warn('get_icon is deprecated, use icon property',
133
+                      DeprecationWarning)
134
+        return self.icon
135
+
136
+    def set_icon(self, icon):
137
+        warnings.warn('set_icon is deprecated, use icon property',
138
+                      DeprecationWarning)
139
+        self.icon = icon
140
+        return self.icon
141
+
142
+    @property
143
+    def thumbnail(self):
144
+        return self._art.get('thumb')
145
+
146
+    @thumbnail.setter
147
+    def thumbnail(self, value):
148
+        self._art['thumb'] = value
149
+        self._listitem.setArt(self._art)
150
+
151
+    def get_thumbnail(self):
152
+        warnings.warn('get_thumbnail is deprecated, use thumbnail property',
153
+                      DeprecationWarning)
154
+        return self.thumbnail
155
+
156
+    def set_thumbnail(self, thumbnail):
157
+        warnings.warn('set_thumbnail is deprecated, use thumbnail property',
158
+                      DeprecationWarning)
159
+        self.thumbnail = thumbnail
160
+        return self.thumbnail
161
+
162
+    @property
163
+    def poster(self):
164
+        return self._art.get('poster')
165
+
166
+    @poster.setter
167
+    def poster(self, value):
168
+        self._art['poster'] = value
169
+        self._listitem.setArt(self._art)
170
+
171
+    @property
172
+    def path(self):
173
+        return self._path
174
+
175
+    @path.setter
176
+    def path(self, value):
177
+        self._path = value
178
+        self._listitem.setPath(value)
179
+
180
+    def get_path(self):
181
+        warnings.warn('get_path is deprecated, use path property',
182
+                      DeprecationWarning)
183
+        return self._path
184
+
185
+    def set_path(self, path):
186
+        warnings.warn('set_path is deprecated, use path property',
187
+                      DeprecationWarning)
188
+        self._path = path
189
+        return self._listitem.setPath(path)
190
+
191
+    @property
192
+    def playable(self):
193
+        return self._playable
194
+
195
+    @playable.setter
196
+    def playable(self, value):
197
+        self._playable = value
198
+        is_playable = 'true' if self._playable else 'false'
199
+        self.set_property('isPlayable', is_playable)
200
+
201
+    def get_is_playable(self):
202
+        warnings.warn('get_is_playable is deprecated, use playable property',
203
+                      DeprecationWarning)
204
+        return self._playable
205
+
206
+    def set_is_playable(self, is_playable):
207
+        warnings.warn('set_is_playable is deprecated, use playable property',
208
+                      DeprecationWarning)
209
+        self._playable = is_playable
210
+        value = 'false'
211
+        if is_playable:
212
+            value = 'true'
213
+        self.set_property('isPlayable', value)
214
+
215
+    @property
216
+    def played(self):
217
+        return self._played
218
+
219
+    @played.setter
220
+    def played(self, value):
221
+        self._played = value
222
+
223
+    def set_played(self, was_played):
224
+        """Sets the played status of the listitem.
225
+
226
+        Used to differentiate between a resolved video versus a playable item.
227
+        Has no effect on Kodi, it is strictly used for kodiswift.
228
+        """
229
+        warnings.warn('set_played is deprecated, use played property',
230
+                      DeprecationWarning)
231
+        self._played = was_played
232
+
233
+    def get_played(self):
234
+        warnings.warn('get_played is deprecated, use played property',
235
+                      DeprecationWarning)
236
+        return self._played
237
+
238
+    @property
239
+    def art(self):
240
+        return self._art
241
+
242
+    @art.setter
243
+    def art(self, value):
244
+        self._art = value
245
+        self._listitem.setArt(value)
246
+
247
+    def set_art(self, value):
248
+        self._art = value
249
+        self._listitem.setArt(value)
250
+
251
+    def set_info(self, info_type, info_labels):
252
+        """Sets the listitem's info"""
253
+        return self._listitem.setInfo(info_type, info_labels)
254
+
255
+    def get_property(self, key):
256
+        """Returns the property associated with the given key"""
257
+        return self._listitem.getProperty(key)
258
+
259
+    def set_property(self, key, value):
260
+        """Sets a property for the given key and value"""
261
+        return self._listitem.setProperty(key, value)
262
+
263
+    def add_stream_info(self, stream_type, stream_values):
264
+        """Adds stream details"""
265
+        return self._listitem.addStreamInfo(stream_type, stream_values)
266
+
267
+    def as_tuple(self):
268
+        """Returns a tuple of list item properties:
269
+            (path, the wrapped xbmcgui.ListItem, is_folder)
270
+        """
271
+        return self.path, self._listitem, self.is_folder
272
+
273
+    def as_xbmc_listitem(self):
274
+        """Returns the wrapped xbmcgui.ListItem"""
275
+        return self._listitem
276
+
277
+    @classmethod
278
+    def from_dict(cls, label=None, label2=None, icon=None, thumbnail=None,
279
+                  path=None, selected=None, info=None, properties=None,
280
+                  context_menu=None, replace_context_menu=False,
281
+                  is_playable=None, info_type='video', stream_info=None,
282
+                  **kwargs):
283
+        """A ListItem constructor for setting a lot of properties not
284
+        available in the regular __init__ method. Useful to collect all
285
+        the properties in a dict and then use the **dct to call this
286
+        method.
287
+        """
288
+        # TODO(Sinap): Should this just use **kwargs? or should art be a dict?
289
+        listitem = cls(label, label2, path=path)
290
+        listitem.art = {
291
+            'icon': icon,
292
+            'thumb': thumbnail,
293
+            'poster': kwargs.get('poster'),
294
+            'banner': kwargs.get('banner'),
295
+            'fanart': kwargs.get('fanart'),
296
+            'landscape': kwargs.get('landscape'),
297
+        }
298
+
299
+        if selected is not None:
300
+            listitem.selected = selected
301
+
302
+        if info:
303
+            listitem.set_info(info_type, info)
304
+
305
+        if is_playable:
306
+            listitem.playable = True
307
+            listitem.is_folder = False # III
308
+
309
+        if properties:
310
+            # Need to support existing tuples, but prefer to have a dict for
311
+            # properties.
312
+            if hasattr(properties, 'items'):
313
+                properties = properties.items()
314
+            for key, val in properties:
315
+                listitem.set_property(key, val)
316
+
317
+        if stream_info:
318
+            for stream_type, stream_values in stream_info.items():
319
+                listitem.add_stream_info(stream_type, stream_values)
320
+
321
+        if context_menu:
322
+            listitem.add_context_menu_items(context_menu, replace_context_menu)
323
+
324
+        return listitem
325
+
326
+    def __eq__(self, other):
327
+        if not isinstance(other, ListItem):
328
+            raise NotImplementedError
329
+        self_props = (self.label, self.label2, self.art, self.path,
330
+                      self.playable, self.selected, self.played,)
331
+        other_props = (other.label, other.label2, other.art, other.path,
332
+                       other.playable, other.selected, other.played,)
333
+        return self_props == other_props
334
+
335
+    def __str__(self):
336
+        return ('%s (%s)' % (self.label, self.path)).encode('utf-8')
337
+
338
+    def __repr__(self):
339
+        return ("<ListItem '%s'>" % self.label).encode('utf-8')

BIN
kodiswift/listitem.pyc View File


BIN
kodiswift/listitem.pyo View File


+ 104
- 0
kodiswift/logger.py View File

@@ -0,0 +1,104 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.log
4
+--------------
5
+
6
+This module contains the kodiswift logger as well as a convenience
7
+method for creating new loggers.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import logging
15
+
16
+from kodiswift import CLI_MODE
17
+
18
+__all__ = ['setup_log', 'GLOBAL_LOG_LEVEL', 'log']
19
+
20
+# TODO: Add logging to a file as well when on CLI with lowest threshold
21
+#       possible
22
+# fh = logging.FileHandler('log_filename.txt')
23
+# fh.setLevel(logging.DEBUG)
24
+# fh.setFormatter(formatter)
25
+# log.addHandler(fh)
26
+# TODO: Allow a global flag to set logging level when dealing with Kodi
27
+# TODO: Add -q and -v flags to CLI to quiet or enable more verbose logging
28
+
29
+
30
+class XBMCFilter(object):
31
+    """A logging filter that streams to STDOUT or to the xbmc log if
32
+    running inside Kodi.
33
+    """
34
+    python_to_xbmc = {
35
+        'DEBUG': 'LOGDEBUG',
36
+        'INFO': 'LOGNOTICE',
37
+        'WARNING': 'LOGWARNING',
38
+        'ERROR': 'LOGERROR',
39
+        'CRITICAL': 'LOGSEVERE',
40
+    }
41
+
42
+    xbmc_levels = {
43
+        'LOGDEBUG': 0,
44
+        'LOGINFO': 1,
45
+        'LOGNOTICE': 2,
46
+        'LOGWARNING': 3,
47
+        'LOGERROR': 4,
48
+        'LOGSEVERE': 5,
49
+        'LOGFATAL': 6,
50
+        'LOGNONE': 7,
51
+    }
52
+
53
+    def __init__(self, prefix):
54
+        self.prefix = prefix
55
+
56
+    def filter(self, record):
57
+        """Returns True for all records if running in the CLI, else returns
58
+        True.
59
+
60
+        When running inside Kodi it calls the xbmc.log() method and prevents
61
+        the message from being double printed to STDOUT.
62
+        """
63
+
64
+        # When running in Kodi, any logged statements will be double printed
65
+        # since we are calling xbmc.log() explicitly. Therefore we return False
66
+        # so every log message is filtered out and not printed again.
67
+        if CLI_MODE:
68
+            return True
69
+        else:
70
+            # Must not be imported until here because of import order issues
71
+            # when running in CLI
72
+            from kodiswift import xbmc
73
+            xbmc_level = XBMCFilter.xbmc_levels.get(
74
+                XBMCFilter.python_to_xbmc.get(record.levelname))
75
+            xbmc.log('%s%s' % (self.prefix, record.getMessage()), xbmc_level)
76
+            return False
77
+
78
+
79
+if CLI_MODE:
80
+    GLOBAL_LOG_LEVEL = logging.INFO
81
+else:
82
+    GLOBAL_LOG_LEVEL = logging.DEBUG
83
+
84
+
85
+def setup_log(name):
86
+    """Returns a logging instance for the provided name. The returned
87
+    object is an instance of logging.Logger. Logged messages will be
88
+    printed to stderr when running in the CLI, or forwarded to Kodi's
89
+    log when running in Kodi mode.
90
+    """
91
+    _log = logging.getLogger(name)
92
+    _log.setLevel(GLOBAL_LOG_LEVEL)
93
+    handler = logging.StreamHandler()
94
+    formatter = logging.Formatter(
95
+        '%(asctime)s - %(levelname)s - [%(name)s] %(message)s')
96
+    handler.setFormatter(formatter)
97
+    _log.addHandler(handler)
98
+    _log.addFilter(XBMCFilter('[%s] ' % name))
99
+    return _log
100
+
101
+
102
+# The kodiswift log
103
+# Plugin writers should use plugin.log instead.
104
+log = setup_log('kodiswift')

BIN
kodiswift/logger.pyc View File


BIN
kodiswift/logger.pyo View File


+ 1
- 0
kodiswift/mockxbmc/__init__.py View File

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

BIN
kodiswift/mockxbmc/__init__.pyc View File


+ 1835
- 0
kodiswift/mockxbmc/polib.py
File diff suppressed because it is too large
View File


BIN
kodiswift/mockxbmc/polib.pyc View File


+ 37
- 0
kodiswift/mockxbmc/utils.py View File

@@ -0,0 +1,37 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import absolute_import
3
+
4
+import os
5
+from xml.dom.minidom import parse
6
+
7
+import kodiswift.mockxbmc.polib as polib
8
+
9
+
10
+def load_addon_strings(addon, filename):
11
+    """This is not an official Kodi method, it is here to facilitate
12
+    mocking up the other methods when running outside of Kodi."""
13
+    def get_strings(fn):
14
+        if os.path.exists(filename):
15
+            po = polib.pofile(fn)
16
+            return {entry.msgctxt[1:]: entry.msgid for entry in po}
17
+        else:
18
+            fn = os.path.splitext(fn)[0] + '.xml'
19
+            xml = parse(fn)
20
+            strings = {tag.getAttribute('id'): tag.firstChild.data
21
+                       for tag in xml.getElementsByTagName('string')}
22
+            return strings
23
+    addon._strings = get_strings(filename)
24
+
25
+
26
+def get_addon_id(addon_xml):
27
+    """Parses an addon id from the given addon.xml filename."""
28
+    xml = parse(addon_xml)
29
+    addon_node = xml.getElementsByTagName('addon')[0]
30
+    return addon_node.getAttribute('id')
31
+
32
+
33
+def get_addon_name(addon_xml):
34
+    """Parses an addon name from the given addon.xml filename."""
35
+    xml = parse(addon_xml)
36
+    addon_node = xml.getElementsByTagName('addon')[0]
37
+    return addon_node.getAttribute('name')

BIN
kodiswift/mockxbmc/utils.pyc View File


+ 97
- 0
kodiswift/mockxbmc/xbmc.py View File

@@ -0,0 +1,97 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import print_function, absolute_import
3
+
4
+import errno
5
+import os
6
+import tempfile
7
+
8
+import kodiswift
9
+from kodiswift.cli.create import get_value
10
+
11
+TEMP_DIR = os.path.join(tempfile.gettempdir(), 'kodiswift_debug')
12
+kodiswift.log.info('Using temp directory %s', TEMP_DIR)
13
+
14
+
15
+def _create_dir(path):
16
+    """Creates necessary directories for the given path or does nothing
17
+    if the directories already exist.
18
+    """
19
+    try:
20
+        os.makedirs(path)
21
+    except OSError as exc:
22
+        if exc.errno == errno.EEXIST:
23
+            pass
24
+        else:
25
+            raise
26
+
27
+
28
+def log(msg, level=0):
29
+    levels = [
30
+        'LOGDEBUG',
31
+        'LOGINFO',
32
+        'LOGNOTICE',
33
+        'LOGWARNING',
34
+        'LOGERROR',
35
+        'LOGSEVERE',
36
+        'LOGFATAL',
37
+        'LOGNONE',
38
+    ]
39
+    print('%s - %s' % (levels[level], msg))
40
+
41
+
42
+# noinspection PyPep8Naming
43
+def translatePath(path):
44
+    """Creates folders in the OS's temp directory. Doesn't touch any
45
+    possible Kodi installation on the machine. Attempting to do as
46
+    little work as possible to enable this function to work seamlessly.
47
+    """
48
+    valid_dirs = ['xbmc', 'home', 'temp', 'masterprofile', 'profile',
49
+                  'subtitles', 'userdata', 'database', 'thumbnails',
50
+                  'recordings', 'screenshots', 'musicplaylists',
51
+                  'videoplaylists', 'cdrips', 'skin', ]
52
+
53
+    # TODO: Remove asserts
54
+    assert path.startswith('special://'), 'Not a valid special:// path.'
55
+    parts = path.split('/')[2:]
56
+    #assert len(parts) > 1, 'Need at least a single root directory'
57
+    assert parts[0] in valid_dirs, '%s is not a valid root dir.' % parts[0]
58
+
59
+    # We don't want to swallow any potential IOErrors here, so only makedir for
60
+    # the root dir, the user is responsible for making any further child dirs
61
+    _create_dir(os.path.join(TEMP_DIR, parts[0]))
62
+
63
+    return os.path.join(TEMP_DIR, *parts)
64
+
65
+
66
+# noinspection PyPep8Naming
67
+class Keyboard(object):
68
+    def __init__(self, default='', heading='', hidden=False):
69
+        self._heading = heading
70
+        self._default = default
71
+        self._hidden = hidden
72
+        self._confirmed = False
73
+        self._input = None
74
+
75
+    def setDefault(self, default):
76
+        self._default = default
77
+
78
+    def setHeading(self, heading):
79
+        self._heading = heading
80
+
81
+    def setHiddenInput(self, hidden):
82
+        self._hidden = hidden
83
+
84
+    def doModal(self):
85
+        self._confirmed = False
86
+        try:
87
+            self._input = get_value(
88
+                self._heading, self._default, hidden=self._hidden)
89
+            self._confirmed = True
90
+        except(KeyboardInterrupt, EOFError):
91
+            pass
92
+
93
+    def isConfirmed(self):
94
+        return self._confirmed
95
+
96
+    def getText(self):
97
+        return self._input

BIN
kodiswift/mockxbmc/xbmc.pyc View File


+ 70
- 0
kodiswift/mockxbmc/xbmcaddon.py View File

@@ -0,0 +1,70 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import absolute_import
3
+
4
+import os
5
+
6
+from kodiswift.logger import log
7
+from kodiswift.mockxbmc import utils
8
+
9
+__all__ = ['Addon']
10
+
11
+
12
+def _get_env_setting(name):
13
+    return os.getenv('KODISWIFT_%s' % name.upper())
14
+
15
+
16
+# noinspection PyPep8Naming
17
+class Addon(object):
18
+    def __init__(self, id=None):
19
+        # In CLI mode, kodiswift must be run from the root of the addon
20
+        # directory, so we can rely on getcwd() being correct.
21
+        addon_xml = os.path.join(os.getcwd(), 'addon.xml')
22
+        _id = None
23
+        if os.path.exists(addon_xml):
24
+            _id = utils.get_addon_id(addon_xml)
25
+        self._info = {
26
+            'id': id or _id,
27
+            'name': utils.get_addon_name(addon_xml),
28
+            'profile': 'special://profile/addon_data/%s/' % _id,
29
+            'path': 'special://home/addons/%s' % _id
30
+        }
31
+        self._strings = {}
32
+        self._settings = {}
33
+        strings_fn = os.path.join(
34
+            os.getcwd(), 'resources', 'language', 'English', 'strings.po')
35
+        utils.load_addon_strings(self, strings_fn)
36
+
37
+    def getAddonInfo(self, prop):
38
+        properties = ['author', 'changelog', 'description', 'disclaimer',
39
+                      'fanart', 'icon', 'id', 'name', 'path', 'profile',
40
+                      'stars', 'summary', 'type', 'version']
41
+        if prop not in properties:
42
+            raise ValueError('%s is not a valid property.' % prop)
43
+        return self._info.get(prop, 'Unavailable')
44
+
45
+    def getLocalizedString(self, str_id):
46
+        key = str(str_id)
47
+        if key not in self._strings:
48
+            raise KeyError('id not found in English/strings.po or '
49
+                           'strings.xml.')
50
+        return self._strings[key]
51
+
52
+    def getSetting(self, key):
53
+        log.warning('xbmcaddon.Plugin.getSetting() has not been implemented '
54
+                    'in CLI mode.')
55
+        try:
56
+            value = self._settings[key]
57
+        except KeyError:
58
+            # see if we have an env var
59
+            value = _get_env_setting(key)
60
+            if _get_env_setting(key) is None:
61
+                value = raw_input(
62
+                    '* Please enter a temporary value for %s: ' % key)
63
+            self._settings[key] = value
64
+        return value
65
+
66
+    def setSetting(self, key, value):
67
+        self._settings[key] = value
68
+
69
+    def openSettings(self):
70
+        pass

BIN
kodiswift/mockxbmc/xbmcaddon.pyc View File


+ 67
- 0
kodiswift/mockxbmc/xbmcgui.py View File

@@ -0,0 +1,67 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+# noinspection PyPep8Naming,PyUnusedLocal
5
+class ListItem(object):
6
+    def __init__(self, label=None, label2=None, iconImage=None,
7
+                 thumbnailImage=None, path=None):
8
+        self.label = label
9
+        self.label2 = label2
10
+        self.iconImage = iconImage
11
+        self.thumbnailImage = thumbnailImage
12
+        self.path = path
13
+        self.properties = {}
14
+        self.stream_info = {}
15
+        self.art = {}
16
+        self.selected = False
17
+        self.context_menu_items = None
18
+        self.infolabels = {}
19
+
20
+    def addContextMenuItems(self, items, replaceItems=False):
21
+        self.context_menu_items = items
22
+
23
+    def getLabel(self):
24
+        return self.label
25
+
26
+    def getLabel2(self):
27
+        return self.label2
28
+
29
+    def getProperty(self, key):
30
+        return self.properties[key.lower()]
31
+
32
+    def isSelected(self):
33
+        return self.selected
34
+
35
+    def select(self, selected):
36
+        self.selected = selected
37
+
38
+    def setIconImage(self, icon):
39
+        self.iconImage = icon
40
+
41
+    def setInfo(self, info_type, infoLabels):
42
+        #assert info_type in ['video', 'music', 'pictures']
43
+        self.infolabels.update(infoLabels)
44
+
45
+    def setLabel(self, label):
46
+        self.label = label
47
+
48
+    def setLabel2(self, label2):
49
+        self.label2 = label2
50
+
51
+    def setPath(self, path):
52
+        self.path = path
53
+
54
+    def setProperty(self, key, value):
55
+        self.properties[key.lower()] = value
56
+
57
+    def addStreamInfo(self, stream_type, stream_values):
58
+        self.stream_info.update({stream_type: stream_values})
59
+
60
+    def setThumbnailImage(self, thumb):
61
+        self.thumbnailImage = thumb
62
+
63
+    def setArt(self, values):
64
+        self.art = values
65
+
66
+class WindowXMLDialog:
67
+    pass

BIN
kodiswift/mockxbmc/xbmcgui.pyc View File


+ 88
- 0
kodiswift/mockxbmc/xbmcplugin.py View File

@@ -0,0 +1,88 @@
1
+# coding: utf-8
2
+"""
3
+Functions for Kodi plugins
4
+"""
5
+import sys
6
+
7
+SORT_METHOD_ALBUM = 13
8
+SORT_METHOD_ALBUM_IGNORE_THE = 14
9
+SORT_METHOD_ARTIST = 11
10
+SORT_METHOD_ARTIST_IGNORE_THE = 12
11
+SORT_METHOD_BITRATE = 40
12
+SORT_METHOD_CHANNEL = 38
13
+SORT_METHOD_COUNTRY = 16
14
+SORT_METHOD_DATE = 3
15
+SORT_METHOD_DATEADDED = 19
16
+SORT_METHOD_DATE_TAKEN = 41
17
+SORT_METHOD_DRIVE_TYPE = 6
18
+SORT_METHOD_DURATION = 8
19
+SORT_METHOD_EPISODE = 22
20
+SORT_METHOD_FILE = 5
21
+SORT_METHOD_FULLPATH = 32
22
+SORT_METHOD_GENRE = 15
23
+SORT_METHOD_LABEL = 1
24
+SORT_METHOD_LABEL_IGNORE_FOLDERS = 33
25
+SORT_METHOD_LABEL_IGNORE_THE = 2
26
+SORT_METHOD_LASTPLAYED = 34
27
+SORT_METHOD_LISTENERS = 36
28
+SORT_METHOD_MPAA_RATING = 28
29
+SORT_METHOD_NONE = 0
30
+SORT_METHOD_PLAYCOUNT = 35
31
+SORT_METHOD_PLAYLIST_ORDER = 21
32
+SORT_METHOD_PRODUCTIONCODE = 26
33
+SORT_METHOD_PROGRAM_COUNT = 20
34
+SORT_METHOD_SIZE = 4
35
+SORT_METHOD_SONG_RATING = 27
36
+SORT_METHOD_STUDIO = 30
37
+SORT_METHOD_STUDIO_IGNORE_THE = 31
38
+SORT_METHOD_TITLE = 9
39
+SORT_METHOD_TITLE_IGNORE_THE = 10
40
+SORT_METHOD_TRACKNUM = 7
41
+SORT_METHOD_UNSORTED = 37
42
+SORT_METHOD_VIDEO_RATING = 18
43
+SORT_METHOD_VIDEO_RUNTIME = 29
44
+SORT_METHOD_VIDEO_SORT_TITLE = 24
45
+SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE = 25
46
+SORT_METHOD_VIDEO_TITLE = 23
47
+SORT_METHOD_VIDEO_YEAR = 17
48
+
49
+def player(url, title = "", suburl= "",headers={}):
50
+    from subprocess import call
51
+    if not url:
52
+        return
53
+    cmd1 = [r"c:\Program Files\VideoLAN\VLC\vlc.exe",url,
54
+           "--meta-title",title.decode("utf8").encode(sys.getfilesystemencoding()),
55
+           "--http-user-agent","Enigma2"
56
+    ]
57
+    # gst-launch-1.0 -v souphttpsrc ssl-strict=false proxy=127.0.0.1:8888 extra-headers="Origin:adadadasd"  location="http://bitdash-a.akamaihd.net/content/sintel/sintel.mpd" ! decodebin! autovideosink
58
+    cmd2 = [
59
+        r"C:\gstreamer\1.0\x86_64\bin\gst-launch-1.0","-v",
60
+        "playbin", 'uri="%s"'%url,
61
+        #"souphttpsrc", "ssl-strict=false",
62
+        #"proxy=127.0.0.1:8888",
63
+        #'location="%s"'%url,
64
+        #'!decodebin!autovideosink'
65
+    ]
66
+    cmd3 = ["ffplay.exe",url]
67
+    cmd = cmd1 if url.startswith("https") else cmd2
68
+    ret = call(cmd3)
69
+    #if ret:
70
+        #a = raw_input("*** Error, continue")
71
+    return
72
+
73
+def setResolvedUrl(handle, succeeded, listitem):
74
+    """Callback function to tell XBMC that the file plugin has been resolved to a url
75
+
76
+    :param handle: integer - handle the plugin was started with.
77
+    :param succeeded: bool - True=script completed successfully/False=Script did not.
78
+    :param listitem: ListItem - item the file plugin resolved to for playback.
79
+
80
+    Example::
81
+
82
+        xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
83
+
84
+    """
85
+    url = listitem.path
86
+    title = listitem.label
87
+    player(url,title)
88
+    pass

BIN
kodiswift/mockxbmc/xbmcplugin.pyc View File


+ 25
- 0
kodiswift/mockxbmc/xbmcvfs.py View File

@@ -0,0 +1,25 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import os
4
+
5
+
6
+def exists(target):
7
+    return os.path.exists(target)
8
+
9
+
10
+def rename(origin, target):
11
+    return os.rename(origin, target)
12
+
13
+
14
+def delete(target):
15
+    if os.path.isfile(target) and not os.path.isdir(target):
16
+        return os.unlink(target)
17
+    return False
18
+
19
+
20
+def mkdir(target):
21
+    os.mkdir(target)
22
+
23
+
24
+def listdir(target):
25
+    return os.listdir(target)

BIN
kodiswift/mockxbmc/xbmcvfs.pyc View File


+ 155
- 0
kodiswift/module.py View File

@@ -0,0 +1,155 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.module
4
+-----------------
5
+
6
+This module contains the Module Class.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+from kodiswift import setup_log
14
+from kodiswift.xbmcmixin import XBMCMixin
15
+
16
+__all__ = ['Module']
17
+
18
+
19
+class Module(XBMCMixin):
20
+    """Modules are basically mini plugins except they don't have any
21
+    functionality until they are registered with a Plugin.
22
+    """
23
+
24
+    def __init__(self, namespace):
25
+        # Get rid of package prefixes
26
+        self._namespace = namespace.split('.')[-1]
27
+        self._view_functions = {}
28
+        self._routes = []
29
+        self._register_funcs = []
30
+        self._plugin = None
31
+        self._url_prefix = None
32
+        self._log = setup_log(namespace)
33
+
34
+    @property
35
+    def plugin(self):
36
+        """Returns the plugin this module is registered to, or
37
+
38
+        Returns:
39
+            kodiswift.Plugin:
40
+
41
+        Raises:
42
+            RuntimeError: If not registered
43
+        """
44
+        if self._plugin is None:
45
+            raise RuntimeError('Module must be registered in order to call'
46
+                               'this method.')
47
+        return self._plugin
48
+
49
+    @plugin.setter
50
+    def plugin(self, value):
51
+        self._plugin = value
52
+
53
+    @property
54
+    def cache_path(self):
55
+        """Returns the module's cache_path."""
56
+        return self.plugin.storage_path
57
+
58
+    @property
59
+    def addon(self):
60
+        """Returns the module's addon"""
61
+        return self.plugin.addon
62
+
63
+    @property
64
+    def added_items(self):
65
+        """Returns this module's added_items"""
66
+        return self.plugin.added_items
67
+
68
+    @property
69
+    def handle(self):
70
+        """Returns this module's handle"""
71
+        return self.plugin.handle
72
+
73
+    @property
74
+    def request(self):
75
+        """Returns the current request"""
76
+        return self.plugin.request
77
+
78
+    @property
79
+    def log(self):
80
+        """Returns the registered plugin's log."""
81
+        return self._log
82
+
83
+    @property
84
+    def url_prefix(self):
85
+        """Sets or gets the url prefix of the module.
86
+
87
+        Raises an Exception if this module is not registered with a
88
+        Plugin.
89
+
90
+        Returns:
91
+            str:
92
+
93
+        Raises:
94
+            RuntimeError:
95
+        """
96
+        if self._url_prefix is None:
97
+            raise RuntimeError(
98
+                'Module must be registered in order to call this method.')
99
+        return self._url_prefix
100
+
101
+    @url_prefix.setter
102
+    def url_prefix(self, value):
103
+        self._url_prefix = value
104
+
105
+    @property
106
+    def register_funcs(self):
107
+        return self._register_funcs
108
+
109
+    def route(self, url_rule, name=None, options=None):
110
+        """A decorator to add a route to a view. name is used to
111
+        differentiate when there are multiple routes for a given view."""
112
+        def decorator(func):
113
+            """Adds a url rule for the provided function"""
114
+            view_name = name or func.__name__
115
+            self.add_url_rule(url_rule, func, name=view_name, options=options)
116
+            return func
117
+        return decorator
118
+
119
+    def url_for(self, endpoint, explicit=False, **items):
120
+        """Returns a valid Kodi plugin URL for the given endpoint name.
121
+        endpoint can be the literal name of a function, or it can
122
+        correspond to the name keyword arguments passed to the route
123
+        decorator.
124
+
125
+        Currently, view names must be unique across all plugins and
126
+        modules. There are not namespace prefixes for modules.
127
+        """
128
+        # TODO: Enable items to be passed with keywords of other var names
129
+        #       such as endpoing and explicit
130
+        # TODO: Figure out how to handle the case where a module wants to
131
+        #       call a parent plugin view.
132
+        if not explicit and not endpoint.startswith(self._namespace):
133
+            endpoint = '%s.%s' % (self._namespace, endpoint)
134
+        return self._plugin.url_for(endpoint, **items)
135
+
136
+    def add_url_rule(self, url_rule, view_func, name, options=None):
137
+        """This method adds a URL rule for routing purposes. The
138
+        provided name can be different from the view function name if
139
+        desired. The provided name is what is used in url_for to build
140
+        a URL.
141
+
142
+        The route decorator provides the same functionality.
143
+        """
144
+        name = '%s.%s' % (self._namespace, name)
145
+
146
+        def register_rule(plugin, url_prefix):
147
+            """Registers a url rule for the provided plugin and
148
+            url_prefix.
149
+            """
150
+            full_url_rule = url_prefix + url_rule
151
+            plugin.add_url_rule(full_url_rule, view_func, name, options)
152
+
153
+        # Delay actual registration of the url rule until this module is
154
+        # registered with a plugin
155
+        self._register_funcs.append(register_rule)

BIN
kodiswift/module.pyc View File


BIN
kodiswift/module.pyo View File


+ 357
- 0
kodiswift/plugin.py View File

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

BIN
kodiswift/plugin.pyc View File


BIN
kodiswift/plugin.pyo View File


+ 46
- 0
kodiswift/request.py View File

@@ -0,0 +1,46 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.request
4
+------------------
5
+
6
+This module contains the Request class. This class represents an incoming
7
+request from Kodi.
8
+
9
+:copyright: (c) 2012 by Jonathan Beluch
10
+:license: GPLv3, see LICENSE for more details.
11
+"""
12
+from __future__ import absolute_import
13
+
14
+import urlparse
15
+
16
+from kodiswift.common import unpickle_args
17
+
18
+__all__ = ['Request']
19
+
20
+
21
+class Request(object):
22
+
23
+    def __init__(self, url, handle):
24
+        """The request objects contains all the arguments passed to the plugin via
25
+        the command line.
26
+
27
+        Args:
28
+            url (str): The complete plugin URL being requested. Since Kodi
29
+                typically passes the URL query string in a separate argument
30
+                from the base URL, they must be joined into a single string
31
+                before being provided.
32
+            handle (Union[int, str]): The handle associated with the current
33
+                request.
34
+        """
35
+        self.url = url
36
+
37
+        #: The current request's handle, an integer.
38
+        self.handle = int(handle)
39
+
40
+        # urlparse doesn't like the 'plugin' scheme, so pass a protocol
41
+        # relative url, e.g. //plugin.video.helloxbmc/path
42
+        self.scheme, remainder = url.split(':', 1)
43
+        parts = urlparse.urlparse(remainder)
44
+        self.netloc, self.path, self.query_string = (
45
+            parts[1], parts[2], parts[4])
46
+        self.args = unpickle_args(urlparse.parse_qs(self.query_string))

BIN
kodiswift/request.pyc View File


BIN
kodiswift/request.pyo View File


+ 163
- 0
kodiswift/storage.py View File

@@ -0,0 +1,163 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.storage
4
+-----------------
5
+
6
+This module contains persistent storage classes.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import collections
14
+import json
15
+import os
16
+import time
17
+import shutil
18
+from datetime import datetime
19
+
20
+try:
21
+    import cPickle as pickle
22
+except ImportError:
23
+    import pickle
24
+
25
+__all__ = ['Formats', 'PersistentStorage', 'TimedStorage', 'UnknownFormat']
26
+
27
+
28
+class UnknownFormat(Exception):
29
+    pass
30
+
31
+
32
+class Formats(object):
33
+    PICKLE = 'pickle'
34
+    JSON = 'json'
35
+
36
+
37
+class PersistentStorage(collections.MutableMapping):
38
+    def __init__(self, file_path, file_format=Formats.PICKLE):
39
+        """
40
+        Args:
41
+            file_path (str):
42
+            file_format (Optional[kodiswift.Formats]):
43
+        """
44
+        super(PersistentStorage, self).__init__()
45
+        self.file_path = file_path
46
+        self.file_format = file_format
47
+        self._store = {}
48
+        self._loaded = False
49
+
50
+    def __getitem__(self, key):
51
+        return self._store[key]
52
+
53
+    def __setitem__(self, key, value):
54
+        self._store[key] = value
55
+
56
+    def __delitem__(self, key):
57
+        del self._store[key]
58
+
59
+    def __iter__(self):
60
+        return iter(self._store)
61
+
62
+    def __len__(self):
63
+        return len(self._store)
64
+
65
+    def __enter__(self):
66
+        self.load()
67
+        self.sync()
68
+        return self
69
+
70
+    def __exit__(self, exc_type, exc_val, exc_tb):
71
+        self.sync()
72
+
73
+    def __repr__(self):
74
+        return '%s(%r)' % (self.__class__.__name__, self._store)
75
+
76
+    def items(self):
77
+        return self._store.items()
78
+
79
+    def load(self):
80
+        """Load the file from disk.
81
+
82
+        Returns:
83
+            bool: True if successfully loaded, False if the file
84
+                doesn't exist.
85
+
86
+        Raises:
87
+            UnknownFormat: When the file exists but couldn't be loaded.
88
+        """
89
+
90
+        if not self._loaded and os.path.exists(self.file_path):
91
+            with open(self.file_path, 'rb') as f:
92
+                for loader in (pickle.load, json.load):
93
+                    try:
94
+                        f.seek(0)
95
+                        self._store = loader(f)
96
+                        self._loaded = True
97
+                        break
98
+                    except pickle.UnpicklingError:
99
+                        pass
100
+            # If the file exists and wasn't able to be loaded, raise an error.
101
+            if not self._loaded:
102
+                raise UnknownFormat('Failed to load file')
103
+        return self._loaded
104
+
105
+    def close(self):
106
+        self.sync()
107
+
108
+    def sync(self):
109
+        temp_file = self.file_path + '.tmp'
110
+        try:
111
+            with open(temp_file, 'wb') as f:
112
+                if self.file_format == Formats.PICKLE:
113
+                    pickle.dump(self._store, f, 2)
114
+                elif self.file_format == Formats.JSON:
115
+                    json.dump(self._store, f, separators=(',', ':'))
116
+                else:
117
+                    raise NotImplementedError(
118
+                        'Unknown file format ' + repr(self.file_format))
119
+        except Exception:
120
+            if os.path.exists(temp_file):
121
+                os.remove(temp_file)
122
+            raise
123
+        shutil.move(temp_file, self.file_path)
124
+
125
+
126
+class TimedStorage(PersistentStorage):
127
+    """A dict with the ability to persist to disk and TTL for items."""
128
+
129
+    def __init__(self, file_path, ttl=None, **kwargs):
130
+        """
131
+        Args:
132
+            file_path (str):
133
+            ttl (Optional[int]):
134
+        """
135
+        super(TimedStorage, self).__init__(file_path, **kwargs)
136
+        self.ttl = ttl
137
+
138
+    def __setitem__(self, key, value):
139
+        self._store[key] = (value, time.time())
140
+
141
+    def __getitem__(self, item):
142
+        val, timestamp = self._store[item]
143
+        ttl_diff = datetime.utcnow() - datetime.utcfromtimestamp(timestamp)
144
+        if self.ttl and ttl_diff > self.ttl:
145
+            del self._store[item]
146
+            raise KeyError
147
+        return val
148
+
149
+    def __repr__(self):
150
+        return '%s(%r)' % (self.__class__.__name__,
151
+                           dict((k, v[0]) for k, v in self._store.items()))
152
+
153
+    def items(self):
154
+        items = []
155
+        for k in self._store.keys():
156
+            try:
157
+                items.append((k, self[k]))
158
+            except KeyError:
159
+                pass
160
+        return items
161
+
162
+    def sync(self):
163
+        super(TimedStorage, self).sync()

BIN
kodiswift/storage.pyc View File


BIN
kodiswift/storage.pyo View File


+ 212
- 0
kodiswift/urls.py View File

@@ -0,0 +1,212 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+kodiswift.urls
4
+---------------
5
+
6
+This module contains URLRule class for dealing with url patterns.
7
+
8
+:copyright: (c) 2012 by Jonathan Beluch
9
+:license: GPLv3, see LICENSE for more details.
10
+"""
11
+from __future__ import absolute_import
12
+
13
+import re
14
+from urllib import urlencode, unquote_plus, quote_plus
15
+
16
+from kodiswift.common import pickle_dict, unpickle_dict
17
+
18
+__all__ = ['UrlRule', 'AmbiguousUrlException', 'NotFoundException']
19
+
20
+
21
+class AmbiguousUrlException(Exception):
22
+    pass
23
+
24
+
25
+class NotFoundException(Exception):
26
+    pass
27
+
28
+
29
+class UrlRule(object):
30
+    """A class the encapsulates a URL
31
+    """
32
+
33
+    def __init__(self, url_rule, view_func, name, options):
34
+        """Stores the various properties related to a routing URL rule.
35
+
36
+        It also provides a few methods to create URLs from the rule or to
37
+        match a given URL against a rule.
38
+
39
+        Args:
40
+            url_rule: The relative url pattern for the rule.
41
+                It may include <var_name> to denote where dynamic variables
42
+                should be matched.
43
+            view_func: The function that should be bound to this rule.
44
+                This should be an actual function object.
45
+            name: The name of the url rule. This is used in the reverse
46
+                process of creating urls for a given rule.
47
+            options: A dict containing any default values for the url rule.
48
+
49
+        Warnings:
50
+            view_func: The function signature should match any variable
51
+                names in the provided url_rule.
52
+        """
53
+        self._name = name
54
+        self._url_rule = url_rule
55
+        self._view_func = view_func
56
+        self._options = options or {}
57
+        self._keywords = re.findall(r'<(.+?)>', url_rule)
58
+
59
+        # change <> to {} for use with str.format()
60
+        self._url_format = self._url_rule.replace('<', '{').replace('>', '}')
61
+
62
+        # Make a regex pattern for matching incoming URLs
63
+        rule = self._url_rule
64
+        if rule != '/':
65
+            # Except for a path of '/', the trailing slash is optional.
66
+            rule = self._url_rule.rstrip('/') + '/?'
67
+        p = rule.replace('<', '(?P<').replace('>', '>[^/]+?)')
68
+
69
+        try:
70
+            self._regex = re.compile('^' + p + '$')
71
+        except re.error:
72
+            raise ValueError('There was a problem creating this URL rule. '
73
+                             'Ensure you do not have any unpaired angle '
74
+                             'brackets: "<" or ">"')
75
+
76
+    def __eq__(self, other):
77
+        if isinstance(other, UrlRule):
78
+            return (
79
+                (self._name, self._url_rule, self._view_func, self._options) ==
80
+                (other._name, other._url_rule, other._view_func, other._options)
81
+            )
82
+        else:
83
+            raise NotImplementedError
84
+
85
+    def __ne__(self, other):
86
+        return not self == other
87
+
88
+    def match(self, path):
89
+        """Attempts to match a url to the given path.
90
+
91
+        If successful, a tuple is returned. The first item is the matched
92
+        function and the second item is a dictionary containing items to be
93
+        passed to the function parsed from the provided path.
94
+
95
+        Args:
96
+            path (str): The URL path.
97
+
98
+        Raises:
99
+            NotFoundException: If the provided path does not match this
100
+                url rule.
101
+        """
102
+        m = self._regex.search(path)
103
+        if not m:
104
+            raise NotFoundException
105
+
106
+        # urlunencode the values
107
+        items = dict((key, unquote_plus(val))
108
+                     for key, val in m.groupdict().items())
109
+
110
+        # unpickle any items if present
111
+        items = unpickle_dict(items)
112
+
113
+        # We need to update our dictionary with default values provided in
114
+        # options if the keys don't already exist.
115
+        [items.setdefault(key, val) for key, val in self._options.items()]
116
+        return self._view_func, items
117
+
118
+    def _make_path(self, items):
119
+        """Returns a relative path for the given dictionary of items.
120
+
121
+        Uses this url rule's url pattern and replaces instances of <var_name>
122
+        with the appropriate value from the items dict.
123
+        """
124
+        for key, val in items.items():
125
+            if not isinstance(val, basestring):
126
+                raise TypeError('Value "%s" for key "%s" must be an instance'
127
+                                ' of basestring' % (val, key))
128
+            items[key] = quote_plus(val)
129
+
130
+        try:
131
+            path = self._url_format.format(**items)
132
+        except AttributeError:
133
+            # Old version of python
134
+            path = self._url_format
135
+            for key, val in items.items():
136
+                path = path.replace('{%s}' % key, val)
137
+        return path
138
+
139
+    def _make_qs(self, items):
140
+        """Returns a query string for the given dictionary of items. All keys
141
+        and values in the provided items will be urlencoded. If necessary, any
142
+        python objects will be pickled before being urlencoded.
143
+        """
144
+        return urlencode(pickle_dict(items))
145
+
146
+    def make_path_qs(self, items):
147
+        """Returns a relative path complete with query string for the given
148
+        dictionary of items.
149
+
150
+        Any items with keys matching this rule's url pattern will be inserted
151
+        into the path. Any remaining items will be appended as query string
152
+        parameters.
153
+
154
+        All items will be urlencoded. Any items which are not instances of
155
+        basestring, or int/long will be pickled before being urlencoded.
156
+
157
+        .. warning:: The pickling of items only works for key/value pairs which
158
+                     will be in the query string. This behavior should only be
159
+                     used for the simplest of python objects. It causes the
160
+                     URL to get very lengthy (and unreadable) and Kodi has a
161
+                     hard limit on URL length. See the caching section if you
162
+                     need to persist a large amount of data between requests.
163
+        """
164
+        # Convert any ints and longs to strings
165
+        for key, val in items.items():
166
+            if isinstance(val, (int, long)):
167
+                items[key] = str(val)
168
+
169
+        # First use our defaults passed when registering the rule
170
+        url_items = dict((key, val) for key, val in self._options.items()
171
+                         if key in self._keywords)
172
+
173
+        # Now update with any items explicitly passed to url_for
174
+        url_items.update((key, val) for key, val in items.items()
175
+                         if key in self._keywords)
176
+
177
+        # Create the path
178
+        path = self._make_path(url_items)
179
+
180
+        # Extra arguments get tacked on to the query string
181
+        qs_items = dict((key, val) for key, val in items.items()
182
+                        if key not in self._keywords)
183
+        qs = self._make_qs(qs_items)
184
+
185
+        if qs:
186
+            return '?'.join([path, qs])
187
+        return path
188
+
189
+    @property
190
+    def regex(self):
191
+        """The regex for matching paths against this url rule."""
192
+        return self._regex
193
+
194
+    @property
195
+    def view_func(self):
196
+        """The bound function"""
197
+        return self._view_func
198
+
199
+    @property
200
+    def url_format(self):
201
+        """The url pattern"""
202
+        return self._url_format
203
+
204
+    @property
205
+    def name(self):
206
+        """The name of this url rule."""
207
+        return self._name
208
+
209
+    @property
210
+    def keywords(self):
211
+        """The list of path keywords for this url rule."""
212
+        return self._keywords

BIN
kodiswift/urls.pyc View File


BIN
kodiswift/urls.pyo View File


+ 572
- 0
kodiswift/xbmcmixin.py View File

@@ -0,0 +1,572 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import absolute_import
3
+
4
+import os
5
+import warnings
6
+from datetime import timedelta
7
+from functools import wraps
8
+
9
+import kodiswift
10
+from kodiswift import xbmc, xbmcplugin, xbmcgui, CLI_MODE
11
+from kodiswift.constants import SortMethod
12
+from kodiswift.logger import log
13
+from kodiswift.storage import TimedStorage, UnknownFormat
14
+
15
+__all__ = ['XBMCMixin']
16
+
17
+# TODO(Sinap): Need to either break the single mixin into multiple or just use
18
+#              a parent class.
19
+
20
+
21
+# noinspection PyUnresolvedReferences,PyAttributeOutsideInit
22
+class XBMCMixin(object):
23
+    """A mixin to add Kodi helper methods. In order to use this mixin,
24
+    the child class must implement the following methods and
25
+    properties:
26
+
27
+        # Also, the child class is responsible for ensuring that this path
28
+        # exists.
29
+        self.storage_path
30
+
31
+        self.added_items
32
+
33
+        self.request
34
+
35
+        self.addon
36
+
37
+        _end_of_directory = False
38
+
39
+        _update_listing
40
+
41
+        self.handle
42
+
43
+    # optional
44
+    self.info_type: should be in ['video', 'music', 'pictures']
45
+    _memoized_storage = None
46
+    _unsynced_storage = None
47
+    # TODO: Ensure above is implemented
48
+    """
49
+
50
+    _function_cache_name = '.functions'
51
+
52
+    def cached(self, ttl=60 * 24):
53
+        """A decorator that will cache the output of the wrapped function.
54
+
55
+        The key used for the cache is the function name as well as the
56
+        `*args` and `**kwargs` passed to the function.
57
+
58
+        Args:
59
+            ttl: Time to live in minutes.
60
+
61
+        Notes:
62
+            ttl: For route caching, you should use
63
+                :meth:`kodiswift.Plugin.cached_route`.
64
+        """
65
+        def decorating_function(function):
66
+            storage = self.get_storage(
67
+                self._function_cache_name, file_format='pickle', ttl=ttl)
68
+            kwd_mark = 'f35c2d973e1bbbc61ca60fc6d7ae4eb3'
69
+
70
+            @wraps(function)
71
+            def wrapper(*args, **kwargs):
72
+                key = (function.__name__, kwd_mark) + args
73
+                if kwargs:
74
+                    key += (kwd_mark,) + tuple(sorted(kwargs.items()))
75
+
76
+                try:
77
+                    result = storage[key]
78
+                    log.debug('Storage hit for function "%s" with args "%s" '
79
+                              'and kwargs "%s"', function.__name__, args,
80
+                              kwargs)
81
+                except KeyError:
82
+                    log.debug('Storage miss for function "%s" with args "%s" '
83
+                              'and kwargs "%s"', function.__name__, args,
84
+                              kwargs)
85
+                    result = function(*args, **kwargs)
86
+                    storage[key] = result
87
+                    storage.sync()
88
+                return result
89
+
90
+            return wrapper
91
+
92
+        return decorating_function
93
+
94
+    def clear_function_cache(self):
95
+        """Clears the storage that caches results when using
96
+        :meth:`kodiswift.Plugin.cached_route` or
97
+        :meth:`kodiswift.Plugin.cached`.
98
+        """
99
+        self.get_storage(self._function_cache_name).clear()
100
+
101
+    def list_storage(self):
102
+        """Returns a list of existing stores.
103
+
104
+        The returned names can then be used to call get_storage().
105
+        """
106
+        # Filter out any storage used by kodiswift so caller doesn't corrupt
107
+        # them.
108
+        return [name for name in os.listdir(self.storage_path)
109
+                if not name.startswith('.')]
110
+
111
+    def get_storage(self, name='main', file_format='pickle', ttl=None):
112
+        """Returns a storage for the given name.
113
+
114
+        The returned storage is a fully functioning python dictionary and is
115
+        designed to be used that way. It is usually not necessary for the
116
+        caller to load or save the storage manually. If the storage does
117
+        not already exist, it will be created.
118
+
119
+        See Also:
120
+            :class:`kodiswift.TimedStorage` for more details.
121
+
122
+        Args:
123
+            name (str): The name  of the storage to retrieve.
124
+            file_format (str): Choices are 'pickle', 'csv', and 'json'.
125
+                Pickle is recommended as it supports python objects.
126
+
127
+                Notes: If a storage already exists for the given name, the
128
+                    file_format parameter is ignored. The format will be
129
+                    determined by the existing storage file.
130
+
131
+            ttl (int): The time to live for storage items specified in minutes
132
+                or None for no expiration. Since storage items aren't expired
133
+                until a storage is loaded form disk, it is possible to call
134
+                get_storage() with a different TTL than when the storage was
135
+                created. The currently specified TTL is always honored.
136
+
137
+        Returns:
138
+            kodiswift.storage.TimedStorage:
139
+        """
140
+        if not hasattr(self, '_unsynced_storage'):
141
+            self._unsynced_storage = {}
142
+        filename = os.path.join(self.storage_path, name)
143
+        try:
144
+            storage = self._unsynced_storage[filename]
145
+            log.debug('Loaded storage "%s" from memory', name)
146
+        except KeyError:
147
+            if ttl:
148
+                ttl = timedelta(minutes=ttl)
149
+            try:
150
+                storage = TimedStorage(filename, ttl, file_format=file_format)
151
+                storage.load()
152
+            except UnknownFormat:
153
+                # Thrown when the storage file is corrupted and can't be read.
154
+                # Prompt user to delete storage.
155
+                choices = ['Clear storage', 'Cancel']
156
+                ret = xbmcgui.Dialog().select(
157
+                    'A storage file is corrupted. It'
158
+                    ' is recommended to clear it.', choices)
159
+                if ret == 0:
160
+                    os.remove(filename)
161
+                    storage = TimedStorage(filename, ttl,
162
+                                           file_format=file_format)
163
+                else:
164
+                    raise Exception('Corrupted storage file at %s' % filename)
165
+
166
+            self._unsynced_storage[filename] = storage
167
+            log.debug('Loaded storage "%s" from disk', name)
168
+        return storage
169
+
170
+    def get_string(self, string_id):
171
+        """Returns the localized string from strings.po or strings.xml for the
172
+        given string_id.
173
+        """
174
+        string_id = int(string_id)
175
+        if not hasattr(self, '_strings'):
176
+            self._strings = {}
177
+        if string_id not in self._strings:
178
+            self._strings[string_id] = self.addon.getLocalizedString(string_id)
179
+        return self._strings[string_id]
180
+
181
+    def set_content(self, content):
182
+        """Sets the content type for the plugin."""
183
+        contents = ['files', 'songs', 'artists', 'albums', 'movies', 'tvshows',
184
+                    'episodes', 'musicvideos']
185
+        if content not in contents:
186
+            self.log.warning('Content type "%s" is not valid', content)
187
+        xbmcplugin.setContent(self.handle, content)
188
+
189
+    def get_setting(self, key, converter=None, choices=None):
190
+        """Returns the settings value for the provided key.
191
+
192
+        If converter is str, unicode, bool or int the settings value will be
193
+        returned converted to the provided type. If choices is an instance of
194
+        list or tuple its item at position of the settings value be returned.
195
+
196
+        Args:
197
+            key (str): The ID of the setting defined in settings.xml.
198
+            converter (Optional[str, unicode, bool, int]): How to convert the
199
+                setting value.
200
+                TODO(Sinap): Maybe this should just be a callable object?
201
+            choices (Optional[list,tuple]):
202
+
203
+        Notes:
204
+            converter: It is suggested to always use unicode for
205
+                text-settings because else xbmc returns utf-8 encoded strings.
206
+
207
+        Examples:
208
+            * ``plugin.get_setting('per_page', int)``
209
+            * ``plugin.get_setting('password', unicode)``
210
+            * ``plugin.get_setting('force_viewmode', bool)``
211
+            * ``plugin.get_setting('content', choices=('videos', 'movies'))``
212
+        """
213
+        # TODO: allow pickling of settings items?
214
+        # TODO: STUB THIS OUT ON CLI
215
+        value = self.addon.getSetting(key)
216
+        if converter is str:
217
+            return value
218
+        elif converter is unicode:
219
+            return value.decode('utf-8')
220
+        elif converter is bool:
221
+            return not(value.lower()=="false" or value=="0")
222
+        elif converter is int:
223
+            return int(value)
224
+        elif isinstance(choices, (list, tuple)):
225
+            return choices[int(value)]
226
+        elif converter is None:
227
+            log.warning('No converter provided, unicode should be used, '
228
+                        'but returning str value')
229
+            return value
230
+        else:
231
+            raise TypeError('Acceptable converters are str, unicode, bool and '
232
+                            'int. Acceptable choices are instances of list '
233
+                            ' or tuple.')
234
+
235
+    def set_setting(self, key, val):
236
+        # TODO: STUB THIS OUT ON CLI - setSetting takes id=x, value=x throws an error otherwise
237
+        return self.addon.setSetting(id=key, value=val)
238
+
239
+    def load_addon_settings(self):
240
+        """ Mock loading addon settings from user_data/settings.xml file.
241
+        If settings.xml does no exists, create using default values"""
242
+        if not CLI_MODE:
243
+            return
244
+        from xml.dom.minidom import parse
245
+        xml = parse(os.path.join("resources","language","English","strings.xml"))
246
+        self.addon._strings = dict((tag.getAttribute('id'), tag.firstChild.data) for tag in xml.getElementsByTagName('string'))
247
+        settings_file = os.path.join("addon_data","settings.xml")
248
+        if not os.path.exists(settings_file):
249
+            xml = parse(os.path.join("resources","settings.xml"))
250
+            self.addon._settings = dict((tag.getAttribute('id'), tag.attributes["default"].value)for tag in xml.getElementsByTagName('setting') if "id" in tag.attributes.keys())
251
+            if not os.path.exists("addon_data"):
252
+                os.mkdir("addon_data")
253
+            with open(settings_file,"w") as f:
254
+                f.write("<settings>\n")
255
+                for s in self.addon._settings:
256
+                    f.write('    <setting id="%s" value="%s" />\n'%(s,self.addon._settings[s]))
257
+                f.write("</settings>\n")
258
+        else:
259
+            xml = parse(settings_file)
260
+            self.addon._settings = dict((tag.getAttribute('id'), tag.attributes["value"].value) for tag in xml.getElementsByTagName('setting'))
261
+
262
+    def open_settings(self):
263
+        """Opens the settings dialog within Kodi"""
264
+        self.addon.openSettings()
265
+
266
+    @staticmethod
267
+    def add_to_playlist(items, playlist='video'):
268
+        """Adds the provided list of items to the specified playlist.
269
+        Available playlists include *video* and *music*.
270
+        """
271
+        playlists = {'music': 0, 'video': 1}
272
+        if playlist not in playlists:
273
+            raise ValueError('Playlist "%s" is invalid.' % playlist)
274
+
275
+        selected_playlist = xbmc.PlayList(playlists[playlist])
276
+        _items = []
277
+        for item in items:
278
+            if not hasattr(item, 'as_xbmc_listitem'):
279
+                if 'info_type' in item:
280
+                    log.warning('info_type key has no affect for playlist '
281
+                                'items as the info_type is inferred from the '
282
+                                'playlist type.')
283
+                # info_type has to be same as the playlist type
284
+                item['info_type'] = playlist
285
+                item = kodiswift.ListItem.from_dict(**item)
286
+            _items.append(item)
287
+            selected_playlist.add(item.get_path(), item.as_xbmc_listitem())
288
+        return _items
289
+
290
+    @staticmethod
291
+    def get_view_mode_id(view_mode):
292
+        warnings.warn('get_view_mode_id is deprecated.', DeprecationWarning)
293
+        return None
294
+
295
+    @staticmethod
296
+    def set_view_mode(view_mode_id):
297
+        """Calls Kodi's Container.SetViewMode. Requires an integer
298
+        view_mode_id"""
299
+        xbmc.executebuiltin('Container.SetViewMode(%d)' % view_mode_id)
300
+
301
+    def keyboard(self, default=None, heading=None, hidden=False):
302
+        """Displays the keyboard input window to the user. If the user does not
303
+        cancel the modal, the value entered by the user will be returned.
304
+
305
+        :param default: The placeholder text used to prepopulate the input
306
+                        field.
307
+        :param heading: The heading for the window. Defaults to the current
308
+                        addon's name. If you require a blank heading, pass an
309
+                        empty string.
310
+        :param hidden: Whether or not the input field should be masked with
311
+                       stars, e.g. a password field.
312
+        """
313
+        if heading is None:
314
+            heading = self.addon.getAddonInfo('name')
315
+        if default is None:
316
+            default = ''
317
+        keyboard = xbmc.Keyboard(default, heading, hidden)
318
+        keyboard.doModal()
319
+        if keyboard.isConfirmed():
320
+            return keyboard.getText()
321
+
322
+    def notify(self, msg='', title=None, delay=5000, image=''):
323
+        """Displays a temporary notification message to the user. If
324
+        title is not provided, the plugin name will be used. To have a
325
+        blank title, pass '' for the title argument. The delay argument
326
+        is in milliseconds.
327
+        """
328
+        if not image:
329
+            image = xbmcgui.NOTIFICATION_INFO
330
+        if not msg:
331
+            log.warning('Empty message for notification dialog')
332
+        if title is None:
333
+            title = self.addon.getAddonInfo('name')
334
+        if isinstance(msg,unicode):
335
+            msg = msg.encode("utf8")
336
+        if isinstance(title,unicode):
337
+            title = title.encode("utf8")
338
+        xbmcgui.Dialog().notification(title, msg, image, delay)
339
+        #xbmc.executebuiltin('Notification("%s", "%s", "%s", "%s")' % (msg, title, delay, image))
340
+
341
+    def set_resolved_url(self, item=None, subtitles=None):
342
+        """Takes a url or a listitem to be played. Used in conjunction with a
343
+        playable list item with a path that calls back into your addon.
344
+
345
+        :param item: A playable list item or url. Pass None to alert Kodi of a
346
+                     failure to resolve the item.
347
+
348
+                     .. warning:: When using set_resolved_url you should ensure
349
+                                  the initial playable item (which calls back
350
+                                  into your addon) doesn't have a trailing
351
+                                  slash in the URL. Otherwise it won't work
352
+                                  reliably with Kodi's PlayMedia().
353
+        :param subtitles: A URL to a remote subtitles file or a local filename
354
+                          for a subtitles file to be played along with the
355
+                          item.
356
+        """
357
+        if self._end_of_directory:
358
+            raise Exception('Current Kodi handle has been removed. Either '
359
+                            'set_resolved_url(), end_of_directory(), or '
360
+                            'finish() has already been called.')
361
+        self._end_of_directory = True
362
+
363
+        succeeded = True
364
+        if item is None:
365
+            # None item indicates the resolve url failed.
366
+            item = {}
367
+            succeeded = False
368
+
369
+        if isinstance(item, basestring):
370
+            # caller is passing a url instead of an item dict
371
+            item = {'path': item}
372
+
373
+        item = self._listitemify(item)
374
+        item.set_played(True)
375
+        xbmcplugin.setResolvedUrl(self.handle, succeeded,
376
+                                  item.as_xbmc_listitem())
377
+
378
+        # call to _add_subtitles must be after setResolvedUrl
379
+        if subtitles:
380
+            if isinstance(subtitles,list):
381
+                for subtitle in subtitles:
382
+                    self._add_subtitles(subtitle)
383
+            else:
384
+                self._add_subtitles(subtitles)
385
+
386
+        return [item]
387
+
388
+    def play_video(self, item, player=None):
389
+        if isinstance(item, dict):
390
+            item['info_type'] = 'video'
391
+
392
+        item = self._listitemify(item)
393
+        item.set_played(True)
394
+        if player:
395
+            _player = xbmc.Player(player)
396
+        else:
397
+            _player = xbmc.Player()
398
+        _player.play(item.get_path(), item.as_xbmc_listitem())
399
+        return [item]
400
+
401
+    def add_items(self, items):
402
+        """Adds ListItems to the Kodi interface.
403
+
404
+        Each item in the provided list should either be instances of
405
+        kodiswift.ListItem, or regular dictionaries that will be passed
406
+        to kodiswift.ListItem.from_dict.
407
+
408
+        Args:
409
+            items: An iterable of items where each item is either a
410
+                dictionary with keys/values suitable for passing to
411
+                :meth:`kodiswift.ListItem.from_dict` or an instance of
412
+                :class:`kodiswift.ListItem`.
413
+
414
+        Returns:
415
+            kodiswift.ListItem: The list of ListItems.
416
+        """
417
+        _items = [self._listitemify(item) for item in items]
418
+        tuples = [item.as_tuple() for item in _items if hasattr(item, 'as_tuple')]
419
+        xbmcplugin.addDirectoryItems(self.handle, tuples, len(tuples))
420
+
421
+        # We need to keep track internally of added items so we can return them
422
+        # all at the end for testing purposes
423
+        self.added_items.extend(_items)
424
+
425
+        # Possibly need an if statement if only for debug mode
426
+        return _items
427
+
428
+    def add_sort_method(self, sort_method, label2_mask=None):
429
+        """A wrapper for `xbmcplugin.addSortMethod()
430
+        <http://mirrors.xbmc.org/docs/python-docs/xbmcplugin.html#-addSortMethod>`_.
431
+        You can use ``dir(kodiswift.SortMethod)`` to list all available sort
432
+        methods.
433
+
434
+        Args:
435
+            sort_method: A valid sort method. You can provided the constant
436
+                from xbmcplugin, an attribute of SortMethod, or a string name.
437
+                For instance, the following method calls are all equivalent:
438
+                 * ``plugin.add_sort_method(xbmcplugin.SORT_METHOD_TITLE)``
439
+                 * ``plugin.add_sort_method(SortMethod.TITLE)``
440
+                 * ``plugin.add_sort_method('title')``
441
+            label2_mask: A mask pattern for label2. See the `Kodi
442
+                documentation <http://mirrors.xbmc.org/docs/python-docs/xbmcplugin.html#-addSortMethod>`_
443
+                for more information.
444
+        """
445
+        try:
446
+            # Assume it's a string and we need to get the actual int value
447
+            sort_method = SortMethod.from_string(sort_method)
448
+        except AttributeError:
449
+            # sort_method was already an int (or a bad value)
450
+            pass
451
+
452
+        if label2_mask:
453
+            xbmcplugin.addSortMethod(self.handle, sort_method, label2_mask)
454
+        else:
455
+            xbmcplugin.addSortMethod(self.handle, sort_method)
456
+
457
+    def end_of_directory(self, succeeded=True, update_listing=False,
458
+                         cache_to_disc=True):
459
+        """Wrapper for xbmcplugin.endOfDirectory. Records state in
460
+        self._end_of_directory.
461
+
462
+        Typically it is not necessary to call this method directly, as
463
+        calling :meth:`~kodiswift.Plugin.finish` will call this method.
464
+        """
465
+        self._update_listing = update_listing
466
+        if not self._end_of_directory:
467
+            self._end_of_directory = True
468
+            # Finalize the directory items
469
+            return xbmcplugin.endOfDirectory(self.handle, succeeded,
470
+                                             update_listing, cache_to_disc)
471
+        else:
472
+            raise Exception('Already called endOfDirectory.')
473
+
474
+    def finish(self, items=None, sort_methods=None, succeeded=True,
475
+               update_listing=False, cache_to_disc=True, view_mode=None):
476
+        """Adds the provided items to the Kodi interface.
477
+
478
+        Args:
479
+            items (List[Dict[str, str]]]): an iterable of items where each
480
+                item is either a dictionary with keys/values suitable for
481
+                passing to :meth:`kodiswift.ListItem.from_dict` or an
482
+                instance of :class:`kodiswift.ListItem`.
483
+
484
+            sort_methods (Union[List[str], str]): A list of valid Kodi
485
+                sort_methods. Each item in the list can either be a sort
486
+                method or a tuple of `sort_method, label2_mask`.
487
+                See :meth:`add_sort_method` for more detail concerning
488
+                valid sort_methods.
489
+
490
+            succeeded (bool):
491
+            update_listing (bool):
492
+            cache_to_disc (bool): Whether to tell Kodi to cache this folder
493
+                to disk.
494
+            view_mode (Union[str, int]): Can either be an integer
495
+                (or parsable integer string) corresponding to a view_mode or
496
+                the name of a type of view. Currently the only view type
497
+                supported is 'thumbnail'.
498
+
499
+        Returns:
500
+            List[kodiswift.listitem.ListItem]: A list of all ListItems added
501
+                to the Kodi interface.
502
+        """
503
+        # If we have any items, add them. Items are optional here.
504
+        if items:
505
+            self.add_items(items)
506
+        if sort_methods:
507
+            for sort_method in sort_methods:
508
+                if isinstance(sort_method, (list, tuple)):
509
+                    self.add_sort_method(*sort_method)
510
+                else:
511
+                    self.add_sort_method(sort_method)
512
+
513
+        # Attempt to set a view_mode if given
514
+        if view_mode is not None:
515
+            # First check if we were given an integer or parsable integer
516
+            try:
517
+                view_mode_id = int(view_mode)
518
+            except ValueError:
519
+                view_mode_id = None
520
+            if view_mode_id is not None:
521
+                self.set_view_mode(view_mode_id)
522
+
523
+        # Finalize the directory items
524
+        self.end_of_directory(succeeded, update_listing, cache_to_disc)
525
+
526
+        # Return the cached list of all the list items that were added
527
+        return self.added_items
528
+
529
+    def _listitemify(self, item):
530
+        """Creates an kodiswift.ListItem if the provided value for item is a
531
+        dict. If item is already a valid kodiswift.ListItem, the item is
532
+        returned unmodified.
533
+        """
534
+        info_type = self.info_type if hasattr(self, 'info_type') else 'video'
535
+
536
+        # Create ListItems for anything that is not already an instance of
537
+        # ListItem
538
+        if not hasattr(item, 'as_tuple') and hasattr(item, 'keys'):
539
+            if 'info_type' not in item:
540
+                item['info_type'] = info_type
541
+            item = kodiswift.ListItem.from_dict(**item)
542
+        return item
543
+
544
+    @staticmethod
545
+    def _add_subtitles(subtitles):
546
+        """Adds subtitles to playing video.
547
+
548
+        Warnings:
549
+            You must start playing a video before calling this method or it
550
+            will raise and Exception after 30 seconds.
551
+
552
+        Args:
553
+            subtitles (str): A URL to a remote subtitles file or a local
554
+                filename for a subtitles file.
555
+        """
556
+        # This method is named with an underscore to suggest that callers pass
557
+        # the subtitles argument to set_resolved_url instead of calling this
558
+        # method directly. This is to ensure a video is played before calling
559
+        # this method.
560
+        player = xbmc.Player()
561
+        monitor = xbmc.Monitor()
562
+        while not monitor.abortRequested():
563
+            if monitor.waitForAbort(30):
564
+                # Abort requested, so exit.
565
+                break
566
+            elif player.isPlaying():
567
+                # No abort requested after 30 seconds and a video is playing
568
+                # so add the subtitles and exit.
569
+                player.setSubtitles(subtitles)
570
+                break
571
+            else:
572
+                raise Exception('No video playing. Aborted after 30 seconds.')

BIN
kodiswift/xbmcmixin.pyc View File


BIN
kodiswift/xbmcmixin.pyo View File