__init__.py 16 KB

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