__init__.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. # -*- coding: utf-8 -*-
  2. import os
  3. import sys
  4. import pickle
  5. import xbmc
  6. import xbmcplugin
  7. import xbmcaddon
  8. import xbmcgui
  9. from common import enum
  10. from common import clean_dict
  11. from request import Request
  12. from listitem import ListItem
  13. from urls import UrlRule, NotFoundException, AmbiguousUrlException
  14. from storage import TimedStorage
  15. class Plugin(object):
  16. def __init__(self, name=None, log_func=None, pre_dispatch=None, post_dispatch=None):
  17. self._routes = []
  18. self._request = None
  19. self._view_functions = {}
  20. self._log_func = log_func
  21. self._pre_dispatch = pre_dispatch
  22. self._post_dispatch = post_dispatch
  23. self._viewmode = 0
  24. self._addon = xbmcaddon.Addon()
  25. self._addon_id = self._addon.getAddonInfo('id')
  26. self._name = self._addon.getAddonInfo('name')
  27. self._contenttype = ""
  28. self._delay = 350
  29. self._goto_top = True
  30. self._end_of_directory = False
  31. self._force_update = False
  32. self._rule = None
  33. self._addondata_path = xbmc.translatePath('special://profile/addon_data/%s/' % self._addon_id)
  34. self._storage_path = os.path.join(self._addondata_path, ".storage/")
  35. if not os.path.isdir(self._storage_path):
  36. os.makedirs(self._storage_path)
  37. types = {
  38. 'video': 'video',
  39. 'audio': 'music',
  40. 'image': 'pictures',
  41. }
  42. self._info_type = types.get(self._addon_id.split('.')[1], 'video')
  43. self.added_items = []
  44. @property
  45. def id(self):
  46. '''The id for the addon instance.'''
  47. return self._addon_id
  48. @property
  49. def storage_path(self):
  50. '''A full path to the storage folder for this plugin's addon data.'''
  51. return self._storage_path
  52. @property
  53. def addondata_path(self):
  54. return self._addondata_path
  55. @property
  56. def handle(self):
  57. return self.request.handle
  58. @property
  59. def addon(self):
  60. return self._addon
  61. @property
  62. def request(self):
  63. if self._request is None:
  64. raise Exception('It seems the current request has not been '
  65. 'initialized yet. Please ensure that '
  66. '`plugin.run()` has been called before attempting '
  67. 'to access the current request.')
  68. return self._request
  69. @property
  70. def name(self):
  71. return self._name
  72. @property
  73. def info_type(self):
  74. return self._info_type
  75. def info(self, key):
  76. return self.addon.getAddonInfo(key)
  77. def log(self, message):
  78. if self._log_func:
  79. self._log_func(message)
  80. def get_setting(self, key, converter=None, choices=None):
  81. '''Returns the settings value for the provided key.
  82. If converter is str, unicode, bool or int the settings value will be
  83. returned converted to the provided type.
  84. If choices is an instance of list or tuple its item at position of the
  85. settings value be returned.
  86. .. note:: It is suggested to always use unicode for text-settings
  87. because else xbmc returns utf-8 encoded strings.
  88. :param key: The id of the setting defined in settings.xml.
  89. :param converter: (Optional) Choices are str, unicode, bool and int.
  90. :param converter: (Optional) Choices are instances of list or tuple.
  91. Examples:
  92. * ``plugin.get_setting('per_page', int)``
  93. * ``plugin.get_setting('password', unicode)``
  94. * ``plugin.get_setting('force_viewmode', bool)``
  95. * ``plugin.get_setting('content', choices=('videos', 'movies'))``
  96. '''
  97. # TODO: allow pickling of settings items?
  98. # TODO: STUB THIS OUT ON CLI
  99. value = self.addon.getSetting(id=key)
  100. if converter is str:
  101. return value
  102. elif converter is unicode:
  103. return value.decode('utf-8')
  104. elif converter is bool:
  105. return value == 'true'
  106. elif converter is int:
  107. return int(value)
  108. elif isinstance(choices, (list, tuple)):
  109. return choices[int(value)]
  110. elif converter is None:
  111. return value
  112. else:
  113. raise TypeError('Acceptable converters are str, unicode, bool and '
  114. 'int. Acceptable choices are instances of list '
  115. ' or tuple.')
  116. def set_setting(self, key, val):
  117. return self.addon.setSetting(id=key, value=val)
  118. def open_settings(self):
  119. self.addon.openSettings()
  120. def _listitemify(self, item):
  121. info_type = self.info_type if hasattr(self, 'info_type') else 'video'
  122. if not hasattr(item, 'as_tuple'):
  123. if 'info_type' not in item.keys():
  124. item['info_type'] = info_type
  125. item = ListItem.from_dict(**item)
  126. return item
  127. def add_items(self, items):
  128. _items = [self._listitemify(item) for item in items]
  129. tuples = [item.as_tuple() for item in _items]
  130. xbmcplugin.addDirectoryItems(self.handle, tuples, len(tuples))
  131. self.added_items.extend(_items)
  132. return _items
  133. def url_for(self, endpoint, **items):
  134. try:
  135. rule = self._view_functions[endpoint]
  136. except KeyError:
  137. try:
  138. rule = (rule for rule in self._view_functions.values() if rule.view_func == endpoint).next()
  139. except StopIteration:
  140. raise NotFoundException('%s doesn\'t match any known patterns.' % endpoint)
  141. if not rule:
  142. raise AmbiguousUrlException
  143. pathqs = rule.make_path_qs(items)
  144. return 'plugin://%s%s' % (self._addon_id, pathqs)
  145. def set_force_update(self, value):
  146. self._force_update = value
  147. def set_delay(self, value):
  148. try:
  149. self._delay = int(value)
  150. except:
  151. self._delay = 350
  152. def set_resolved_url(self, item=None, subtitles=None):
  153. '''Takes a url or a listitem to be played. Used in conjunction with a
  154. playable list item with a path that calls back into your addon.
  155. :param item: A playable list item or url. Pass None to alert XBMC of a
  156. failure to resolve the item.
  157. .. warning:: When using set_resolved_url you should ensure
  158. the initial playable item (which calls back
  159. into your addon) doesn't have a trailing
  160. slash in the URL. Otherwise it won't work
  161. reliably with XBMC's PlayMedia().
  162. :param subtitles: A URL to a remote subtitles file or a local filename
  163. for a subtitles file to be played along with the
  164. item.
  165. '''
  166. if self._end_of_directory:
  167. raise Exception('Current XBMC handle has been removed. Either '
  168. 'set_resolved_url(), end_of_directory(), or '
  169. 'finish() has already been called.')
  170. self._end_of_directory = True
  171. succeeded = True
  172. if item is None:
  173. succeeded = False
  174. item = {}
  175. if isinstance(item, basestring):
  176. # caller is passing a url instead of an item dict
  177. item = {'path': item}
  178. item = self._listitemify(item)
  179. item.set_played(True)
  180. xbmcplugin.setResolvedUrl(self.handle, succeeded, item.as_xbmc_listitem())
  181. return [item]
  182. def _parse_request(self, url=None, handle=None):
  183. if url is None:
  184. url = sys.argv[0]
  185. if len(sys.argv) == 3:
  186. url += sys.argv[2]
  187. if handle is None:
  188. handle = sys.argv[1]
  189. return Request(url, handle)
  190. def route(self, url_rule, name=None, options=None, cache=True, update=False, content_type="", view_mode=0, goto_top=True):
  191. def decorator(f):
  192. view_name = name or f.__name__
  193. self.add_url_rule(url_rule, f,
  194. name=view_name,
  195. options=options,
  196. cache=cache,
  197. update=update,
  198. content_type=content_type,
  199. view_mode=view_mode,
  200. goto_top=goto_top)
  201. return f
  202. return decorator
  203. def add_url_rule(self, url_rule, view_func, name,
  204. options=None,
  205. cache=True,
  206. update=False,
  207. content_type="",
  208. view_mode=0,
  209. goto_top=True):
  210. rule = UrlRule(url_rule, view_func, name,
  211. options=options,
  212. cache=cache,
  213. update=update,
  214. content_type=content_type,
  215. view_mode=view_mode,
  216. goto_top=goto_top)
  217. if name in self._view_functions.keys():
  218. self._view_functions[name] = None
  219. else:
  220. self._view_functions[name] = rule
  221. self._routes.append(rule)
  222. # return rule
  223. def set_content(self, type):
  224. self._contenttype = type
  225. def set_view(self, mode):
  226. self._viewmode = int(mode)
  227. def set_goto_top(self, value):
  228. self._goto_top = value
  229. def redirect(self, url):
  230. new_request = self._parse_request(url=url, handle=self.request.handle)
  231. listitems, rule = self._dispatch(new_request.path)
  232. return listitems
  233. def get_storage(self, name='main', file_format='pickle', TTL=None):
  234. if not hasattr(self, '_unsynced_storages'):
  235. self._unsynced_storages = {}
  236. if not name[1] == ".":
  237. name = "." + name
  238. filename = os.path.join(self.storage_path, name)
  239. try:
  240. storage = self._unsynced_storages[filename]
  241. except KeyError:
  242. if TTL:
  243. TTL = timedelta(minutes=TTL)
  244. try:
  245. storage = TimedStorage(filename, file_format, TTL)
  246. except ValueError:
  247. choices = ['Clear storage', 'Cancel']
  248. ret = xbmcgui.Dialog().select('A storage file is corrupted. It'
  249. ' is recommended to clear it.',
  250. choices)
  251. if ret == 0:
  252. os.remove(filename)
  253. storage = TimedStorage(filename, file_format, TTL)
  254. else:
  255. raise Exception('Corrupted storage file at %s' % filename)
  256. self._unsynced_storages[filename] = storage
  257. return storage
  258. def end_of_directory(self, succeeded=True, update_listing=False, cache_to_disc=True):
  259. self._update_listing = update_listing
  260. if not self._end_of_directory:
  261. self._end_of_directory = True
  262. # Finalize the directory items
  263. if succeeded:
  264. if self._contenttype:
  265. xbmcplugin.setContent(self.handle, self._contenttype)
  266. if self._viewmode > 0:
  267. xbmc.executebuiltin("Container.SetViewMode(" + str(self._viewmode) + ")")
  268. res = xbmcplugin.endOfDirectory(self.handle, succeeded, update_listing, cache_to_disc)
  269. if succeeded:
  270. if self._delay > 75:
  271. xbmc.sleep(self._delay - 75)
  272. if self._goto_top and (not self._rule or self._rule.goto_top):
  273. xbmc.executebuiltin("XBMC.Action(firstpage)")
  274. if self._delay > 0:
  275. xbmc.sleep(75)
  276. return res
  277. assert False, 'Already called endOfDirectory.'
  278. def finish(self, items=None, sort_methods=None, succeeded=True,
  279. update_listing=False, cache_to_disc=True, view_mode=None):
  280. if items:
  281. self.add_items(items)
  282. if sort_methods:
  283. for sort_method in sort_methods:
  284. if not isinstance(sort_method, basestring) and hasattr(sort_method, '__len__'):
  285. self.add_sort_method(*sort_method)
  286. else:
  287. self.add_sort_method(sort_method)
  288. # Finalize the directory items
  289. self.end_of_directory(succeeded, update_listing, cache_to_disc)
  290. # Return the cached list of all the list items that were added
  291. return self.added_items
  292. def _dispatch(self, path):
  293. for rule in self._routes:
  294. try:
  295. view_func, items = rule.match(path)
  296. except NotFoundException:
  297. continue
  298. self._rule = rule
  299. pre = True
  300. if self._pre_dispatch:
  301. pre = self._pre_dispatch(self)
  302. if pre is None:
  303. pre = True
  304. if pre:
  305. listitems = view_func(**items)
  306. else:
  307. listitems = None
  308. if self._post_dispatch:
  309. self._post_dispatch(self, rule)
  310. if rule.content_type:
  311. self.set_content(rule.content_type)
  312. if rule.view_mode:
  313. self.set_view(rule.view_mode)
  314. if not self._end_of_directory and self.handle >= 0:
  315. if listitems is None:
  316. self.finish(succeeded=False, cache_to_disc=rule.cache, update_listing=True if self._force_update else rule.update)
  317. else:
  318. listitems = self.finish(listitems, cache_to_disc=rule.cache, update_listing=True if self._force_update else rule.update)
  319. return listitems, rule
  320. raise NotFoundException, 'No matching view found for %s' % path
  321. def notify(self, msg='', title=None, delay=5000, image=''):
  322. if title is None:
  323. title = self._name
  324. xbmc.executebuiltin('XBMC.Notification("%s", "%s", "%s", "%s")' % (msg, title, delay, image))
  325. def dialog(self, msg="", title=None):
  326. if title is None:
  327. title = self._name
  328. xbmcgui.Dialog().ok(title, msg)
  329. def yesno(self, msg="", title=None):
  330. if title is None:
  331. title = self._name
  332. return xbmcgui.Dialog().yesno(title, msg)
  333. def keyboard(self, default=None, heading=None, hidden=False):
  334. '''Displays the keyboard input window to the user. If the user does not
  335. cancel the modal, the value entered by the user will be returned.
  336. :param default: The placeholder text used to prepopulate the input field.
  337. :param heading: The heading for the window. Defaults to the current
  338. addon's name. If you require a blank heading, pass an
  339. empty string.
  340. :param hidden: Whether or not the input field should be masked with
  341. stars, e.g. a password field.
  342. '''
  343. if heading is None:
  344. heading = self._name
  345. if default is None:
  346. default = ''
  347. keyboard = xbmc.Keyboard(default, heading, hidden)
  348. keyboard.doModal()
  349. if keyboard.isConfirmed():
  350. return keyboard.getText()
  351. return None
  352. def run(self):
  353. self._request = self._parse_request()
  354. items, rule = self._dispatch(self.request.path)
  355. """
  356. wasFinished = self._finished
  357. if items:
  358. self.add_items(items)
  359. if not wasFinished:
  360. if self._contenttype:
  361. xbmcplugin.setContent(self.handle, self._contenttype)
  362. if self._viewmode > 0:
  363. xbmc.executebuiltin("Container.SetViewMode(" + str(self._viewmode) + ")")
  364. if hasattr(self, '_unsynced_storages'):
  365. for storage in self._unsynced_storages.values():
  366. storage.close()
  367. if self.handle > 0 and not wasFinished and items != None:
  368. self.finish()
  369. xbmcplugin.endOfDirectory(self.handle, succeeded=True, updateListing=rule.update, cacheToDisc=rule.cache)
  370. if not wasFinished:
  371. if items:
  372. xbmc.sleep(300)
  373. if self._contenttype:
  374. xbmcplugin.setContent(self.handle, self._contenttype)
  375. if self._viewmode > 0:
  376. xbmc.executebuiltin("Container.SetViewMode(" + str(self._viewmode) + ")")
  377. """
  378. return items