Merge branch 'refs/heads/master' of ssh://sreichholf@scm.schwerkraft.elitedvb.net...
[vuplus_dvbapp-plugin] / tvcharts / src / plugin.py
1 #####################################################
2 # TVCharts Plugin for Enigma2 Dreamboxes
3 # Coded by Homey (c) 2010
4 #
5 # Version: 1.0
6 # Support: www.i-have-a-dreambox.com
7 #####################################################
8 from Components.About import about
9 from Components.ActionMap import ActionMap
10 from Components.Button import Button
11 from Components.config import config, configfile, getConfigListEntry, ConfigSubsection, ConfigYesNo, ConfigInteger, ConfigSelection
12 from Components.ConfigList import ConfigList, ConfigListScreen
13 from Components.Label import Label
14 from Components.MenuList import MenuList
15 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest
16 from Components.Network import iNetwork
17 from Components.ServiceEventTracker import ServiceEventTracker
18 from Components.Sources.StaticText import StaticText
19 from Components.UsageConfig import preferredTimerPath
20 from Components.Pixmap import Pixmap
21 from RecordTimer import RecordTimer, RecordTimerEntry, parseEvent
22 from ServiceReference import ServiceReference
23 from Screens.EventView import EventViewSimple
24 from Screens.MessageBox import MessageBox
25 from Screens.Screen import Screen
26 from Screens.Setup import SetupSummary
27 from Screens.TimerEntry import TimerEntry
28 from Screens.TimerEdit import TimerSanityConflict
29 from Tools.Directories import fileExists
30 from Tools.HardwareInfo import HardwareInfo
31 from Plugins.Plugin import PluginDescriptor
32
33 from enigma import eTimer, eEPGCache, loadJPG, loadPNG, loadPic, eListboxPythonMultiContent, gFont, eServiceReference, eServiceCenter, iPlayableService
34 from time import gmtime, strftime
35 from twisted.web.client import getPage
36 from xml.dom.minidom import parse, parseString
37 from urllib import quote, quote_plus, unquote, unquote_plus, urlencode
38
39 import timer
40 import xml.etree.cElementTree
41 import Screens.Standby
42
43 ##############################
44 #####  CONFIG SETTINGS   #####
45 ##############################
46 config.plugins.tvcharts = ConfigSubsection()
47 config.plugins.tvcharts.enabled = ConfigYesNo(default = True)
48 config.plugins.tvcharts.maxentries = ConfigInteger(default=10, limits=(5, 100))
49 config.plugins.tvcharts.maxtimerentries = ConfigInteger(default=10, limits=(5, 100))
50 config.plugins.tvcharts.submittimers = ConfigYesNo(default = True)
51 config.plugins.tvcharts.bouquetfilter = ConfigYesNo(default = True)
52
53 ##########################################################
54 session = [ ]
55
56 #Channellist Menu Entry
57 class ChannelListMenu(MenuList):
58         def __init__(self, list, enableWrapAround=False):
59                 MenuList.__init__(self, list, enableWrapAround, eListboxPythonMultiContent)
60                 self.l.setFont(0, gFont("Regular", 24))
61                 self.l.setFont(1, gFont("Regular", 20))
62                 self.l.setFont(2, gFont("Regular", 16))
63                 self.l.setItemHeight(76)
64
65 def ChannelListEntryComponent(type, channelname, serviceref, eventid, eventname, starttime, endtime, usercount, percent):
66         res = [ (serviceref, eventid) ]
67
68         # PIXMAP / PICON
69         pixmap = "/usr/share/enigma2/skin_default/picon_default.png"
70         searchPaths = ('/usr/share/enigma2/picon/','/media/cf/picon/','/media/usb/picon/')
71
72         srefstring = serviceref
73         pos = srefstring.rfind(':')
74         if pos != -1:
75                 srefstring = srefstring[:pos].rstrip(':').replace(':','_')
76                 for path in searchPaths:
77                         pngname = path + srefstring + ".png"
78                         if fileExists(pngname):
79                                 pixmap = pngname
80
81         # Build Menu
82         if type == "tvcharts":
83                 res.append(MultiContentEntryPixmapAlphaTest(pos=(8, 8), size=(100, 60), png=loadPNG(pixmap)))
84                 res.append(MultiContentEntryText(pos=(130, 5), size=(480, 30), font=0, text="%s (Viewer: %s)" % (channelname, usercount)))
85                 res.append(MultiContentEntryText(pos=(130, 35), size=(480, 25), font=1, text=eventname))
86         elif type == "timercharts":
87                 res.append(MultiContentEntryPixmapAlphaTest(pos=(10, 10), size=(100, 60), png=loadPNG(pixmap)))
88                 res.append(MultiContentEntryText(pos=(130, 5), size=(480, 28), font=0, text="%s (User: %s)" % (channelname, usercount)))
89                 res.append(MultiContentEntryText(pos=(130, 33), size=(480, 25), font=1, text=eventname))
90                 res.append(MultiContentEntryText(pos=(130, 57), size=(480, 20), font=2, text="%s Uhr - %s Uhr (%smin)" % (strftime("%d.%m.%Y %H:%M", gmtime(starttime)), strftime("%H:%M", gmtime(endtime)), int((endtime-starttime)/60))))
91
92         return res
93
94 ##############################
95 #####   TV Charts MAIN   #####
96 ##############################
97
98 class TVChartsMain(Screen):
99
100         skin = """
101         <screen position="center,center" size="600,510" title="TV Charts">
102                 <widget name="channellist" position="10,10" zPosition="1" size="580,458" scrollbarMode="showOnDemand" />
103                 <widget name="info" position="0,447" zPosition="2" size="600,20" font="Regular;18" noWrap="1" foregroundColor="#ffffff" transparent="1" halign="center" valign="center" />
104                 <ePixmap name="red"    position="20,470"  zPosition="3" size="140,40" pixmap="/usr/lib/enigma2/python/Plugins/Extensions/TVCharts/images/key_red.png" transparent="1" alphatest="on" />
105                 <ePixmap name="green"  position="160,470" zPosition="3" size="140,40" pixmap="/usr/lib/enigma2/python/Plugins/Extensions/TVCharts/images/key_green.png" transparent="1" alphatest="on" />
106                 <ePixmap name="yellow" position="300,470" zPosition="3" size="140,40" pixmap="/usr/lib/enigma2/python/Plugins/Extensions/TVCharts/images/key_yellow.png" transparent="1" alphatest="on" />
107                 <ePixmap name="blue"   position="440,470" zPosition="3" size="140,40" pixmap="/usr/lib/enigma2/python/Plugins/Extensions/TVCharts/images/key_blue.png" transparent="1" alphatest="on" />
108                 <widget name="key_red" position="20,470" zPosition="4" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
109                 <widget name="key_green" position="160,470" zPosition="4" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
110                 <widget name="key_yellow" position="300,470" zPosition="4" size="140,40" valign="center" halign="center"  font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
111                 <widget name="key_blue" position="440,470" zPosition="4" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
112         </screen>"""
113
114         def __init__(self, session):
115                 Screen.__init__(self, session)
116
117                 self.session = session
118
119                 self["channellist"] = ChannelListMenu([])
120                 self["info"] = Label()
121
122                 self["key_red"] = Button("TV Charts")
123                 self["key_green"] = Button("Timer Charts")
124                 self["key_yellow"] = Button("")
125                 self["key_blue"] = Button("Settings")
126
127                 self["actions"] = ActionMap(["OkCancelActions", "ColorActions", "EPGSelectActions"],
128                 {
129                         "ok": self.okClicked,
130                         "red": self.switchToTVCharts,
131                         "green": self.switchToTimerCharts,
132                         "blue": self.SettingsMenu,
133                         "info": self.ShowEventInfo,
134                         "cancel": self.close
135                 }, -1)
136
137                 self.epgcache = eEPGCache.getInstance()
138                 self.eventcache = []
139
140                 self.RefreshTimer = eTimer()
141                 self.RefreshTimer.callback.append(self.downloadList)
142
143                 self.onLayoutFinish.append(self.firstPluginExec)
144
145         def firstPluginExec(self):
146                 self.updateEventCache()
147                 self.switchToTVCharts()
148
149         def okClicked(self):
150                 current = self["channellist"].getCurrent()
151                 if current is None:
152                         return
153
154                 if self.mode == "tvcharts":
155                         self.session.nav.playService(eServiceReference(str(current[0][0])))
156                 elif self.mode == "timercharts":
157                         serviceref = ServiceReference(current[0][0])
158                         eventid = int(current[0][1])
159                         event = self.getEventFromId(serviceref, eventid)
160                         if event is not None:
161                                 newEntry = RecordTimerEntry(serviceref, *parseEvent(event), checkOldTimers = True, dirname = preferredTimerPath())
162                                 self.session.openWithCallback(self.addTimerCallback, TimerEntry, newEntry)
163                         else:
164                                 self.session.open(MessageBox, "Sorry, no EPG Info available for this event", type=MessageBox.TYPE_ERROR, timeout=10)
165
166         def addTimerCallback(self, answer):
167                 if answer[0]:
168                         entry = answer[1]
169                         simulTimerList = self.session.nav.RecordTimer.record(entry)
170                         if simulTimerList is not None:
171                                 for x in simulTimerList:
172                                         if x.setAutoincreaseEnd(entry):
173                                                 self.session.nav.RecordTimer.timeChanged(x)
174                                 simulTimerList = self.session.nav.RecordTimer.record(entry)
175                                 if simulTimerList is not None:
176                                         self.session.openWithCallback(self.finishSanityCorrection, TimerSanityConflict, simulTimerList)
177                 else:
178                         print "Timeredit aborted"
179
180         def finishSanityCorrection(self, answer):
181                 self.addTimerCallback(answer)
182
183         def SettingsMenu(self):
184                 self.session.open(TVChartsSetup)
185
186         def ShowEventInfo(self):
187                 current = self["channellist"].getCurrent()
188                 if current is None:
189                         return
190
191                 serviceref = current[0][0]
192                 eventid = current[0][1]
193
194                 service = ServiceReference(serviceref)
195                 event = self.getEventFromId(service, eventid)
196
197                 if event is not None:
198                         self.session.open(EventViewSimple, event, service)
199
200         def getEventFromId(self, service, eventid):
201                 event = None
202                 if self.epgcache is not None and eventid is not None:
203                         event = self.epgcache.lookupEventId(service.ref, eventid)
204                 return event
205
206         def updateEventCache(self):
207                 try:
208                         from Screens.ChannelSelection import service_types_tv
209                         from Components.Sources.ServiceList import ServiceList
210                         bouquetlist = ServiceList(eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet'), validate_commands=False).getServicesAsList()
211                         for bouquetitem in bouquetlist:
212                                 serviceHandler = eServiceCenter.getInstance()
213                                 list = serviceHandler.list(eServiceReference(str(bouquetitem[0])))
214                                 services = list and list.getContent('S')
215                                 search = ['IBDCTSERNX']
216
217                                 if services: # It's a Bouquet
218                                         search.extend([(service, 0, -1) for service in services])
219
220                                 events = self.epgcache.lookupEvent(search)
221
222                                 for eventinfo in events:
223                                         #0 eventID | 4 eventname | 5 short descr | 6 long descr | 7 serviceref | 8 channelname
224                                         self.eventcache.append((eventinfo[0], eventinfo[7], eventinfo[8], eventinfo[4]))
225
226                 except Exception:
227                         print "[TVCharts Plugin] Error creating eventcache!"
228
229         def switchToTVCharts(self):
230                 self.mode = "tvcharts"
231                 self.setTitle("TV Charts")
232                 self["channellist"].setList([])
233                 self.feedurl = "http://www.dreambox-plugins.de/feeds/topchannels.php"
234                 self.downloadList()
235
236         def switchToTimerCharts(self):
237                 self.mode = "timercharts"
238                 self.setTitle("Timer Charts")
239                 self["channellist"].setList([])
240                 self.feedurl = "http://www.dreambox-plugins.de/feeds/toptimers.php?limit=%s" % config.plugins.tvcharts.maxtimerentries.value
241                 self.downloadList()
242
243         def downloadList(self):
244                 if not config.plugins.tvcharts.enabled.value:
245                         self["info"].setText("Error: Plugin disabled in Settings ...")
246                 else:
247                         self["info"].setText("Download feeds from server ...")
248                         getPage(self.feedurl).addCallback(self.downloadListCallback).addErrback(self.downloadListError)
249
250         def downloadListError(self, error=""):
251                 print str(error)
252                 self.session.open(MessageBox, "Error downloading Feed:\n%s" % str(error), type=MessageBox.TYPE_ERROR)
253                 self["info"].setText("Error downloading Feed!")
254
255         def downloadListCallback(self, page=""):
256                 self["info"].setText("Parsing Feeds ...")
257
258                 channellist = []
259                 channelcount = 0
260                 useronline = 0
261                 totalusers = 0
262                 xml = parseString(page)
263
264                 #channellist.append(ChannelListEntryComponent("NAME", "SERVICEREF", "EVENTNAME", "USERCOUNT", "PERCENT"))
265
266                 if self.mode == "tvcharts":
267                         for node in xml.getElementsByTagName("DATA"):
268                                 useronline = int(node.getElementsByTagName("USERCOUNT")[0].childNodes[0].data)
269                                 totalusers = int(node.getElementsByTagName("TOTALUSERS")[0].childNodes[0].data)
270
271                         for node in xml.getElementsByTagName("CHANNEL"):
272                                 event_id = None
273                                 channelname = unquote_plus(str(node.getElementsByTagName("NAME")[0].childNodes[0].data))
274                                 serviceref = unquote_plus(str(node.getElementsByTagName("SERVICEREF")[0].childNodes[0].data))
275                                 eventname = unquote_plus(str(node.getElementsByTagName("EVENTNAME")[0].childNodes[0].data))
276                                 usercount = int(node.getElementsByTagName("USERCOUNT")[0].childNodes[0].data)
277                                 percent = int(node.getElementsByTagName("PERCENT")[0].childNodes[0].data)
278                                 inBouquet = False
279
280                                 # Look for favourite channel for this event in my bouqets
281                                 for sepginfo in self.eventcache:
282                                         if sepginfo[2] == channelname:
283                                                 inBouquet = True
284                                         if sepginfo[3] == eventname:
285                                                 event_id = sepginfo[0]
286                                         if sepginfo[3] == eventname and sepginfo[1] != serviceref:
287                                                 if channelname[0:3].lower() == sepginfo[2][0:3].lower():
288                                                         serviceref = sepginfo[1]
289                                                         channelname = sepginfo[2]
290                                                 inBouquet = True
291                                                 break
292                                         elif sepginfo[3] == eventname and sepginfo[1] == serviceref:
293                                                 break
294
295                                 # Skip Channels that are not in my bouquets
296                                 if config.plugins.tvcharts.bouquetfilter.value and not inBouquet:
297                                         continue
298
299                                 # Skip Channels that are not in my bouquets
300                                 channelcount += 1
301                                 if channelcount > config.plugins.tvcharts.maxentries.value:
302                                         break
303
304                                 # Add to List
305                                 channellist.append(ChannelListEntryComponent(self.mode, channelname, serviceref, event_id, eventname, 0, 0, usercount, percent))
306
307                         if totalusers > 0:
308                                 self.setTitle("TV Charts (User online: %s of %s)" % (useronline, totalusers))
309
310                 elif self.mode == "timercharts":
311                         for node in xml.getElementsByTagName("TIMER"):
312                                 eitID = str(node.getElementsByTagName("ID")[0].childNodes[0].data)
313                                 channelname = str(node.getElementsByTagName("CHANNELNAME")[0].childNodes[0].data)
314                                 serviceref = str(node.getElementsByTagName("SERVICEREF")[0].childNodes[0].data)
315                                 eventname = str(node.getElementsByTagName("EVENTNAME")[0].childNodes[0].data)
316                                 starttime = str(node.getElementsByTagName("STARTTIME")[0].childNodes[0].data)
317                                 endtime = str(node.getElementsByTagName("ENDTIME")[0].childNodes[0].data)
318                                 usercount = str(node.getElementsByTagName("USERCOUNT")[0].childNodes[0].data)
319                                 percent = str(node.getElementsByTagName("PERCENT")[0].childNodes[0].data)
320
321                                 # Add to List
322                                 channellist.append(ChannelListEntryComponent(self.mode, unquote_plus(channelname), unquote_plus(serviceref), int(eitID), unquote_plus(eventname), int(starttime), int(endtime), unquote_plus(usercount), unquote_plus(percent)))
323
324                 self["info"].setText("")
325                 self["channellist"].setList(channellist)
326
327                 self.RefreshTimer.start(60000, True)
328
329
330 ############################
331 #####  SETTINGS SCREEN #####
332 ############################
333 class TVChartsSetup(Screen, ConfigListScreen):
334         def __init__(self, session):
335                 Screen.__init__(self, session)
336                 self.skinName = [ "TVChartsSetup", "Setup" ]
337                 self.setup_title = _("TV Charts Settings")
338
339                 self.onChangedEntry = [ ]
340                 self.list = [ ]
341                 ConfigListScreen.__init__(self, self.list, session = session, on_change = self.changedEntry)
342
343                 self["actions"] = ActionMap(["SetupActions", "ColorActions"],
344                 {
345                         "ok": self.SaveSettings,
346                         "green": self.SaveSettings,
347                         "red": self.Exit,
348                         "cancel": self.Exit
349                 }, -2)
350
351                 self["key_green"] = StaticText(_("OK"))
352                 self["key_red"] = StaticText(_("Cancel"))
353
354                 self.createSetup()
355                 self.onLayoutFinish.append(self.layoutFinished)
356
357         def layoutFinished(self):
358                 self.setTitle(self.setup_title)
359
360         def createSetup(self):
361                 self.list = [ getConfigListEntry(_("TV Charts Plugin Enable"), config.plugins.tvcharts.enabled) ]
362                 if config.plugins.tvcharts.enabled.value:
363                         self.list.extend((
364                                 getConfigListEntry(_("Max Toplist Entries"), config.plugins.tvcharts.maxentries),
365                                 getConfigListEntry(_("Max Timerlist Entries"), config.plugins.tvcharts.maxtimerentries),
366                                 getConfigListEntry(_("Enable Bouquet-Filter?"), config.plugins.tvcharts.bouquetfilter),
367                                 getConfigListEntry(_("Submit Timerlist?"), config.plugins.tvcharts.submittimers),
368                         ))
369
370                 self["config"].list = self.list
371                 self["config"].setList(self.list)
372
373         def keyLeft(self):
374                 ConfigListScreen.keyLeft(self)
375                 if self["config"].getCurrent()[1] == config.plugins.tvcharts.enabled:
376                         self.createSetup()
377
378         def keyRight(self):
379                 ConfigListScreen.keyRight(self)
380                 if self["config"].getCurrent()[1] == config.plugins.tvcharts.enabled:
381                         self.createSetup()
382
383         def changedEntry(self):
384                 for x in self.onChangedEntry:
385                         x()
386
387         def getCurrentEntry(self):
388                 return self["config"].getCurrent()[0]
389
390         def getCurrentValue(self):
391                 return str(self["config"].getCurrent()[1].getText())
392
393         def createSummary(self):
394                 return SetupSummary
395
396         def SaveSettings(self):
397                 config.plugins.tvcharts.save()
398                 configfile.save()
399                 self.close()
400
401         def Exit(self):
402                 self.close()
403
404
405 ##############################
406 #####   UPDATE STATUS    #####
407 ##############################
408
409 class DBUpdateStatus(Screen):
410         def __init__(self, session):
411                 Screen.__init__(self, session)
412
413                 self.DBStatusTimer = eTimer()
414                 self.DBStatusTimer.callback.append(self.updateStatus)
415
416                 self.__event_tracker = ServiceEventTracker(screen = self, eventmap =
417                         {
418                                 iPlayableService.evUpdatedInfo: self.restartTimer,
419                                 iPlayableService.evUpdatedEventInfo: self.restartTimer
420                         })
421
422                 self.recordtimer = session.nav.RecordTimer
423                 self.NetworkConnectionAvailable = False
424
425                 self.onShow.append(self.restartTimer)
426
427         def restartTimer(self):
428                 if self.NetworkConnectionAvailable:
429                         self.DBStatusTimer.stop()
430                         self.DBStatusTimer.start(15000, True)
431                 else:
432                         iNetwork.checkNetworkState(self.checkNetworkCB)
433
434         def checkNetworkCB(self, data):
435                 if data is not None:
436                         if data <= 2:
437                                 self.NetworkConnectionAvailable = True
438                                 self.DBStatusTimer.start(15000, True)
439                         else:
440                                 self.NetworkConnectionAvailable = False
441                                 self.DBStatusTimer.stop()
442
443         def updateStatus(self):
444                 print "[TVCharts] Status Update ..."
445                 self.DBStatusTimer.stop()
446
447                 if not config.plugins.tvcharts.enabled.value or Screens.Standby.inStandby:
448                         return
449
450                 # Get Channelname
451                 ref = self.session.nav.getCurrentlyPlayingServiceReference()
452                 if ref is not None:
453                         serviceHandler = eServiceCenter.getInstance()
454                         info = serviceHandler.info(ref)
455                         channel_name = info and info.getName(ref).replace('\xc2\x86', '').replace('\xc2\x87', '').decode("utf-8", "ignore").encode("utf-8") or ""
456                         self.serviceref = ref.toString()
457                 else:
458                         channel_name = ""
459                         self.serviceref = ""
460
461                 # Get Event Info
462                 service = self.session.nav.getCurrentService()
463                 info = service and service.info()
464                 event = info and info.getEvent(0)
465                 event_name = event and event.getEventName() or ""
466                 event_begin = 0
467
468                 if event is not None:
469                         curEvent = parseEvent(event)
470                         event_begin = int(curEvent[0])
471
472                 # Get Box Info
473                 self.BoxID = iNetwork.getAdapterAttribute("eth0", "mac")
474                 self.DeviceName = HardwareInfo().get_device_name()
475                 self.EnigmaVersion = about.getEnigmaVersionString()
476                 self.ImageVersion = about.getVersionString()
477
478                 # Get TimerList
479                 self.timerlist = ""
480                 if config.plugins.tvcharts.submittimers.value:
481                         try:
482                                 for timer in self.recordtimer.timer_list:
483                                         if timer.disabled == 0 and timer.justplay == 0:
484                                                 self.timerlist += "%s|%s|%s|%s|%s|%s\n" % (timer.eit,str(int(timer.begin)+(config.recording.margin_before.value*60)), str(int(timer.end)-(config.recording.margin_after.value*60)), str(timer.service_ref), timer.name, timer.service_ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '').decode("utf-8", "ignore").encode("utf-8"))
485                         except Exception:
486                                 print "[TVCharts] Error loading timers!"
487
488                 # Status Update
489                 url = "http://www.dreambox-plugins.de/feeds/TVCharts/status.php"
490                 getPage(url, method='POST', headers={'Content-Type':'application/x-www-form-urlencoded'}, postdata=urlencode({'boxid' : self.BoxID, 'devicename' : self.DeviceName, 'imageversion' : self.ImageVersion, 'enigmaversion' : self.EnigmaVersion, 'lastchannel' : channel_name, 'lastevent' : event_name, 'lastbegin' : event_begin, 'lastserviceref' : self.serviceref, 'timerlist' : self.timerlist}))
491
492                 # Restart Timer
493                 self.DBStatusTimer.start(900000, True)
494
495 ##########################################################
496
497 def main(session, **kwargs):
498         session.open(TVChartsMain)
499
500 def autostart(reason, **kwargs):
501         global session
502         if "session" in kwargs:
503                 session = kwargs["session"]
504                 DBUpdateStatus(session)
505
506 def Plugins(path, **kwargs):
507         return [
508                 PluginDescriptor(where = [PluginDescriptor.WHERE_SESSIONSTART], fnc = autostart),
509                 PluginDescriptor(name="TV Charts", description="TV Charts Plugin", where=PluginDescriptor.WHERE_EXTENSIONSMENU, fnc=main),
510                 PluginDescriptor(name="TV Charts", description="TV Charts Plugin", where=PluginDescriptor.WHERE_PLUGINMENU, fnc=main) ]