1 from os import path as os_path, remove as os_remove, listdir as os_listdir
2 from time import strftime
3 from enigma import eTimer, eServiceCenter, iServiceInformation
4 from Screens.Screen import Screen
5 from Screens.MessageBox import MessageBox
6 from Screens.InputBox import InputBox
7 from Components.ActionMap import NumberActionMap, HelpableActionMap
8 from Components.Label import Label
9 from Components.Pixmap import Pixmap
10 from Components.Label import Label
11 from Components.FileList import FileList
12 from Components.MediaPlayer import PlayList
13 from Tools.Directories import resolveFilename, SCOPE_CONFIG, SCOPE_PLAYLIST, SCOPE_SKIN_IMAGE
14 from Components.ServicePosition import ServicePositionGauge
15 from Components.ServiceEventTracker import InfoBarBase
16 from Components.Playlist import PlaylistIOInternal, PlaylistIOM3U, PlaylistIOPLS
17 from Screens.InfoBarGenerics import InfoBarSeek, InfoBarAudioSelection, InfoBarCueSheetSupport, InfoBarNotifications
18 from ServiceReference import ServiceReference
19 from Screens.ChoiceBox import ChoiceBox
20 from Screens.HelpMenu import HelpableScreen
23 class MyPlayList(PlayList):
25 PlayList.__init__(self)
27 def PlayListShuffle(self):
28 random.shuffle(self.list)
29 self.l.setList(self.list)
31 self.oldCurrPlaying = -1
33 class MediaPixmap(Pixmap):
34 def applySkin(self, desktop, screen):
35 self.default_pixmap = None
36 if self.skinAttributes is not None:
37 for (attrib, value) in self.skinAttributes:
38 if attrib == "pixmap":
39 self.default_pixmap = value
41 if self.default_pixmap is None:
42 self.default_pixmap = resolveFilename(SCOPE_SKIN_IMAGE, "skin_default/no_coverArt.png")
43 return Pixmap.applySkin(self, desktop, screen)
45 class MediaPlayer(Screen, InfoBarBase, InfoBarSeek, InfoBarAudioSelection, InfoBarCueSheetSupport, InfoBarNotifications, HelpableScreen):
47 ENABLE_RESUME_SUPPORT = True
49 def __init__(self, session, args = None):
50 Screen.__init__(self, session)
51 InfoBarAudioSelection.__init__(self)
52 InfoBarCueSheetSupport.__init__(self, actionmap = "MediaPlayerCueSheetActions")
53 InfoBarNotifications.__init__(self)
54 InfoBarBase.__init__(self)
55 HelpableScreen.__init__(self)
57 self.oldService = self.session.nav.getCurrentlyPlayingServiceReference()
58 self.session.nav.stopService()
60 self.playlistparsers = {}
61 self.addPlaylistParser(PlaylistIOM3U, "m3u")
62 self.addPlaylistParser(PlaylistIOPLS, "pls")
63 self.addPlaylistParser(PlaylistIOInternal, "e2pls")
65 # 'None' is magic to start at the list of mountpoints
66 self.filelist = FileList(None, matchingPattern = "(?i)^.*\.(mp3|ogg|ts|wav|wave|m3u|pls|e2pls|mpg|vob)", useServiceRef = True, additionalExtensions = "4098:m3u 4098:e2pls 4098:pls")
67 self["filelist"] = self.filelist
69 self.playlist = MyPlayList()
70 #self.playlist = PlayList()
71 self.is_closing = False
73 self["playlist"] = self.playlist
75 self["PositionGauge"] = ServicePositionGauge(self.session.nav)
77 self["currenttext"] = Label("")
79 self["artisttext"] = Label(_("Artist:"))
80 self["artist"] = Label("")
81 self["titletext"] = Label(_("Title:"))
82 self["title"] = Label("")
83 self["albumtext"] = Label(_("Album:"))
84 self["album"] = Label("")
85 self["yeartext"] = Label(_("Year:"))
86 self["year"] = Label("")
87 self["genretext"] = Label(_("Genre:"))
88 self["genre"] = Label("")
89 self["coverArt"] = MediaPixmap()
91 self.seek_target = None
93 class MoviePlayerActionMap(NumberActionMap):
94 def __init__(self, player, contexts = [ ], actions = { }, prio=0):
95 NumberActionMap.__init__(self, contexts, actions, prio)
98 def action(self, contexts, action):
100 return NumberActionMap.action(self, contexts, action)
103 self["OkCancelActions"] = HelpableActionMap(self, "OkCancelActions",
105 "ok": (self.ok, _("add file to playlist")),
106 "cancel": (self.exit, _("exit mediaplayer")),
109 self["MediaPlayerActions"] = HelpableActionMap(self, "MediaPlayerActions",
111 "play": (self.xplayEntry, _("play entry")),
112 "pause": (self.pauseEntry, _("pause")),
113 "stop": (self.stopEntry, _("stop entry")),
114 "previous": (self.previousMarkOrEntry, _("play from previous mark or playlist entry")),
115 "next": (self.nextMarkOrEntry, _("play from next mark or playlist entry")),
116 "menu": (self.showMenu, _("menu")),
117 "skipListbegin": (self.skip_listbegin, _("jump to listbegin")),
118 "skipListend": (self.skip_listend, _("jump to listend")),
119 "prevBouquet": (self.switchToPlayList, _("switch to playlist")),
120 "nextBouquet": (self.switchToFileList, _("switch to filelist")),
121 "delete": (self.deletePlaylistEntry, _("delete playlist entry")),
122 "shift_stop": (self.clear_playlist, _("clear playlist")),
123 "shift_record": (self.playlist.PlayListShuffle, _("shuffle playlist")),
126 self["InfobarEPGActions"] = HelpableActionMap(self, "InfobarEPGActions",
128 "showEventInfo": (self.showEventInformation, _("show event details")),
131 self["actions"] = MoviePlayerActionMap(self, ["DirectionActions"],
133 "right": self.rightDown,
134 "rightRepeated": self.doNothing,
135 "rightUp": self.rightUp,
136 "left": self.leftDown,
137 "leftRepeated": self.doNothing,
138 "leftUp": self.leftUp,
141 "upRepeated": self.up,
142 "upUp": self.doNothing,
144 "downRepeated": self.down,
145 "downUp": self.doNothing,
148 InfoBarSeek.__init__(self, actionmap = "MediaPlayerSeekActions")
150 self.onClose.append(self.delMPTimer)
151 self.onClose.append(self.__onClose)
153 self.righttimer = False
154 self.rightKeyTimer = eTimer()
155 self.rightKeyTimer.callback.append(self.rightTimerFire)
157 self.lefttimer = False
158 self.leftKeyTimer = eTimer()
159 self.leftKeyTimer.callback.append(self.leftTimerFire)
161 self.currList = "filelist"
163 self.coverArtFileName = ""
164 self.isAudioCD = False
166 self.playlistIOInternal = PlaylistIOInternal()
167 list = self.playlistIOInternal.open(resolveFilename(SCOPE_CONFIG, "playlist.e2pls"))
170 self.playlist.addFile(x.ref)
171 self.playlist.updateList()
176 def createSummary(self):
177 return MediaPlayerLCDScreen
180 self.session.openWithCallback(self.exitCB, MessageBox, _("Do you really want to exit?"), timeout=5)
182 def exitCB(self, answer):
184 self.playlistIOInternal.clear()
185 for x in self.playlist.list:
186 self.playlistIOInternal.addService(ServiceReference(x[0]))
187 if not self.isAudioCD:
188 self.playlistIOInternal.save(resolveFilename(SCOPE_CONFIG, "playlist.e2pls"))
191 def checkSkipShowHideLock(self):
192 self.updatedSeekState()
194 def doEofInternal(self, playing):
201 self.session.nav.playService(self.oldService)
203 def delMPTimer(self):
204 del self.rightKeyTimer
205 del self.leftKeyTimer
207 def readTitleInformation(self):
208 currPlay = self.session.nav.getCurrentService()
209 if currPlay is not None:
210 stitle = currPlay.info().getInfoString(iServiceInformation.sTitle)
212 stitle = currPlay.info().getName().split('/')[-1]
214 self.updateMusicInformation( artist = currPlay.info().getInfoString(iServiceInformation.sArtist),
216 album = currPlay.info().getInfoString(iServiceInformation.sAlbum),
217 genre = currPlay.info().getInfoString(iServiceInformation.sGenre),
220 self.updateMusicInformation()
222 def updateMusicInformation(self, artist = "", title = "", album = "", year = "", genre = "", clear = False):
223 self.updateSingleMusicInformation("artist", artist, clear)
224 self.updateSingleMusicInformation("title", title, clear)
225 self.updateSingleMusicInformation("album", album, clear)
226 self.updateSingleMusicInformation("year", year, clear)
227 self.updateSingleMusicInformation("genre", genre, clear)
229 def updateSingleMusicInformation(self, name, info, clear):
230 if info != "" or clear:
231 if self[name].getText() != info:
232 self[name].setText(info)
234 def updateCoverArtPixmap(self, path):
235 while not path.endswith("/"):
237 pngname = path + "folder.png"
239 if not os_path.exists(pngname):
240 pngname = self["coverArt"].default_pixmap
241 if self.coverArtFileName != pngname:
242 self.coverArtFileName = pngname
243 self["coverArt"].instance.setPixmapFromFile(self.coverArtFileName)
246 self.lefttimer = True
247 self.leftKeyTimer.start(1000)
250 self.righttimer = True
251 self.rightKeyTimer.start(1000)
255 self.leftKeyTimer.stop()
256 self.lefttimer = False
257 self[self.currList].pageUp()
258 self.updateCurrentInfo()
262 self.rightKeyTimer.stop()
263 self.righttimer = False
264 self[self.currList].pageDown()
265 self.updateCurrentInfo()
267 def leftTimerFire(self):
268 self.leftKeyTimer.stop()
269 self.lefttimer = False
270 self.switchToFileList()
272 def rightTimerFire(self):
273 self.rightKeyTimer.stop()
274 self.righttimer = False
275 self.switchToPlayList()
277 def switchToFileList(self):
278 self.currList = "filelist"
279 self.filelist.selectionEnabled(1)
280 self.playlist.selectionEnabled(0)
281 self.updateCurrentInfo()
283 def switchToPlayList(self):
284 if len(self.playlist) != 0:
285 self.currList = "playlist"
286 self.filelist.selectionEnabled(0)
287 self.playlist.selectionEnabled(1)
288 self.updateCurrentInfo()
291 self[self.currList].up()
292 self.updateCurrentInfo()
295 self[self.currList].down()
296 self.updateCurrentInfo()
298 def showAfterSeek(self):
301 def showAfterCuesheetOperation(self):
304 def hideAfterResume(self):
307 # FIXME: maybe this code can be optimized
308 def updateCurrentInfo(self):
310 if self.currList == "filelist":
311 idx = self.filelist.getSelectionIndex()
312 r = self.filelist.list[idx]
319 self.summaries.setText(text,1)
322 if idx < len(self.filelist.list):
323 r = self.filelist.list[idx]
327 self.summaries.setText(text,3)
329 self.summaries.setText(" ",3)
332 if idx < len(self.filelist.list):
333 r = self.filelist.list[idx]
337 self.summaries.setText(text,4)
339 self.summaries.setText(" ",4)
342 if not self.filelist.canDescent():
343 r = self.filelist.getServiceRef()
347 self["currenttext"].setText(os_path.basename(text))
349 if self.currList == "playlist":
350 t = self.playlist.getSelection()
353 #display current selected entry on LCD
355 text = text.split('/')[-1]
356 self.summaries.setText(text,1)
357 self["currenttext"].setText(text)
358 idx = self.playlist.getSelectionIndex()
360 if idx < len(self.playlist):
361 currref = self.playlist.getServiceRefList()[idx]
362 text = currref.getPath()
363 text = text.split('/')[-1]
364 self.summaries.setText(text,3)
366 self.summaries.setText(" ",3)
369 if idx < len(self.playlist):
370 currref = self.playlist.getServiceRefList()[idx]
371 text = currref.getPath()
372 text = text.split('/')[-1]
373 self.summaries.setText(text,4)
375 self.summaries.setText(" ",4)
378 if self.currList == "filelist":
379 if self.filelist.canDescent():
380 self.filelist.descent()
381 self.updateCurrentInfo()
385 if self.currList == "playlist":
386 selection = self["playlist"].getSelection()
387 self.changeEntry(self.playlist.getSelectionIndex())
391 if self.currList == "filelist":
392 if self.filelist.canDescent():
393 menu.append((_("add directory to playlist"), "copydir"))
395 menu.append((_("add files to playlist"), "copyfiles"))
396 menu.append((_("switch to playlist"), "playlist"))
398 menu.append((_("switch to filelist"), "filelist"))
400 menu.append((_("shuffle playlist"), "shuffle"))
402 menu.append((_("delete"), "delete"))
403 menu.append((_("clear playlist"), "clear"))
404 menu.append((_("hide player"), "hide"));
405 menu.append((_("save playlist"), "saveplaylist"));
406 menu.append((_("load playlist"), "loadplaylist"));
407 menu.append((_("delete saved playlist"), "deleteplaylist"));
408 self.session.openWithCallback(self.menuCallback, ChoiceBox, title="", list=menu)
410 def menuCallback(self, choice):
414 if choice[1] == "copydir":
415 self.copyDirectory(self.filelist.getSelection()[0])
416 elif choice[1] == "copyfiles":
418 self.playlist.clear()
419 self.copyDirectory(os_path.dirname(self.filelist.getSelection()[0].getPath()) + "/", recursive = False)
420 self.playServiceRefEntry(self.filelist.getServiceRef())
421 elif choice[1] == "playlist":
422 self.switchToPlayList()
423 elif choice[1] == "filelist":
424 self.switchToFileList()
425 elif choice[1] == "delete":
426 if self.playlist.getSelectionIndex() == self.playlist.getCurrentIndex():
429 elif choice[1] == "clear":
431 self.playlist.clear()
432 self.switchToFileList()
433 elif choice[1] == "hide":
435 elif choice[1] == "saveplaylist":
437 elif choice[1] == "loadplaylist":
439 elif choice[1] == "deleteplaylist":
440 self.delete_saved_playlist()
441 elif choice[1] == "shuffle":
442 self.playlist.PlayListShuffle()
445 def showEventInformation(self):
446 from Screens.EventView import EventViewSimple
447 from ServiceReference import ServiceReference
448 evt = self[self.currList].getCurrentEvent()
450 self.session.open(EventViewSimple, evt, ServiceReference(self.getCurrent()))
452 # also works on filelist (?)
453 def getCurrent(self):
454 return self["playlist"].getCurrent()
456 def deletePlaylistEntry(self):
457 if self.currList == "playlist":
458 if self.playlist.getSelectionIndex() == self.playlist.getCurrentIndex():
462 def skip_listbegin(self):
463 if self.currList == "filelist":
464 self.filelist.moveToIndex(0)
466 self.playlist.moveToIndex(0)
467 self.updateCurrentInfo()
469 def skip_listend(self):
470 if self.currList == "filelist":
471 idx = len(self.filelist.list)
472 self.filelist.moveToIndex(idx - 1)
474 self.playlist.moveToIndex(len(self.playlist)-1)
475 self.updateCurrentInfo()
477 def save_playlist(self):
478 self.session.openWithCallback(self.save_playlist2,InputBox, title=_("Please enter filename (empty = use current date)"),windowTitle = _("Save Playlist"))
480 def save_playlist2(self, name):
484 name = strftime("%y%m%d_%H%M%S")
486 self.playlistIOInternal.clear()
487 for x in self.playlist.list:
488 self.playlistIOInternal.addService(ServiceReference(x[0]))
489 self.playlistIOInternal.save(resolveFilename(SCOPE_PLAYLIST) + name)
491 def load_playlist(self):
493 playlistdir = resolveFilename(SCOPE_PLAYLIST)
495 for i in os_listdir(playlistdir):
496 listpath.append((i,playlistdir + i))
498 print "Error while scanning subdirs ",e
499 self.session.openWithCallback(self.PlaylistSelected, ChoiceBox, title=_("Please select a playlist..."), list = listpath)
501 def PlaylistSelected(self,path):
503 self.clear_playlist()
504 self.playlistIOInternal = PlaylistIOInternal()
505 list = self.playlistIOInternal.open(path[1])
508 self.playlist.addFile(x.ref)
509 self.playlist.updateList()
511 def delete_saved_playlist(self):
513 playlistdir = resolveFilename(SCOPE_PLAYLIST)
515 for i in os_listdir(playlistdir):
516 listpath.append((i,playlistdir + i))
518 print "Error while scanning subdirs ",e
519 self.session.openWithCallback(self.DeletePlaylistSelected, ChoiceBox, title=_("Please select a playlist to delete..."), list = listpath)
521 def DeletePlaylistSelected(self,path):
523 self.delname = path[1]
524 self.session.openWithCallback(self.deleteConfirmed, MessageBox, _("Do you really want to delete %s?") % (path[1]))
526 def deleteConfirmed(self, confirmed):
528 os_remove(self.delname)
530 def clear_playlist(self):
532 self.playlist.clear()
533 self.switchToFileList()
535 def copyDirectory(self, directory, recursive = True):
536 print "copyDirectory", directory
537 filelist = FileList(directory, useServiceRef = True, isTop = True)
539 for x in filelist.getFileList():
540 if x[0][1] == True: #isDir
542 self.copyDirectory(x[0][0])
544 self.playlist.addFile(x[0][0])
545 self.playlist.updateList()
548 if self.filelist.getServiceRef().type == 4098: # playlist
549 ServiceRef = self.filelist.getServiceRef()
550 extension = ServiceRef.getPath()[ServiceRef.getPath().rfind('.') + 1:]
551 print "extension:", extension
552 if self.playlistparsers.has_key(extension):
553 playlist = self.playlistparsers[extension]()
554 list = playlist.open(ServiceRef.getPath())
556 self.playlist.addFile(x.ref)
558 self.playlist.addFile(self.filelist.getServiceRef())
559 self.playlist.updateList()
560 if len(self.playlist) == 1:
563 def addPlaylistParser(self, parser, extension):
564 self.playlistparsers[extension] = parser
567 next = self.playlist.getCurrentIndex() + 1
568 if next < len(self.playlist):
569 self.changeEntry(next)
571 def nextMarkOrEntry(self):
572 if not self.jumpPreviousNextMark(lambda x: x):
573 next = self.playlist.getCurrentIndex() + 1
574 if next < len(self.playlist):
575 self.changeEntry(next)
579 def previousMarkOrEntry(self):
580 if not self.jumpPreviousNextMark(lambda x: -x-5*90000, start=True):
581 next = self.playlist.getCurrentIndex() - 1
583 self.changeEntry(next)
585 def deleteEntry(self):
586 self.playlist.deleteFile(self.playlist.getSelectionIndex())
587 self.playlist.updateList()
588 if len(self.playlist) == 0:
589 self.switchToFileList()
591 def changeEntry(self, index):
592 self.playlist.setCurrentPlaying(index)
595 def playServiceRefEntry(self, serviceref):
596 serviceRefList = self.playlist.getServiceRefList()
597 for count in range(len(serviceRefList)):
598 if serviceRefList[count] == serviceref:
599 self.changeEntry(count)
602 def xplayEntry(self):
603 if self.currList == "playlist":
607 self.playlist.clear()
608 sel = self.filelist.getSelection()
610 if sel[1]: # can descent
611 # add directory to playlist
612 self.copyDirectory(sel[0])
614 # add files to playlist
615 self.copyDirectory(os_path.dirname(sel[0].getPath()) + "/", recursive = False)
616 if len(self.playlist) > 0:
620 if len(self.playlist.getServiceRefList()):
621 needsInfoUpdate = False
622 currref = self.playlist.getServiceRefList()[self.playlist.getCurrentIndex()]
623 if self.session.nav.getCurrentlyPlayingServiceReference() is None or currref != self.session.nav.getCurrentlyPlayingServiceReference():
624 self.session.nav.playService(self.playlist.getServiceRefList()[self.playlist.getCurrentIndex()])
625 info = eServiceCenter.getInstance().info(currref)
626 description = info and info.getInfoString(currref, iServiceInformation.sDescription) or ""
627 self["title"].setText(description)
628 # display just playing musik on LCD
629 idx = self.playlist.getCurrentIndex()
630 currref = self.playlist.getServiceRefList()[idx]
631 text = currref.getPath()
632 text = text.split('/')[-1]
634 ext = text[-3:].lower()
636 # FIXME: the information if the service contains video (and we should hide our window) should com from the service instead
637 if ext not in ["mp3", "wav", "ogg"]:
640 needsInfoUpdate = True
641 self.summaries.setText(text,1)
643 # get the next two entries
645 if idx < len(self.playlist):
646 currref = self.playlist.getServiceRefList()[idx]
647 text = currref.getPath()
648 text = text.split('/')[-1]
649 self.summaries.setText(text,3)
651 self.summaries.setText(" ",3)
654 if idx < len(self.playlist):
655 currref = self.playlist.getServiceRefList()[idx]
656 text = currref.getPath()
657 text = text.split('/')[-1]
658 self.summaries.setText(text,4)
660 self.summaries.setText(" ",4)
662 idx = self.playlist.getCurrentIndex()
663 currref = self.playlist.getServiceRefList()[idx]
664 text = currref.getPath()
665 ext = text[-3:].lower()
666 if ext not in ["mp3", "wav", "ogg"]:
669 needsInfoUpdate = True
671 self.unPauseService()
672 if needsInfoUpdate == True:
673 self.updateCoverArtPixmap(currref.getPath())
675 pngname = self["coverArt"].default_pixmap
676 self.coverArtFileName = pngname
677 self["coverArt"].instance.setPixmapFromFile(self.coverArtFileName)
678 self.readTitleInformation()
680 def updatedSeekState(self):
681 if self.seekstate == self.SEEK_STATE_PAUSE:
682 self.playlist.pauseFile()
683 elif self.seekstate == self.SEEK_STATE_PLAY:
684 self.playlist.playFile()
685 elif self.isStateForward(self.seekstate):
686 self.playlist.forwardFile()
687 elif self.isStateBackward(self.seekstate):
688 self.playlist.rewindFile()
690 def pauseEntry(self):
695 self.playlist.stopFile()
696 self.session.nav.playService(None)
697 self.updateMusicInformation(clear=True)
700 def unPauseService(self):
701 self.setSeekState(self.SEEK_STATE_PLAY)
704 class MediaPlayerLCDScreen(Screen):
706 <screen position="0,0" size="132,64" title="LCD Text">
707 <widget name="text1" position="4,0" size="132,35" font="Regular;16"/>
708 <widget name="text3" position="4,36" size="132,14" font="Regular;10"/>
709 <widget name="text4" position="4,49" size="132,14" font="Regular;10"/>
712 def __init__(self, session, parent):
713 Screen.__init__(self, session)
714 self["text1"] = Label("Mediaplayer")
715 self["text3"] = Label("")
716 self["text4"] = Label("")
718 def setText(self, text, line):
719 print "lcd set text:", text, line
721 if text[-4:] == ".mp3":
724 text = text + textleer*10
726 self["text1"].setText(text)
728 self["text3"].setText(text)
730 self["text4"].setText(text)
732 def main(session, **kwargs):
733 session.open(MediaPlayer)
735 def menu(menuid, **kwargs):
736 if menuid == "mainmenu":
737 return [(_("Media player"), main, "media_player", 45)]
740 def filescan_open(list, session, **kwargs):
741 from enigma import eServiceReference
743 mp = session.open(MediaPlayer)
745 mp.switchToPlayList()
747 ref = eServiceReference(4097, 0, file.path)
748 mp.playlist.addFile(ref)
750 # TODO: rather play first than last file?
751 mp.playServiceRefEntry(ref)
752 mp.playlist.updateList()
754 def audioCD_open(list, session, **kwargs):
755 from enigma import eServiceReference
757 mp = session.open(MediaPlayer)
762 mp.switchToPlayList()
764 ref = eServiceReference(4097, 0, file.path)
765 mp.playlist.addFile(ref)
767 # TODO: rather play first than last file?
768 mp.playServiceRefEntry(ref)
769 mp.playlist.updateList()
772 def filescan(**kwargs):
773 from Components.Scanner import Scanner, ScanPath
775 Scanner(mimetypes = ["video/mpeg"],
778 ScanPath(path = "", with_subdirs = False),
781 description = "View Movies...",
782 openfnc = filescan_open,
784 Scanner(mimetypes = ["audio/mpeg", "audio/x-wav", "application/ogg"],
787 ScanPath(path = "", with_subdirs = False),
790 description = "Play Music...",
791 openfnc = filescan_open,
793 Scanner(mimetypes = ["audio/x-cda", "audio/x-wav"],
796 ScanPath(path = "", with_subdirs = False),
799 description = "Play Audio-CD...",
800 openfnc = audioCD_open,
804 from Plugins.Plugin import PluginDescriptor
805 def Plugins(**kwargs):
807 PluginDescriptor(name = "MediaPlayer", description = "Play back media files", where = PluginDescriptor.WHERE_MENU, fnc = menu),
808 PluginDescriptor(name = "MediaPlayer", where = PluginDescriptor.WHERE_FILESCAN, fnc = filescan)