pluginsort: keep path
[enigma2-plugins.git] / pluginsort / src / plugin.py
1 from __future__ import print_function
2
3 # Plugin definition
4 from Plugins.Plugin import PluginDescriptor
5
6 from Screens import PluginBrowser
7 from Screens.MessageBox import MessageBox
8 from Screens.ChoiceBox import ChoiceBox
9 from Components.PluginComponent import PluginComponent, plugins
10 from Tools.Directories import resolveFilename, fileExists, SCOPE_SKIN_IMAGE, SCOPE_PLUGINS
11 from Tools.BoundFunction import boundFunction
12 from Screens.InfoBarGenerics import InfoBarPlugins
13 from Components.config import config, ConfigSubsection, ConfigYesNo
14 from Components.PluginList import PluginList
15 from Components.Converter.TemplatedMultiContent import TemplatedMultiContent
16 from Components.Renderer.Listbox import Listbox as ListboxRenderer
17
18 from Components.ActionMap import ActionMap, NumberActionMap
19 from operator import attrgetter # python 2.5+
20
21 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest
22
23 from enigma import eListboxPythonMultiContent, gFont
24 from Tools.LoadPixmap import LoadPixmap
25
26 from xml.etree.cElementTree import parse as cet_parse
27 try:
28         from xml.etree.cElementTree import ParseError
29 except ImportError as ie:
30         ParseError = SyntaxError
31 from Tools.XMLTools import stringToXML
32
33 from shutil import copyfile, Error
34
35 XML_CONFIG = "/etc/enigma2/pluginsort.xml"
36 DEBUG = False
37
38 config.plugins.pluginsort = ConfigSubsection()
39 config.plugins.pluginsort.show_help = ConfigYesNo(default=True)
40
41 def MyPluginEntryComponent(plugin, backcolor_sel=None):
42         if plugin.icon is None:
43                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "skin_default/icons/plugin.png"))
44         else:
45                 png = plugin.icon
46
47         return [
48                 plugin, plugin.name, plugin.description, png,
49                 #plugin, backcolor_sel, plugin.name, plugin.description, png,
50         ]
51
52 # TODO: make selected color themable
53 SelectedPluginEntryComponent = lambda plugin: MyPluginEntryComponent(plugin, backcolor_sel=8388608)
54
55 class MyPluginList(PluginList):
56         def __init__(self, *args, **kwargs):
57                 PluginList.__init__(self, *args, **kwargs)
58                 self.__inst = None
59
60         def __instance(self):
61                 if self.__inst is not None:
62                         return self.__inst
63                 for x in self.downstream_elements:
64                         if isinstance(x, TemplatedMultiContent):
65                                 for y in x.downstream_elements:
66                                         if isinstance(y, ListboxRenderer):
67                                                 self.__inst = y.instance
68                                                 return self.__inst
69                 return None
70
71         def up(self):
72                 instance = self.__instance()
73                 if instance: instance.moveSelection(instance.moveUp)
74         def down(self):
75                 instance = self.__instance()
76                 if instance: instance.moveSelection(instance.moveDown)
77         def pageUp(self):
78                 instance = self.__instance()
79                 if instance: instance.moveSelection(instance.pageUp)
80         def pageDown(self):
81                 instance = self.__instance()
82                 if instance: instance.moveSelection(instance.pageDown)
83
84 WHEREMAP = {}
85 pdict = PluginDescriptor.__dict__
86 for where in pdict:
87         if where.startswith('WHERE_'):
88                 WHEREMAP[where] = pdict[where]
89 del pdict
90
91 try:
92         dict.iteritems
93         iteritems = lambda d: d.iteritems()
94 except AttributeError:
95         iteritems = lambda d: d.items()
96 reverse = lambda map: dict((v,k) for k,v in iteritems(map))
97
98 class PluginWeights:
99         def __init__(self):
100                 self.plugins = {}
101                 self.load()
102
103         def load(self):
104                 if not fileExists(XML_CONFIG):
105                         return
106
107                 try:
108                         config = cet_parse(XML_CONFIG).getroot()
109                 except ParseError as pe:
110                         from time import time
111                         print("[PluginSort] Parse Error occured in configuration, backing it up and starting from scratch!")
112                         try:
113                                 copyfile(XML_CONFIG, "/etc/enigma2/pluginsort.xml.%d" % (int(time()),))
114                         except Error as she:
115                                 print("[PluginSort] Uh oh, failed to create the backup... I hope you have one anyway :D")
116                         return
117
118                 for wheresection in config.findall('where'):
119                         where = wheresection.get('type')
120                         whereid = WHEREMAP.get(where, None)
121                         whereplugins = wheresection.findall('plugin')
122                         if whereid is None or not whereplugins:
123                                 print("[PluginSort] Ignoring section %s because of invalid id (%s) or no plugins (%s)" % (where, repr(whereid), repr(whereplugins)))
124                                 continue
125
126                         for plugin in whereplugins:
127                                 name = plugin.get('name')
128                                 try:
129                                         weight = int(plugin.get('weight'))
130                                 except ValueError as ve:
131                                         print("[PluginSort] Invalid weight of %s received for plugin %s, ignoring" % (repr(plugin.get('weight')), repr(name)))
132                                 else:
133                                         self.plugins.setdefault(whereid, {})[name] = weight
134
135         def save(self):
136                 lst = ['<?xml version="1.0" ?>\n<pluginsort>\n\n']
137                 append = lst.append
138                 extend = lst.extend
139
140                 idmap = reverse(WHEREMAP)
141                 for key in self.plugins.keys():
142                         whereplugins = self.plugins.get(key, None)
143                         if not whereplugins:
144                                 continue
145
146                         where = idmap[key]
147                         extend((' <where type="', str(where), '">\n'))
148                         for key, value in iteritems(whereplugins):
149                                 extend(('  <plugin name="', stringToXML(str(key)), '" weight="', str(value), '" />\n'))
150                         append((' </where>\n'))
151                 append('\n</pluginsort>\n')
152                 
153                 with open(XML_CONFIG, 'w') as config:
154                         config.writelines(lst)
155
156         def get(self, plugin):
157                 for x in plugin.where:
158                         whereplugins = self.plugins.get(x, None)
159                         weight = whereplugins and whereplugins.get(plugin.name, None)
160                         if weight is not None:
161                                 return weight
162                 return plugin.weight
163
164         def set(self, plugin):
165                 for x in plugin.where:
166                         whereplugins = self.plugins.get(x, None)
167                         if whereplugins:
168                                 whereplugins[plugin.name] = plugin.weight
169                         else:
170                                 self.plugins[x] = {plugin.name: plugin.weight}
171
172 pluginWeights = PluginWeights()
173
174 def PluginComponent_addPlugin(self, plugin, *args, **kwargs):
175         if len(plugin.where) > 1:
176                 print("[PluginSort] Splitting %s up in individual entries (%s)" % (plugin.name, repr(plugin.where)))
177                 for x in plugin.where:
178                         pd = PluginDescriptor(name=plugin.name, where=[x], description=plugin.description, icon=plugin.icon, fnc=plugin.__call__, wakeupfnc=plugin.wakeupfnc, needsRestart=plugin.needsRestart, internal=plugin.internal, weight=plugin.weight)
179                         pd.path = plugin.path
180
181                         newWeight = pluginWeights.get(pd)
182                         if DEBUG: print("[PluginSort] Setting weight of %s from %d to %d" % (pd.name, pd.weight, newWeight))
183                         pd.weight = newWeight
184                         PluginComponent.pluginSort_baseAddPlugin(self, pd, *args, **kwargs)
185
186                 # installedPluginList is a list of original descriptors, but we changed it to be a copy, not a reference. so keep it up to date
187                 if self.firstRun:
188                         self.installedPluginList.append(plugin)
189                         if DEBUG: print("[PluginSort] Adding %s to list of installed plugins (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where)))
190                 return
191
192         newWeight = pluginWeights.get(plugin)
193         if DEBUG: print("[PluginSort] Setting weight of %s from %d to %d" % (plugin.name, plugin.weight, newWeight))
194         plugin.weight = newWeight
195         PluginComponent.pluginSort_baseAddPlugin(self, plugin, *args, **kwargs)
196
197         if self.firstRun:
198                 if DEBUG: print("[PluginSort] Adding %s to list of installed plugins (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where)))
199                 self.installedPluginList.append(plugin)
200
201 if DEBUG:
202         def PluginComponent_removePlugin(self, plugin, *args, **kwargs):
203                 print("[PluginSort] Supposed to remove plugin: %s (%s, %s)." % (plugin.name, plugin.path, repr(plugin.where)))
204                 try:
205                         PluginComponent.pluginSort_baseRemovePlugin(self, plugin, *args, **kwargs)
206                 except ValueError as ve:
207                         revMap = reverse(WHEREMAP)
208                         print("-"*40)
209                         print("-"*40)
210                         print("-"*40)
211                         print("[PluginSort] pluginList: %s" % (repr([(x.name, x.path, repr([revMap[y] for y in x.where])) for x in self.pluginList]),))
212                         for w in plugin.where:
213                                 print("[PluginSort] plugins[%s]: %s" % (revMap[w], repr([(x.name, x.path, repr([revMap[y] for y in x.where])) for x in self.plugins[w]])))
214         PluginComponent.pluginSort_baseRemovePlugin = PluginComponent.removePlugin
215         PluginComponent.removePlugin = PluginComponent_removePlugin
216
217 OriginalPluginBrowser = PluginBrowser.PluginBrowser
218 class SortingPluginBrowser(OriginalPluginBrowser):
219         def __init__(self, *args, **kwargs):
220                 self.movemode = False
221                 self.selected = -1
222                 if 'where' in kwargs:
223                         self.where = kwargs['where']
224                         del kwargs['where']
225                 else:
226                         self.where = PluginDescriptor.WHERE_PLUGINMENU
227
228                 OriginalPluginBrowser.__init__(self, *args, **kwargs)
229                 self.skinName = ["SortingPluginBrowser", "PluginBrowser"] # XXX: fallback is evil because it makes moving more confusing :P
230
231                 self["pluginlist"] = MyPluginList(self.list)
232
233                 self["ColorActions"] = ActionMap(["ColorActions"],
234                         {
235                                 "green": self.toggleMoveMode,
236                         }, -2
237                 )
238
239                 self["WizardActions"] = ActionMap(["WizardActions"],
240                         {
241                                 "left": boundFunction(self.doMove, self["pluginlist"].pageUp),
242                                 "right": boundFunction(self.doMove, self["pluginlist"].pageDown),
243                                 "up": boundFunction(self.doMove, self["pluginlist"].up),
244                                 "down": boundFunction(self.doMove, self["pluginlist"].down),
245                         }, -2
246                 )
247
248                 if self.where != PluginDescriptor.WHERE_PLUGINMENU:
249                         self.toggleMoveMode()
250                         self.onShow.append(self.setCustomTitle)
251                 else:
252                         self["NumberActions"] = NumberActionMap(["NumberActions"],
253                                 {
254                                         "1": self.keyNumberGlobal,
255                                         "2": self.keyNumberGlobal,
256                                         "3": self.keyNumberGlobal,
257                                         "4": self.keyNumberGlobal,
258                                         "5": self.keyNumberGlobal,
259                                         "6": self.keyNumberGlobal,
260                                         "7": self.keyNumberGlobal,
261                                         "8": self.keyNumberGlobal,
262                                         "9": self.keyNumberGlobal,
263                                         "0": self.keyNumberGlobal,
264                                 }, -2
265                         )
266
267                         self["MenuActions"] = ActionMap(["MenuActions"],
268                                 {
269                                         "menu": self.openMenu,
270                                 }, -1
271                         )
272                 self.onFirstExecBegin.append(self.firstExec)
273
274         def firstExec(self):
275                 if config.plugins.pluginsort.show_help.value and pluginSortHelp:
276                         config.plugins.pluginsort.show_help.value = False
277                         config.plugins.pluginsort.show_help.save()
278                         pluginSortHelp.open(self.session)
279
280         def setCustomTitle(self):
281                 titleMap = {
282                         PluginDescriptor.WHERE_EXTENSIONSMENU: _("Sort Extensions"),
283                         PluginDescriptor.WHERE_MOVIELIST: _("Sort MovieList Extensions"),
284                         PluginDescriptor.WHERE_EVENTINFO: _("Sort EventInfo Extensions"),
285                 }
286                 title = titleMap.get(self.where, None)
287                 if title:
288                         self.setTitle(title)
289
290         def keyNumberGlobal(self, number):
291                 if not self.movemode:
292                         realnumber = (number - 1) % 10
293                         if realnumber < len(self.list):
294                                 self["pluginlist"].index = realnumber
295                                 self.save()
296
297         def close(self, *args, **kwargs):
298                 if self.movemode:
299                         self.toggleMoveMode()
300                 OriginalPluginBrowser.close(self, *args, **kwargs)
301
302         # copied from PluginBrowser because we redo pretty much anything :-)
303         def updateList(self):
304                 self.pluginlist = plugins.getPlugins(self.where)
305                 if self.where in (PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU):
306                         self.pluginlist.sort(key=attrgetter('weight', 'name')) # sort first by weight, then by name; we get pretty much a weight sorted but otherwise random list
307                 else: #if self.where in (PluginDescriptor.WHERE_EVENTINFO, PluginDescriptor.WHERE_MOVIELIST):
308                         self.pluginlist.sort(key=attrgetter('weight'))
309                 self.list = [MyPluginEntryComponent(plugin) for plugin in self.pluginlist]
310                 self["pluginlist"].list = self.list
311                 if self.where == PluginDescriptor.WHERE_PLUGINMENU:
312                         # TRANSLATORS: leaving this empty is encouraged to not cause any confusion (this string was taken directly from the standard PluginBrowser)
313                         self["red"].setText(_("Manage extensions"))
314                         self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
315                 else:
316                         self["red"].setText("")
317                         self["green"].setText(_("Sort") if not self.movemode else _("End Sort"))
318                         self["SoftwareActions"].setEnabled(False)
319
320         def doMove(self, func):
321                 if self.selected != -1:
322                         oldpos = self["pluginlist"].index
323                         func()
324                         entry = self.list.pop(oldpos)
325                         newpos = self["pluginlist"].index
326                         self.list.insert(newpos, entry)
327                         # XXX: modifyEntry is broken - I'd say a job well done :P
328                         #self["pluginlist"].modifyEntry(oldpos, self.list[oldpos])
329                         #self["pluginlist"].modifyEntry(newpos, self.list[newpos])
330                         self["pluginlist"].updateList(self.list)
331                 else:
332                         func()
333
334         def save(self):
335                 selected = self.selected
336                 if not self.movemode:
337                         OriginalPluginBrowser.save(self)
338                 elif selected != -1:
339                         Len = len(self.pluginlist)
340                         newpos = self["pluginlist"].index
341                         entry = self.pluginlist[selected]
342                         self.pluginlist.remove(entry)
343                         self.pluginlist.insert(newpos, entry)
344
345                         # we moved up, increase weight of plugins after us
346                         if newpos < selected:
347                                 print("[PluginSort]", entry.name, "moved up")
348                                 i = newpos + 1
349                                 # since we moved up, there has to be an entry after this one
350                                 diff = abs(self.pluginlist[i].weight - self.pluginlist[newpos].weight) + 1
351                                 print("[PluginSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (i, self.pluginlist[i].weight, newpos, self.pluginlist[newpos].weight, diff))
352                                 while i < Len:
353                                         if DEBUG: print("[PluginSort] INCREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff)
354                                         self.pluginlist[i].weight += diff
355                                         i += 1
356                         # we moved down, decrease weight of plugins before us
357                         elif newpos > selected:
358                                 print("[PluginSort]", entry.name, "moved down")
359                                 i = newpos - 1
360                                 # since we moved up, there has to be an entry before this one
361                                 diff = abs(self.pluginlist[newpos].weight - self.pluginlist[i].weight) + 1
362                                 print("[PluginSort] Using weight from %d (%d) and %d (%d) to calculate diff (%d)" % (newpos, self.pluginlist[newpos].weight, i, self.pluginlist[i].weight, diff))
363                                 while i > -1:
364                                         if DEBUG: print("[PluginSort] DECREASE WEIGHT OF", self.pluginlist[i].name, "BY", diff)
365                                         self.pluginlist[i].weight -= diff
366                                         i -= 1
367                         else:
368                                 if DEBUG: print("[PluginSort]", entry.name, "did not move (%d to %d)?" % (selected, newpos))
369
370                         self.list = [MyPluginEntryComponent(plugin) for plugin in self.pluginlist]
371                         if DEBUG: print("[PluginSort] NEW LIST:", [(plugin.name, plugin.weight) for plugin in self.pluginlist])
372                         # XXX: modifyEntry is broken - I'd say a job well done :P
373                         #self["pluginlist"].modifyEntry(newpos, self.list[newpos])
374                         self["pluginlist"].updateList(self.list)
375                         self.selected = -1
376                 else:
377                         self.selected = self["pluginlist"].index
378                         self.list[self.selected] = SelectedPluginEntryComponent(self.pluginlist[self.selected])
379                         # XXX: modifyEntry is broken - I'd say a job well done :P
380                         #self["pluginlist"].modifyEntry(self.selected, self.list[self.selected])
381                         self["pluginlist"].updateList(self.list)
382         
383         def openMenu(self):
384                 if self.movemode:
385                         # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
386                         moveString = _("disable move mode")
387                 else:
388                         # TRANSLATORS: there is no need to translate this string, as it was reused from e2 core
389                         moveString = _("enable move mode")
390
391                 list = [
392                         (moveString, self.toggleMoveMode),
393                         (_("move extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_EXTENSIONSMENU)),
394                         (_("move movie extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_MOVIELIST)),
395                         (_("move event extensions"), boundFunction(self.openMover, PluginDescriptor.WHERE_EVENTINFO)),
396                 ]
397
398                 if fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/PluginHider/plugin.py")):
399                         list.insert(0, (_("hide selected plugin"), self.hidePlugin))
400
401                 if pluginSortHelp:
402                         list.insert(0, (_("Help"), boundFunction(pluginSortHelp.open, self.session)))
403
404                 self.session.openWithCallback(
405                         self.menuCallback,
406                         ChoiceBox,
407                         list = list,
408                 )
409
410         def menuCallback(self, ret):
411                 ret and ret[1]()
412
413         def openMover(self, where):
414                 self.session.open(SortingPluginBrowser, where=where)
415
416         def hidePlugin(self):
417                 try:
418                         from Plugins.Extensions.PluginHider.plugin import hidePlugin
419                 except Exception as e:
420                         self.session.open(MessageBox, _("Unable to load PluginHider"), MessageBox.TYPE_ERROR)
421                 else:
422                         hidePlugin(self["pluginlist"].current[0])
423
424                         # we were actually in move mode, so save the current position
425                         if self.selected != -1:
426                                 self.save()
427                         self.updateList()
428
429         def toggleMoveMode(self):
430                 if self.movemode:
431                         if self.selected != -1:
432                                 self.save()
433                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
434                                 self["green"].setText(_("Sort"))
435
436                         for plugin in self.pluginlist:
437                                 pluginWeights.set(plugin)
438                         pluginWeights.save()
439
440                         # auto-close if not "PluginBrowser"
441                         if self.where != PluginDescriptor.WHERE_PLUGINMENU:
442                                 self.movemode = False
443                                 return self.close()
444                 else:
445                         if fileExists(resolveFilename(SCOPE_PLUGINS, "SystemPlugins/SoftwareManager/plugin.py")):
446                                 self["green"].setText(_("End Sort"))
447                 self.movemode = not self.movemode
448
449 def autostart(reason, *args, **kwargs):
450         if reason == 0:
451                 if hasattr(PluginComponent, 'pluginSort_baseAddPlugin'):
452                         print("[PluginSort] Something went wrong as our autostart handler was called multiple times for startup, printing traceback and ignoring.")
453                         import traceback, sys
454                         traceback.print_stack(limit=5, file=sys.stdout)
455                 else:
456                         PluginComponent.pluginSort_baseAddPlugin = PluginComponent.addPlugin
457                         PluginComponent.addPlugin = PluginComponent_addPlugin
458
459                         # we use a copy for installed plugins because we might change the 'where'-lists
460                         plugins.installedPluginList = plugins.pluginList[:]
461                         def PluginComponent__setattr__(self, key, value):
462                                 if key == 'installedPluginList': return
463                                 else: self.__dict__[key] = value
464                         PluginComponent.__setattr__ = PluginComponent__setattr__
465
466                         if hasattr(plugins, 'pluginHider_baseGetPlugins'):
467                                 pluginlist = plugins.pluginHider_baseGetPlugins([PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU, PluginDescriptor.WHERE_MOVIELIST, PluginDescriptor.WHERE_EVENTINFO])
468                         else:
469                                 pluginlist = plugins.getPlugins([PluginDescriptor.WHERE_PLUGINMENU, PluginDescriptor.WHERE_EXTENSIONSMENU, PluginDescriptor.WHERE_MOVIELIST, PluginDescriptor.WHERE_EVENTINFO])
470
471                         # "fix" weight of plugins already added to list, future ones will be fixed automatically
472                         fixed = []
473                         for plugin in pluginlist:
474                                 if plugin in fixed: continue # skip double entries
475
476                                 # create individual entries for multiple wheres, this is potentially harmful!
477                                 if len(plugin.where) > 1:
478                                         # remove all entries except for a potential autostart one (highly unlikely to mix autostart with one of the above, but you never know :D)
479                                         if PluginDescriptor.WHERE_AUTOSTART in plugin.where:
480                                                 plugin.where.remove(PluginDescriptor.WHERE_AUTOSTART)
481                                                 hadAutostart = True
482                                         else:
483                                                 hadAutostart = False
484                                         plugins.removePlugin(plugin)
485                                         plugins.addPlugin(plugin) # this is our own addPlugin now, which automatically creates copies
486
487                                         # HACK: re-add autostart entry to internal list inside PluginComponent
488                                         if hadAutostart:
489                                                 plugin.where = [ PluginDescriptor.WHERE_AUTOSTART ]
490                                                 plugins.pluginList.append(plugin)
491
492                                 # we're keeping the entry, just fix the weight
493                                 else:
494                                         newWeight = pluginWeights.get(plugin)
495                                         print("[PluginSort] Fixing weight for %s (was %d, now %d)" % (plugin.name, plugin.weight, newWeight))
496                                         plugin.weight = newWeight
497
498                                 fixed.append(plugin)
499
500                         # let movieepg fix extensions list sorting if installed, else do this ourselves
501                         if not fileExists(resolveFilename(SCOPE_PLUGINS, "Extensions/MovieEPG/plugin.py")):
502                                 def InfoBarPlugins_getPluginList(self, *args, **kwargs):
503                                         l = InfoBarPlugins.pluginSort_baseGetPluginList(self, *args, **kwargs)
504                                         try:
505                                                 l.sort(key=lambda e: (e[0][1].args[0].weight, e[2]))
506                                         except Exception as e:
507                                                 print("[PluginSort] Failed to sort extensions", e)
508                                         return l
509
510                                 InfoBarPlugins.pluginSort_baseGetPluginList = InfoBarPlugins.getPluginList
511                                 InfoBarPlugins.getPluginList = InfoBarPlugins_getPluginList
512
513
514                         PluginBrowser.PluginBrowser = SortingPluginBrowser
515         else:
516                 if hasattr(PluginComponent, 'pluginSort_baseAddPlugin'):
517                         PluginComponent.addPlugin = PluginComponent.pluginSort_baseAddPlugin
518                         del PluginComponent.pluginSort_baseAddPlugin
519                 if hasattr(InfoBarPlugins, 'pluginSort_baseGetPluginList'):
520                         InfoBarPlugins.getPluginList = InfoBarPlugins.pluginSort_baseGetPluginList
521                         del InfoBarPlugins.pluginSort_baseGetPluginList
522                 PluginBrowser.PluginBrowser = OriginalPluginBrowser
523
524 #pragma mark - Help
525 try:
526         from Plugins.SystemPlugins.MPHelp import registerHelp, showHelp, XMLHelpReader
527         reader = XMLHelpReader(resolveFilename(SCOPE_PLUGINS, "Extensions/PluginSort/mphelp.xml"), translate=_)
528         pluginSortHelp = registerHelp(*reader)
529 except Exception as e:
530         print("[PluginSort] Unable to initialize MPHelp:", e,"- Help not available!")
531         pluginSortHelp = None
532 #pragma mark -
533
534 def Plugins(**kwargs):
535         return [
536                 PluginDescriptor(
537                         where=PluginDescriptor.WHERE_AUTOSTART,
538                         fnc=autostart,
539                         needsRestart=True,
540                 ),
541         ]