fd04869536181f3f8a31591ff6777ef99501bb82
[vuplus_dvbapp] / lib / python / Plugins / Extensions / DVDPlayer / plugin.py
1 from os import path as os_path, remove as os_remove, listdir as os_listdir, system
2 from time import strftime
3 from enigma import eTimer, iPlayableService, eServiceCenter, iServiceInformation, eServiceReference, iServiceKeys
4 from Screens.Screen import Screen
5 from Screens.MessageBox import MessageBox
6 from Screens.ChoiceBox import ChoiceBox
7 from Screens.InputBox import InputBox
8 from Screens.HelpMenu import HelpableScreen
9 from Screens.InfoBarGenerics import InfoBarSeek, InfoBarPVRState, InfoBarCueSheetSupport, InfoBarShowHide, InfoBarNotifications
10 from Components.ActionMap import ActionMap, NumberActionMap, HelpableActionMap
11 from Components.Label import Label
12 from Components.FileList import FileList
13 from Components.ServiceEventTracker import ServiceEventTracker, InfoBarBase
14 from Components.config import config
15 from Components.ProgressBar import ProgressBar
16 from ServiceReference import ServiceReference
17 from Tools.Directories import pathExists, fileExists
18
19 import random
20 import servicedvd # load c++ part of dvd player plugin
21
22 lastpath = ""
23
24 class FileBrowser(Screen):
25         skin = """
26         <screen name="FileBrowser" position="100,100" size="520,376" title="DVD File Browser" >
27                 <widget name="filelist" position="0,0" size="520,376" scrollbarMode="showOnDemand" />
28         </screen>"""
29         def __init__(self, session):
30                 Screen.__init__(self, session)
31                 global lastpath
32                 if lastpath is not None:
33                         currDir = lastpath + "/"
34                 else:
35                         currDir = "/media/dvd/"
36                 if not pathExists(currDir):
37                         currDir = "/"
38                 #else:
39                         #print system("mount "+currDir)
40                 self.filelist = FileList(currDir, matchingPattern = "(?i)^.*\.(iso)", useServiceRef = True)
41                 self["filelist"] = self.filelist
42
43                 self["FilelistActions"] = ActionMap(["OkCancelActions"],
44                         {
45                                 "ok": self.ok,
46                                 "cancel": self.exit
47                         })
48
49         def ok(self):
50                 global lastpath
51                 filename = self["filelist"].getFilename()
52                 if filename is not None:
53                         lastpath = filename[0:filename.rfind("/")]
54                         if filename.upper().endswith("VIDEO_TS/"):
55                                 print "dvd structure found, trying to open..."
56                                 self.close(filename[0:-9])
57                 if self["filelist"].canDescent(): # isDir
58                         self["filelist"].descent()
59                 else:
60                         self.close(filename)
61
62         def exit(self):
63                 self.close(None)
64                 
65 class DVDSummary(Screen):
66         skin = """
67         <screen position="0,0" size="132,64">
68                 <widget source="session.CurrentService" render="Label" position="5,4" size="120,28" font="Regular;12" transparent="1" >
69                         <convert type="ServiceName">Name</convert>
70                 </widget>
71                 <widget name="DVDPlayer" position="5,30" size="66,16" font="Regular;12" transparent="1" />
72                 <widget name="Chapter" position="72,30" size="54,16" font="Regular;12" transparent="1" halign="right" />
73                 <widget source="session.CurrentService" render="Label" position="66,46" size="60,18" font="Regular;16" transparent="1" halign="right" >
74                         <convert type="ServicePosition">Position</convert>
75                 </widget>
76                 <widget source="session.CurrentService" render="Progress" position="6,46" size="60,18" borderWidth="1" >
77                         <convert type="ServicePosition">Position</convert>
78                 </widget>
79         </screen>"""
80
81         def __init__(self, session, parent):
82                 Screen.__init__(self, session, parent)
83
84                 self["DVDPlayer"] = Label("DVD Player")
85                 self["Title"] = Label("")
86                 self["Time"] = Label("")
87                 self["Chapter"] = Label("")
88
89         def updateChapter(self, chapter):
90                 self["Chapter"].setText(chapter)
91
92         def setTitle(self, title):
93                 self["Title"].setText(title)
94
95 class DVDOverlay(Screen):
96         skin = """<screen name="DVDOverlay" position="0,0" size="720,576" flags="wfNoBorder" zPosition="-1" backgroundColor="transparent" />"""
97         def __init__(self, session, args = None):
98                 Screen.__init__(self, session)
99                 
100 class ChapterZap(Screen):
101         skin = """
102         <screen name="ChapterZap" position="235,255" size="250,60" title="Chapter" >
103                 <widget name="chapter" position="35,15" size="110,25" font="Regular;23" />
104                 <widget name="number" position="145,15" size="80,25" halign="right" font="Regular;23" />
105         </screen>"""
106         
107         def quit(self):
108                 self.Timer.stop()
109                 self.close(0)
110
111         def keyOK(self):
112                 self.Timer.stop()
113                 self.close(int(self["number"].getText()))
114
115         def keyNumberGlobal(self, number):
116                 self.Timer.start(3000, True)            #reset timer
117                 self.field = self.field + str(number)
118                 self["number"].setText(self.field)
119                 if len(self.field) >= 4:
120                         self.keyOK()
121
122         def __init__(self, session, number):
123                 Screen.__init__(self, session)
124                 self.field = str(number)
125
126                 self["chapter"] = Label(_("Chapter:"))
127
128                 self["number"] = Label(self.field)
129
130                 self["actions"] = NumberActionMap( [ "SetupActions" ],
131                         {
132                                 "cancel": self.quit,
133                                 "ok": self.keyOK,
134                                 "1": self.keyNumberGlobal,
135                                 "2": self.keyNumberGlobal,
136                                 "3": self.keyNumberGlobal,
137                                 "4": self.keyNumberGlobal,
138                                 "5": self.keyNumberGlobal,
139                                 "6": self.keyNumberGlobal,
140                                 "7": self.keyNumberGlobal,
141                                 "8": self.keyNumberGlobal,
142                                 "9": self.keyNumberGlobal,
143                                 "0": self.keyNumberGlobal
144                         })
145
146                 self.Timer = eTimer()
147                 self.Timer.callback.append(self.keyOK)
148                 self.Timer.start(3000, True)
149
150 class DVDPlayer(Screen, InfoBarBase, InfoBarNotifications, InfoBarSeek, InfoBarCueSheetSupport, InfoBarPVRState, InfoBarShowHide, HelpableScreen):
151         ALLOW_SUSPEND = True
152         ENABLE_RESUME_SUPPORT = True
153         
154         skin = """
155         <screen name="DVDPlayer" flags="wfNoBorder" position="0,380" size="720,160" title="InfoBar" backgroundColor="transparent" >
156                 <!-- Background -->
157                 <ePixmap position="0,0" zPosition="-2" size="720,160" pixmap="skin_default/info-bg_mp.png" alphatest="off" />
158                 <ePixmap position="29,40" zPosition="0" size="665,104" pixmap="skin_default/screws_mp.png" alphatest="on" transparent="1" />
159                 <!-- colorbuttons -->
160                 <ePixmap position="48,70" zPosition="0" size="108,13" pixmap="skin_default/icons/mp_buttons.png" alphatest="on" />
161                 <!-- Servicename -->
162                 <ePixmap pixmap="skin_default/icons/icon_event.png" position="207,78" zPosition="1" size="15,10" alphatest="on" />
163                 <widget source="session.CurrentService" render="Label" position="230,73" size="300,22" font="Regular;20" backgroundColor="#263c59" shadowColor="#1d354c" shadowOffset="-1,-1" transparent="1" noWrap="1">
164                         <convert type="ServiceName">Name</convert>
165                 </widget>
166                 <!-- Chapter info -->
167                 <widget name="chapterLabel" position="230,96" size="360,22" font="Regular;20" foregroundColor="#c3c3c9" backgroundColor="#263c59" transparent="1" />
168                 <!-- Audio track info -->
169                 <ePixmap pixmap="skin_default/icons/icon_dolby.png" position="540,73" zPosition="1" size="26,16" alphatest="on"/>
170                 <widget name="audioLabel" position="570,73" size="130,22" font="Regular;18" backgroundColor="#263c59" shadowColor="#1d354c" shadowOffset="-1,-1" transparent="1" />
171                 <!-- Subtitle track info -->
172                 <widget source="session.CurrentService" render="Pixmap" pixmap="skin_default/icons/icon_txt.png" position="540,96" zPosition="1" size="26,16" alphatest="on" >
173                         <convert type="ServiceInfo">HasTelext</convert>
174                         <convert type="ConditionalShowHide" />
175                 </widget>
176                 <widget name="subtitleLabel" position="570,96" size="130,22" font="Regular;18" backgroundColor="#263c59" shadowColor="#1d354c" shadowOffset="-1,-1" transparent="1" />
177                 <!-- Elapsed time -->
178                 <widget source="session.CurrentService" render="Label" position="205,129" size="100,20" font="Regular;18" halign="center" valign="center" backgroundColor="#06224f" shadowColor="#1d354c" shadowOffset="-1,-1" transparent="1" >
179                         <convert type="ServicePosition">Position,ShowHours</convert>
180                 </widget>
181                 <!-- Progressbar (movie position)-->
182                 <widget source="session.CurrentService" render="PositionGauge" position="300,133" size="270,10" zPosition="2" pointer="skin_default/position_pointer.png:540,0" transparent="1" >
183                         <convert type="ServicePosition">Gauge</convert>
184                 </widget>
185                 <!-- Remaining time -->
186                 <widget source="session.CurrentService" render="Label" position="576,129" size="100,20" font="Regular;18" halign="center" valign="center" backgroundColor="#06224f" shadowColor="#1d354c" shadowOffset="-1,-1" transparent="1" >
187                         <convert type="ServicePosition">Remaining,Negate,ShowHours</convert>
188                 </widget>
189         </screen>"""
190
191         def save_infobar_seek_config(self):
192                 self.saved_config_speeds_forward = config.seek.speeds_forward.value
193                 self.saved_config_speeds_backward = config.seek.speeds_backward.value
194                 self.saved_config_enter_forward = config.seek.enter_forward.value
195                 self.saved_config_enter_backward = config.seek.enter_backward.value
196                 self.saved_config_seek_stepwise_minspeed = config.seek.stepwise_minspeed.value
197                 self.saved_config_seek_stepwise_repeat = config.seek.stepwise_repeat.value
198                 self.saved_config_seek_on_pause = config.seek.on_pause.value
199                 self.saved_config_seek_speeds_slowmotion = config.seek.speeds_slowmotion.value
200
201         def change_infobar_seek_config(self):
202                 config.seek.speeds_forward.value = [2, 4, 8, 16, 32, 64]
203                 config.seek.speeds_backward.value = [8, 16, 32, 64]
204                 config.seek.speeds_slowmotion.value = [ ]
205                 config.seek.enter_forward.value = "2"
206                 config.seek.enter_backward.value = "2"
207                 config.seek.stepwise_minspeed.value = "Never"
208                 config.seek.stepwise_repeat.value = "3"
209                 config.seek.on_pause.value = "play"
210
211         def restore_infobar_seek_config(self):
212                 config.seek.speeds_forward.value = self.saved_config_speeds_forward
213                 config.seek.speeds_backward.value = self.saved_config_speeds_backward
214                 config.seek.speeds_slowmotion.value = self.saved_config_seek_speeds_slowmotion
215                 config.seek.enter_forward.value = self.saved_config_enter_forward
216                 config.seek.enter_backward.value = self.saved_config_enter_backward
217                 config.seek.stepwise_minspeed.value = self.saved_config_seek_stepwise_minspeed
218                 config.seek.stepwise_repeat.value = self.saved_config_seek_stepwise_repeat
219                 config.seek.on_pause.value = self.saved_config_seek_on_pause
220
221         def __init__(self, session, args = None):
222                 Screen.__init__(self, session)
223                 InfoBarBase.__init__(self)
224                 InfoBarNotifications.__init__(self)
225                 InfoBarCueSheetSupport.__init__(self, actionmap = "MediaPlayerCueSheetActions")
226                 InfoBarShowHide.__init__(self)
227                 HelpableScreen.__init__(self)
228                 self.save_infobar_seek_config()
229                 self.change_infobar_seek_config()
230                 InfoBarSeek.__init__(self)
231                 InfoBarPVRState.__init__(self)
232                 self.dvdScreen = self.session.instantiateDialog(DVDOverlay)
233
234                 self.oldService = self.session.nav.getCurrentlyPlayingServiceReference()
235                 self.session.nav.stopService()
236                 self["audioLabel"] = Label("1")
237                 self["subtitleLabel"] = Label("")
238                 self["chapterLabel"] = Label("")
239                 self.totalChapters = 0
240                 self.currentChapter = 0
241                 self.totalTitles = 0
242                 self.currentTitle = 0
243
244                 self.__event_tracker = ServiceEventTracker(screen=self, eventmap=
245                         {
246                                 iPlayableService.evUser: self.__timeUpdated,
247                                 iPlayableService.evUser+1: self.__statePlay,
248                                 iPlayableService.evUser+2: self.__statePause,
249                                 iPlayableService.evUser+3: self.__osdFFwdInfoAvail,
250                                 iPlayableService.evUser+4: self.__osdFBwdInfoAvail,
251                                 iPlayableService.evUser+5: self.__osdStringAvail,
252                                 iPlayableService.evUser+6: self.__osdAudioInfoAvail,
253                                 iPlayableService.evUser+7: self.__osdSubtitleInfoAvail,
254                                 iPlayableService.evUser+8: self.__chapterUpdated,
255                                 iPlayableService.evUser+9: self.__titleUpdated,
256                                 #iPlayableService.evUser+10: self.__initializeDVDinfo,
257                                 iPlayableService.evUser+11: self.__menuOpened,
258                                 iPlayableService.evUser+12: self.__menuClosed
259                         })
260
261                 self["DVDPlayerDirectionActions"] = HelpableActionMap(self, "DirectionActions",
262                         {
263                                 #MENU KEY DOWN ACTIONS
264                                 "left": (self.keyLeft, _("DVD left key")),
265                                 "right": (self.keyRight, _("DVD right key")),
266                                 "up": (self.keyUp, _("DVD up key")),
267                                 "down": (self.keyDown, _("DVD down key")),
268
269                                 #MENU KEY REPEATED ACTIONS
270                                 "leftRepeated": self.doNothing,
271                                 "rightRepeated": self.doNothing,
272                                 "upRepeated": self.doNothing,
273                                 "downRepeated": self.doNothing,
274
275                                 #MENU KEY UP ACTIONS
276                                 "leftUp": self.doNothing,
277                                 "rightUp": self.doNothing,
278                                 "upUp": self.doNothing,
279                                 "downUp": self.doNothing,
280                         }, -2)
281
282                 self["OkCancelActions"] = HelpableActionMap(self, "OkCancelActions",
283                         {
284                                 "ok": (self.keyOk, _("DVD ENTER key")),
285                                 "cancel": self.keyCancel,
286                         }, -2)
287
288                 self["DVDPlayerPlaybackActions"] = HelpableActionMap(self, "DVDPlayerActions",
289                         {
290                                 #PLAYER ACTIONS
291                                 "dvdMenu": (self.enterDVDMenu, _("show DVD main menu")),
292                                 "toggleInfo": (self.toggleInfo, _("toggle time, chapter, audio, subtitle info")),
293                                 "nextChapter": (self.nextChapter, _("forward to the next chapter")),
294                                 "prevChapter": (self.prevChapter, _("rewind to the previous chapter")),
295                                 "nextTitle": (self.nextTitle, _("jump forward to the next title")),
296                                 "prevTitle": (self.prevTitle, _("jump back to the previous title")),
297                                 "tv": (self.askLeavePlayer, _("exit DVD player or return to file browser")),
298                                 "dvdAudioMenu": (self.enterDVDAudioMenu, _("(show optional DVD audio menu)")),
299                                 "nextAudioTrack": (self.nextAudioTrack, _("switch to the next audio track")),
300                                 "nextSubtitleTrack": (self.nextSubtitleTrack, _("switch to the next subtitle language")),
301                                 "seekBeginning": (self.seekBeginning, _("Jump to video title 1 (play movie from start)")),
302                         }, -2)
303                         
304                 self["NumberActions"] = NumberActionMap( [ "NumberActions"],
305                         {
306                                 "1": self.keyNumberGlobal,
307                                 "2": self.keyNumberGlobal,
308                                 "3": self.keyNumberGlobal,
309                                 "4": self.keyNumberGlobal,
310                                 "5": self.keyNumberGlobal,
311                                 "6": self.keyNumberGlobal,
312                                 "7": self.keyNumberGlobal,
313                                 "8": self.keyNumberGlobal,
314                                 "9": self.keyNumberGlobal,
315                                 "0": self.keyNumberGlobal,
316                         })
317
318                 self.onClose.append(self.__onClose)
319                 self.onFirstExecBegin.append(self.showFileBrowser)
320                 self.service = None
321                 self.in_menu = False
322                 
323         def keyNumberGlobal(self, number):
324                 print "You pressed number " + str(number)
325                 self.session.openWithCallback(self.numberEntered, ChapterZap, number)
326
327         def numberEntered(self, retval):
328 #               print self.servicelist
329                 if retval > 0:
330                         self.zapToNumber(retval)
331
332         def serviceStarted(self): #override InfoBarShowHide function
333                 pass
334
335         def doEofInternal(self, playing):
336                 if self.in_menu:
337                         self.hide()
338
339         def __menuOpened(self):
340                 self.hide()
341                 self.in_menu = True
342
343         def __menuClosed(self):
344                 self.show()
345                 self.in_menu = False
346
347         def setChapterLabel(self):
348                 chapterLCD = "Menu"
349                 chapterOSD = "DVD Menu"
350                 if self.currentTitle > 0:
351                         chapterLCD = "%s %d" % (_("Chap."), self.currentChapter)
352                         chapterOSD = "DVD %s %d/%d" % (_("Chapter"), self.currentChapter, self.totalChapters)
353                         chapterOSD += " (%s %d/%d)" % (_("Title"), self.currentTitle, self.totalTitles)
354                 self["chapterLabel"].setText(chapterOSD)
355                 try:
356                         self.session.summary.updateChapter(chapterLCD)
357                 except:
358                         pass
359
360         def doNothing(self):
361                 pass
362
363         def toggleInfo(self):
364                 if not self.in_menu:
365                         self.toggleShow()
366                         print "toggleInfo"
367
368         def __timeUpdated(self):
369                 print "timeUpdated"
370
371         def __statePlay(self):
372                 print "statePlay"
373
374         def __statePause(self):
375                 print "statePause"
376
377         def __osdFFwdInfoAvail(self):
378                 self.setChapterLabel()
379                 print "FFwdInfoAvail"
380
381         def __osdFBwdInfoAvail(self):
382                 self.setChapterLabel()
383                 print "FBwdInfoAvail"
384
385         def __osdStringAvail(self):
386                 print "StringAvail"
387
388         def __osdAudioInfoAvail(self):
389                 audioString = self.service.info().getInfoString(iPlayableService.evUser+6)
390                 print "AudioInfoAvail "+audioString
391                 self["audioLabel"].setText(audioString)
392                 if not self.in_menu:
393                         self.doShow()
394
395         def __osdSubtitleInfoAvail(self):
396                 subtitleString = self.service.info().getInfoString(iPlayableService.evUser+7)
397                 print "SubtitleInfoAvail "+subtitleString
398                 self["subtitleLabel"].setText(subtitleString)
399                 if not self.in_menu:
400                         self.doShow()
401
402         def __chapterUpdated(self):
403                 self.currentChapter = self.service.info().getInfo(iPlayableService.evUser+8)
404                 self.totalChapters = self.service.info().getInfo(iPlayableService.evUser+80)
405                 self.setChapterLabel()
406                 print "__chapterUpdated: %d/%d" % (self.currentChapter, self.totalChapters)
407
408         def __titleUpdated(self):
409                 self.currentTitle = self.service.info().getInfo(iPlayableService.evUser+9)
410                 self.totalTitles = self.service.info().getInfo(iPlayableService.evUser+90)
411                 self.setChapterLabel()
412                 print "__titleUpdated: %d/%d" % (self.currentTitle, self.totalTitles)
413                 if not self.in_menu:
414                         self.doShow()
415                 
416         #def __initializeDVDinfo(self):
417                 #self.__osdAudioInfoAvail()
418                 #self.__osdSubtitleInfoAvail()
419
420         def askLeavePlayer(self):
421                 self.session.openWithCallback(self.exitCB, ChoiceBox, title=_("Leave DVD Player?"), list=[(_("Exit"), "exit"), (_("Return to file browser"), "browser"), (_("Continue playing"), "play")])
422
423         def nextAudioTrack(self):
424                 if self.service:
425                         self.service.keys().keyPressed(iServiceKeys.keyUser)
426
427         def nextSubtitleTrack(self):
428                 if self.service:
429                         self.service.keys().keyPressed(iServiceKeys.keyUser+1)
430
431         def enterDVDAudioMenu(self):
432                 if self.service:
433                         self.service.keys().keyPressed(iServiceKeys.keyUser+2)
434
435         def nextChapter(self):
436                 if self.service:
437                         self.service.keys().keyPressed(iServiceKeys.keyUser+3)
438
439         def prevChapter(self):
440                 if self.service:
441                         self.service.keys().keyPressed(iServiceKeys.keyUser+4)
442
443         def nextTitle(self):
444                 if self.service:
445                         self.service.keys().keyPressed(iServiceKeys.keyUser+5)
446
447         def prevTitle(self):
448                 if self.service:
449                         self.service.keys().keyPressed(iServiceKeys.keyUser+6)
450
451         def enterDVDMenu(self):
452                 if self.service:
453                         self.service.keys().keyPressed(iServiceKeys.keyUser+7)
454                         
455         def seekBeginning(self):
456                 if self.service:
457                         seekable = self.getSeek()
458                         if seekable is not None:
459                                 seekable.seekTo(0)
460                                 
461         def zapToNumber(self, number):
462                 if self.service:
463                         seekable = self.getSeek()
464                         if seekable is not None:
465                                 print "seek to chapter %d" % number
466                                 seekable.seekChapter(number)
467
468 #       MENU ACTIONS
469         def keyRight(self):
470                 if self.service:
471                         self.service.keys().keyPressed(iServiceKeys.keyRight)
472
473         def keyLeft(self):
474                 if self.service:
475                         self.service.keys().keyPressed(iServiceKeys.keyLeft)
476
477         def keyUp(self):
478                 if self.service:
479                         self.service.keys().keyPressed(iServiceKeys.keyUp)
480
481         def keyDown(self):
482                 if self.service:
483                         self.service.keys().keyPressed(iServiceKeys.keyDown)
484
485         def keyOk(self):
486                 if self.service:
487                         if not self.in_menu:
488                                 self.toggleInfo()
489                         self.service.keys().keyPressed(iServiceKeys.keyOk)
490
491         def keyCancel(self):
492                 self.askLeavePlayer()
493
494         def showFileBrowser(self):
495                 self.session.openWithCallback(self.FileBrowserClosed, FileBrowser)
496
497         def FileBrowserClosed(self, val):
498                 curref = self.session.nav.getCurrentlyPlayingServiceReference()
499                 print "FileBrowserClosed", val
500                 if val is None:
501                         self.askLeavePlayer()
502                 else:
503                         newref = eServiceReference(4369, 0, val)
504                         print "play", newref.toString()
505                         if curref is None or curref != newref:
506                                 self.session.nav.playService(newref)
507                                 self.service = self.session.nav.getCurrentService()
508                                 print "self.service", self.service
509                                 print "cur_dlg", self.session.current_dialog
510                                 self.dvdScreen.show()
511                                 self.service.subtitle().enableSubtitles(self.dvdScreen.instance, None)
512
513         def exitCB(self, answer):
514                 if answer is not None:
515                         if answer[1] == "exit":
516                                 if self.service:
517                                         self.dvdScreen.hide()
518                                         self.service.subtitle().disableSubtitles(self.session.current_dialog.instance)
519                                         self.service = None
520                                 self.close()
521                         if answer[1] == "browser":
522                                 #TODO check here if a paused dvd playback is already running... then re-start it...
523                                 #else
524                                 self.showFileBrowser()
525                         else:
526                                 pass
527
528         def __onClose(self):
529                 self.restore_infobar_seek_config()
530                 self.session.nav.playService(self.oldService)
531
532         def showAfterCuesheetOperation(self):
533                 if not self.in_menu:
534                         self.show()
535
536         def createSummary(self):
537                 print "DVDCreateSummary"
538                 return DVDSummary
539
540 def main(session, **kwargs):
541         session.open(DVDPlayer)
542
543 def menu(menuid, **kwargs):
544         if menuid == "mainmenu":
545                 return [(_("DVD Player"), main, "dvd_player", 46)]
546         return []
547
548 from Plugins.Plugin import PluginDescriptor
549 def Plugins(**kwargs):
550         return [PluginDescriptor(name = "DVDPlayer", description = "Play DVDs", where = PluginDescriptor.WHERE_MENU, fnc = menu)]