1 from Plugins.Plugin import PluginDescriptor
3 import time, os, socket, thread, socket, copy
4 from socket import gaierror, error
5 from os import path as os_path, remove as os_remove
8 import gdata.youtube.service
9 from gdata.service import BadAuthentication
11 from twisted.web import client
12 from twisted.internet import reactor
14 from urlparse import parse_qs
15 from urllib import quote, unquote_plus, unquote
16 from urllib2 import Request, URLError, urlopen as urlopen2
17 from httplib import HTTPConnection, CannotSendRequest, BadStatusLine, HTTPException
19 from Components.Button import Button
20 from Components.Label import Label
21 from Components.Pixmap import Pixmap
22 from Components.Language import language
23 from Components.Sources.List import List
24 from Components.ConfigList import ConfigListScreen
25 from Components.Sources.StaticText import StaticText
26 from Components.ActionMap import NumberActionMap, ActionMap
27 from Components.ServiceEventTracker import ServiceEventTracker
28 from Components.config import config, ConfigSelection, getConfigListEntry, ConfigSlider
30 from Screens.Screen import Screen
31 from Screens.ChoiceBox import ChoiceBox
32 from Screens.MessageBox import MessageBox
33 from Screens.DefaultWizard import DefaultWizard
34 from Screens.InfoBarGenerics import InfoBarNotifications
36 from enigma import eTimer, eServiceReference, iPlayableService, fbClass, eRCInput, eConsoleAppContainer
38 HTTPConnection.debuglevel = 1
41 print "prepared cmd:", cmd
45 def change_galpha(set_const, set_value):
46 op = "/proc/stb/fb/alpha_op"
47 val = "/proc/stb/fb/alpha_value"
49 if os.path.exists(op) and set_const and alpha_value < 255:
50 excute_cmd("echo \"const\" > %s" % (op))
52 excute_cmd("echo \"copypsrc\" > %s" % (op))
54 if os.path.exists(val) and set_value:
55 excute_cmd("echo \"%s\" > %s" % (str(hex(alpha_value)), val))
57 def enable_rc_mouse(mode): #mode=[0|1]|[False|True]
58 mouse_cond = "/proc/stb/fp/mouse"
59 if os.path.exists(mouse_cond):
60 excute_cmd("echo %d > %s" % (mode, mouse_cond))
62 def is_process_running(pname):
63 if pname is None or len(pname) == 0:
66 cmd = "/bin/ps -ef | grep %s | grep -v grep | awk \'{print $5}\'"%(pname)
67 for line in os.popen(cmd).readlines():
72 def wb_lock(alpha_on=True):
76 change_galpha(set_const=False, set_value=False)
77 fbClass.getInstance().unlock()
79 def wb_unlock(alpha_on=True):
82 change_galpha(set_const=True, set_value=False)
83 fbClass.getInstance().lock()
90 class VuPlayer(Screen, InfoBarNotifications):
92 <screen name="VuPlayer" flags="wfNoBorder" position="center,620" size="455,53" title="VuPlayer" backgroundColor="transparent">
93 <ePixmap pixmap="Vu_HD/mp_wb_background.png" position="0,0" zPosition="-1" size="455,53" />
94 <ePixmap pixmap="Vu_HD/icons/mp_wb_buttons.png" position="40,23" size="30,13" alphatest="on" />
96 <widget source="session.CurrentService" render="PositionGauge" position="80,25" size="220,10" zPosition="2" pointer="skin_default/position_pointer.png:540,0" transparent="1" foregroundColor="#20224f">
97 <convert type="ServicePosition">Gauge</convert>
100 <widget source="session.CurrentService" render="Label" position="310,20" size="50,20" font="Regular;18" halign="center" valign="center" backgroundColor="#4e5a74" transparent="1" >
101 <convert type="ServicePosition">Position</convert>
103 <widget name="sidebar" position="362,20" size="10,20" font="Regular;18" halign="center" valign="center" backgroundColor="#4e5a74" transparent="1" />
104 <widget source="session.CurrentService" render="Label" position="374,20" size="50,20" font="Regular;18" halign="center" valign="center" backgroundColor="#4e5a74" transparent="1" >
105 <convert type="ServicePosition">Length</convert>
113 def __init__(self, session, service, lastservice):
114 Screen.__init__(self, session)
115 InfoBarNotifications.__init__(self)
117 self.session = session
118 self.service = service
119 self.lastservice = lastservice
120 self["actions"] = ActionMap(["OkCancelActions", "InfobarSeekActions", "MediaPlayerActions", "MovieSelectionActions"],
122 "ok": self.doInfoAction,
123 "cancel": self.doExit,
125 "playpauseService": self.playpauseService,
127 self["sidebar"] = Label(_("/"))
129 self.__event_tracker = ServiceEventTracker(screen = self, eventmap =
131 iPlayableService.evSeekableStatusChanged: self.__seekableStatusChanged,
132 iPlayableService.evStart: self.__serviceStarted,
133 iPlayableService.evEOF: self.__evEOF,
136 self.hidetimer = eTimer()
137 self.hidetimer.timeout.get().append(self.doInfoAction)
139 self.state = self.PLAYER_PLAYING
140 self.lastseekstate = self.PLAYER_PLAYING
141 self.__seekableStatusChanged()
143 self.onClose.append(self.__onClose)
147 self.session.nav.stopService()
149 def __seekableStatusChanged(self):
150 service = self.session.nav.getCurrentService()
151 if service is not None:
152 seek = service.seek()
153 if seek is None or not seek.isCurrentlySeekable():
154 self.setSeekState(self.PLAYER_PLAYING)
156 def __serviceStarted(self):
157 self.state = self.PLAYER_PLAYING
158 self.__seekableStatusChanged()
163 def __setHideTimer(self):
164 self.hidetimer.start(5000)
167 list = ((_("Yes"), "y"), (_("No, but play video again"), "n"),)
168 self.session.openWithCallback(self.cbDoExit, ChoiceBox, title=_("Stop playing this movie?"), list = list)
170 def cbDoExit(self, answer):
171 answer = answer and answer[1]
176 if self.state != self.PLAYER_IDLE:
177 self.session.nav.stopService()
178 self.state = self.PLAYER_IDLE
181 def setSeekState(self, wantstate):
182 service = self.session.nav.getCurrentService()
184 print "No Service found"
187 pauseable = service.pause()
188 if pauseable is not None:
189 if wantstate == self.PLAYER_PAUSED:
191 self.state = self.PLAYER_PAUSED
193 self.hidetimer.stop()
195 elif wantstate == self.PLAYER_PLAYING:
197 self.state = self.PLAYER_PLAYING
199 self.__setHideTimer()
201 self.state = self.PLAYER_PLAYING
203 def doInfoAction(self):
206 self.hidetimer.stop()
209 if self.state == self.PLAYER_PLAYING:
210 self.__setHideTimer()
213 if self.state == self.PLAYER_PAUSED:
215 self.__setHideTimer()
216 self.state = self.PLAYER_PLAYING
217 self.session.nav.playService(self.service)
219 self.__setHideTimer()
221 def playpauseService(self):
222 if self.state == self.PLAYER_PLAYING:
223 self.setSeekState(self.PLAYER_PAUSED)
224 elif self.state == self.PLAYER_PAUSED:
225 self.setSeekState(self.PLAYER_PLAYING)
227 VIDEO_FMT_PRIORITY_MAP = {
228 '38' : 1, #MP4 Original (HD)
229 '37' : 2, #MP4 1080p (HD)
230 '22' : 3, #MP4 720p (HD)
236 'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100627 Firefox/3.6.6',
237 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
238 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
239 'Accept-Language': 'en-us,en;q=0.5',
242 class VuPlayerLauncher:
243 def getVideoUrl(self, video_id):
246 if video_id is None or video_id == "":
249 # Getting video webpage
250 watch_url = 'http://www.youtube.com/watch?v=%s&gl=US&hl=en' % video_id
251 watchrequest = Request(watch_url, None, std_headers)
253 #print "trying to find out if a HD Stream is available",watch_url
254 watchvideopage = urlopen2(watchrequest).read()
255 except (URLError, HTTPException, socket.error), err:
256 print "Error: Unable to retrieve watchpage - Error code: ", str(err)
260 for el in ['&el=embedded', '&el=detailpage', '&el=vevo', '']:
261 info_url = ('http://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en' % (video_id, el))
262 request = Request(info_url, None, std_headers)
264 infopage = urlopen2(request).read()
265 videoinfo = parse_qs(infopage)
266 if ('url_encoded_fmt_stream_map' or 'fmt_url_map') in videoinfo:
268 except (URLError, HTTPException, socket.error), err:
269 print "Error: unable to download video infopage",str(err)
272 if ('url_encoded_fmt_stream_map' or 'fmt_url_map') not in videoinfo:
273 if 'reason' not in videoinfo:
274 print 'Error: unable to extract "fmt_url_map" or "url_encoded_fmt_stream_map" parameter for unknown reason'
276 reason = unquote_plus(videoinfo['reason'][0])
277 print 'Error: YouTube said: %s' % reason.decode('utf-8')
282 if videoinfo.has_key('url_encoded_fmt_stream_map'):
283 tmp_fmtUrlDATA = videoinfo['url_encoded_fmt_stream_map'][0].split(',url=')
285 tmp_fmtUrlDATA = videoinfo['fmt_url_map'][0].split(',')
286 for fmtstring in tmp_fmtUrlDATA:
287 if videoinfo.has_key('url_encoded_fmt_stream_map'):
288 (fmturl, fmtid) = fmtstring.split('&itag=')
289 if fmturl.find("url=") !=-1:
290 fmturl = fmturl.replace("url=","")
292 (fmtid,fmturl) = fmtstring.split('|')
293 if VIDEO_FMT_PRIORITY_MAP.has_key(fmtid):
294 video_fmt_map[VIDEO_FMT_PRIORITY_MAP[fmtid]] = { 'fmtid': fmtid, 'fmturl': unquote_plus(fmturl) }
295 fmt_infomap[int(fmtid)] = unquote_plus(fmturl)
296 print "got",sorted(fmt_infomap.iterkeys())
297 if video_fmt_map and len(video_fmt_map):
298 video_url = video_fmt_map[sorted(video_fmt_map.iterkeys())[0]]['fmturl'].split(';')[0]
299 #print "found best available video format:",video_fmt_map[sorted(video_fmt_map.iterkeys())[0]]['fmtid']
300 #print "found best available video url:",video_url
303 def run(self, tubeid, session, service):
305 myurl = self.getVideoUrl(tubeid)
306 print "Playing URL", myurl
308 session.open(MessageBox, _("Sorry, video is not available!"), MessageBox.TYPE_INFO)
310 myreference = eServiceReference(4097, 0, myurl)
311 session.open(VuPlayer, myreference, service)
312 except Exception, msg:
314 print "Error >>", msg
316 class VuPlayerService:
317 def __init__(self, session):
319 self.socket_timeout = 0
320 self.max_buffer_size = 1024
321 self.uds_file = "/tmp/vuplus.tmp"
322 self.session = session
324 os.remove(self.uds_file)
328 def start(self, timeout = 1):
329 self.socket_timeout = timeout
330 thread.start_new_thread(self.run, (True,))
338 def run(self, e = True):
341 print "VuPlayerService start!!"
343 self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
344 self.sock.settimeout(self.socket_timeout)
345 self.sock.bind(self.uds_file)
349 conn, addr = self.sock.accept()
350 self.parseHandle(conn, addr)
351 except socket.timeout:
352 #print "[socket timeout]"
354 print "VuPlayerService stop!!"
356 def parseHandle(self, conn, addr):
357 # [http://www.youtube.com/watch?v=BpThu778qB4&feature=related]
358 data = conn.recv(self.max_buffer_size)
359 print "[%s]" % (data)
360 enable_rc_mouse(False)
361 if data.startswith("http://www.youtube.com"):
362 print "youtube start!!"
363 tmp = data.split("?")
364 print tmp # ['http://www.youtube.com/watch', 'v=BpThu778qB4&feature=related']
365 service = self.session.nav.getCurrentlyPlayingServiceReference()
366 if len(tmp) == 2 and tmp[0] == "http://www.youtube.com/watch":
367 tmp = tmp[1].split("&")
368 print tmp # ['v=BpThu778qB4', 'feature=related']
370 tmp = tmp[0].split("=")
371 print tmp # ['v', 'BpThu778qB4']
372 if len(tmp) == 2 and tmp[0] == "v":
374 player = VuPlayerLauncher()
375 player.run(tmp[1], self.session, service)
378 self.session.nav.playService(service)
381 data = "nok$parsing fail"
383 data = "nok$parsing fail"
385 data = "nok$parsing fail"
386 self.sendResponse(conn, data)
387 elif data.startswith("vk://open"):
388 print "virtual keyboard start!!"
389 from Screens.VirtualKeyBoard import VirtualKeyBoard
392 self.session.openWithCallback(self.cbOpenKeyboard, VirtualKeyBoard, title = (_("Enter your input data")), text = "")
394 def cbOpenKeyboard(self, data = None):
395 print "virtual keyboard callback!!"
397 self.sendResponse(self.vk_conn, data)
399 def sendResponse(self, conn, data):
400 if data is None or len(data) == 0:
402 enable_rc_mouse(True)
406 class BrowserLauncher(ConfigListScreen, Screen):
408 <screen name="BrowserLauncher" position="center,center" size="309,498" title="Web Browser">
409 <ePixmap pixmap="Vu_HD/buttons/red.png" position="4,0" size="40,40" alphatest="on" />
410 <ePixmap pixmap="Vu_HD/buttons/green.png" position="100,0" size="40,40" alphatest="on" />
411 <ePixmap pixmap="Vu_HD/buttons/button_off.png" position="200,0" size="40,40" alphatest="on" />
412 <widget source="key_red" render="Label" position="15,0" zPosition="1" size="50,30" font="Regular;20" halign="right" valign="center" transparent="1" />
413 <widget source="key_green" render="Label" position="120,0" zPosition="1" size="50,30" font="Regular;20" halign="right" valign="center" transparent="1" />
414 <widget name="config" position="0,50" size="309,100" scrollbarMode="showOnDemand" />
415 <ePixmap pixmap="Vu_HD/rc_wb_desc.png" position="0,150" size="309,296" alphatest="on" />
416 <widget name="info" position="0,455" size="309,50" font="Regular;18" halign="center" foregroundColor="blue" transparent="1" />
420 def __init__(self, session):
421 Screen.__init__(self, session)
422 self.session = session
424 ConfigListScreen.__init__(self, self.list)
426 self.browser_root = "/usr/bin"
427 self.browser_name = "arora"
428 self.conf_file = "/usr/lib/enigma2/python/Plugins/Extensions/WebBrowser/settings.conf"
429 self["actions"] = ActionMap(["OkCancelActions", "ShortcutActions", "WizardActions", "ColorActions", "SetupActions", ],
430 { "red": self.keyCancel,
432 "cancel": self.keyExit,
434 self.info = Label(_("If you want to quit the Browser,\nPress RED -> EXIT."))
435 self["info"] = self.info
436 self["key_red"] = StaticText(_("Exit"))
437 self["key_green"] = StaticText(_("Start"))
441 self.conf_keyboard = ""
442 self.conf_keymap = ""
444 self.usb_mouse = None
445 self.usb_keyboard = None
447 self.rc_keyboard = None
453 self.vu_service = VuPlayerService(self.session)
454 self.vu_service.start(timeout=5)
456 self.exit_wait_cond = False
457 self.timer_exit_cond = eTimer()
458 self.timer_exit_cond.callback.append(self.resetExitCond)
460 self.test_cond = True
461 self.current_lang_idx = language.getActiveLanguageIndex()
467 change_galpha(set_const=False, set_value=False)
469 self.vu_service.stop()
470 excute_cmd("killall -15 %s"%(self.browser_name))
471 excute_cmd("echo 60 > /proc/sys/vm/swappiness")
472 enable_rc_mouse(False) #rc-mouse off
473 language.activateLanguageIndex(self.current_lang_idx)
474 fbClass.getInstance().unlock()
475 #eRCInput.getInstance().unlock()
479 if self.exit_wait_cond:
481 if is_process_running(self.browser_name) == False:
485 if is_process_running(self.browser_name) == False:
486 ConfigListScreen.keyLeft(self)
488 alpha_value = self.alpha.value
492 if is_process_running(self.browser_name) == False:
493 ConfigListScreen.keyRight(self)
494 alpha_value = self.alpha.value
498 if is_process_running(self.browser_name) == False:
500 self.exit_wait_cond = True
501 self.timer_exit_cond.start(5000)
503 # mouse:keyboard:alpha_value
504 def saveConfig(self):
505 if is_process_running(self.browser_name) == False:
506 command = "echo \"%s:%s:%d:%s\" > %s"%(self.mouse.value, self.keyboard.value, int(self.alpha.value), self.langs.value, self.conf_file)
509 # mouse:keyboard:alpha_value
510 def loadConfig(self):
511 if os.path.exists(self.conf_file) == False:
513 config_list = open(self.conf_file).readline().strip().split(':')
514 if len(config_list) == 3:
515 self.conf_mouse = config_list[0]
516 self.conf_keyboard = config_list[1]
517 self.conf_alpha = config_list[2]
518 elif len(config_list) == 4:
519 self.conf_mouse = config_list[0]
520 self.conf_keyboard = config_list[1]
521 self.conf_alpha = config_list[2]
522 self.conf_keymap = config_list[3]
523 print "load config : ", config_list
525 def resetExitCond(self):
526 self.timer_exit_cond.stop()
527 self.exit_wait_cond = False
529 def makeConfig(self):
531 self.devices_string = ""
532 self.devices = eConsoleAppContainer()
533 self.devices.dataAvail.append(self.callbackDevicesDataAvail)
534 self.devices.appClosed.append(self.callbakcDevicesAppClose)
535 self.devices.execute(_("cat /proc/bus/input/devices"))
537 def callbackDevicesDataAvail(self, ret_data):
538 self.devices_string = self.devices_string + ret_data
540 def callbakcDevicesAppClose(self, retval):
542 self.mouse_list = None
543 self.keyboard_list = None
545 self.makeHandlerList(self.devices_string)
547 if self.conf_mouse == "" or self.getHandlerName(self.conf_mouse) is None:
548 self.conf_mouse = self.mouse_list[0][0]
549 self.mouse = ConfigSelection(default = self.conf_mouse, choices = self.mouse_list)
550 self.list.append(getConfigListEntry(_('Mouse'), self.mouse))
552 if self.conf_keyboard == "" or self.getHandlerName(self.conf_keyboard) is None:
553 self.conf_keyboard = self.keyboard_list[0][0]
554 self.keyboard = ConfigSelection(default = self.conf_keyboard, choices = self.keyboard_list)
555 self.list.append(getConfigListEntry(_('Keyboard'), self.keyboard))
557 if self.conf_alpha == "":
558 self.conf_alpha = "255"
559 self.alpha = ConfigSlider(default = int(self.conf_alpha), increment = 10, limits = (0, 255))
560 self.list.append(getConfigListEntry(_("Alpha Value"), self.alpha))
562 if self.conf_keymap == "":
563 self.conf_keymap = self.getLanguage()
564 self.lang_list = [("en", "English"), ("de", "German")]
565 self.langs = ConfigSelection(default = self.conf_keymap, choices = self.lang_list)
566 self.list.append(getConfigListEntry(_("Language"), self.langs))
568 self["config"].list = self.list
569 self["config"].l.setList(self.list)
571 def getLanguage(self, lang=language.getLanguage()):
572 if self.current_lang_idx == 1:
576 def makeHandlerList(self, data):
582 lines = data.split('\n')
585 if line is not None and len(line) > 0:
595 h_list = line[12:].strip().split()
596 tn = line[12:].strip().find("mouse")
598 event_list.append((h, _(h)))
599 self.name_list.append((h, n))
600 if n[1:].startswith("dream") and self.rc_mouse is None:
601 self.rc_mouse = copy.deepcopy(h)
602 self.rc_keyboard = copy.deepcopy(h)
603 print "detected!! rc"
605 if h.startswith("mouse") and self.usb_mouse is None:
606 self.usb_mouse = copy.deepcopy(h)
607 print "detected!! usb mouse"
609 if tn == -1 and self.usb_keyboard is None:
610 self.usb_keyboard = copy.deepcopy(h)
611 print "detected!! usb keyboard"
612 elif line[0] == 'B' and line[3:].startswith("ABS") and p.startswith("usb-"):
614 if self.usb_mouse is not None:
616 if self.usb_keyboard is not None and h == self.usb_keyboard[0]:
617 self.usb_keyboard = None
618 print "clean!! usb keyboard"
619 self.usb_mouse = copy.deepcopy(h)
620 print "detected!! usb mouse"
622 tmp = copy.deepcopy(event_list)
623 if self.usb_mouse is not None:
624 tmp.insert(0, ("musb", "USB Mouse"))
625 if self.rc_mouse is not None:
626 tmp.insert(0, ("mrc", "Remote Control"))
627 self.mouse_list = tmp
629 tmp = copy.deepcopy(event_list)
630 if self.usb_keyboard is not None:
631 tmp.insert(0, ("kusb", "USB Keyboard"))
632 if self.rc_keyboard is not None:
633 tmp.insert(0, ("krc", "Remote Control"))
634 self.keyboard_list = tmp
635 print "E:", event_list
636 print "M:", self.mouse_list
637 print "K:", self.keyboard_list
639 def startBrowser(self):
640 self.timer_start.stop()
643 excute_cmd("killall -15 %s"%(self.browser_name))
644 excute_cmd("echo 0 > /proc/sys/vm/swappiness")
649 browser_cmd = "%s/%s -qws" % (self.browser_root, self.browser_name)
651 mouse_param = self.mouse.value
652 if self.mouse.value == "mrc":
653 mouse_param = self.rc_mouse
654 elif self.mouse.value == "musb":
655 mouse_param = self.usb_mouse
656 keyboard_param = self.keyboard.value
657 if self.keyboard.value == "krc":
658 keyboard_param = self.rc_keyboard
659 elif self.keyboard.value == "kusb":
660 keyboard_param = self.usb_keyboard
662 if self.getHandlerName(mouse_param)[1:].startswith("dreambox"):
663 enable_rc_mouse(True) #rc-mouse on
664 if str(mouse_param).startswith("event"):
665 mouse_cmd = "export QWS_MOUSE_PROTO=LinuxInput:/dev/input/%s; " % (str(mouse_param))
668 if self.langs.value == "de":
669 keymap_param = ":keymap=/usr/share/keymaps/vuplus/de.qmap"
670 kbd_cmd = "export QWS_KEYBOARD=LinuxInput:/dev/input/%s%s; " % (str(keyboard_param), keymap_param)
672 cmd = "%s%s%s%s" % (extra_cmd, kbd_cmd, mouse_cmd, browser_cmd)
673 print "prepared command : [%s]" % cmd
675 self.launcher = eConsoleAppContainer()
676 self.launcher.appClosed.append(self.callbackLauncherAppClosed)
677 self.launcher.dataAvail.append(self.callbackLauncherDataAvail)
679 fbClass.getInstance().lock()
680 #eRCInput.getInstance().lock()
683 alpha_value = self.alpha.value
684 change_galpha(set_const=True, set_value=True)
686 self.launcher.execute(cmd)
687 print "started browser..."
691 self.info.setText("Starting Webbrowser. Please wait...")
692 if self.lock == False:
693 if self.langs.value == "de":
694 language.activateLanguageIndex(1)
696 language.activateLanguageIndex(0)
697 self.timer_start = eTimer()
698 self.timer_start.callback.append(self.startBrowser)
699 self.timer_start.start(10)
701 def getHandlerName(self, v):
709 v = self.usb_keyboard
710 for l in self.name_list:
715 def callbackLauncherDataAvail(self, ret_data):
717 if ret_data.startswith("--done--"):
721 def callbackLauncherAppClosed(self, retval = 1):
724 def sessionstart(session, **kwargs):
725 enable_rc_mouse(False)
726 change_galpha(set_const=False, set_value=True)
727 excute_cmd("killall -15 arora")
729 def main(session, **kwargs):
730 session.open(BrowserLauncher)
732 def Plugins(**kwargs):
733 return [PluginDescriptor(where = PluginDescriptor.WHERE_SESSIONSTART, needsRestart = False, fnc=sessionstart),
734 PluginDescriptor(name=_("Web Browser"), description="start web browser", where = PluginDescriptor.WHERE_PLUGINMENU, fnc=main)]