*** empty log message ***
[vuplus_dvbapp-plugin] / webinterface / src / webif.py
1 #
2 # OK, this is more than a proof of concept
3 # things to improve:
4 #  - nicer code
5 #  - screens need to be defined somehow else. 
6 #    I don't know how, yet. Probably each in an own file.
7 #  - more components, like the channellist
8 #  - better error handling
9 #  - use namespace parser
10
11 from Screens.Screen import Screen
12 from Tools.Import import my_import
13
14 # for our testscreen
15 from Screens.InfoBarGenerics import InfoBarServiceName, InfoBarEvent, InfoBarTuner
16
17 from Components.Sources.Clock import Clock
18 from Components.Sources.ServiceList import ServiceList
19 from WebComponents.Sources.Volume import Volume
20 from WebComponents.Sources.EPG import EPG
21 from WebComponents.Sources.Timer import Timer
22 from WebComponents.Sources.Movie import Movie
23 from WebComponents.Sources.Message import Message
24 from WebComponents.Sources.PowerState import PowerState
25 from WebComponents.Sources.RemoteControl import RemoteControl
26 from WebComponents.Sources.Settings import Settings
27 from WebComponents.Sources.SubServices import SubServices
28 from WebComponents.Sources.ParentControl import ParentControl
29 from WebComponents.Sources.About import About
30
31 from WebComponents.Sources.RequestData import RequestData
32 from Components.Sources.FrontendStatus import FrontendStatus
33
34 from Components.Converter.Converter import Converter
35
36 from Components.Element import Element
37
38 from xml.sax import make_parser
39 from xml.sax.handler import ContentHandler, feature_namespaces
40 from twisted.python import util
41 import sys
42 import time
43  
44 # prototype of the new web frontend template system.
45
46 class WebScreen(Screen):
47         def __init__(self, session,request):
48                 Screen.__init__(self, session)
49                 self.stand_alone = True
50                 self.request = request
51 # a test screen
52 class TestScreen(InfoBarServiceName, InfoBarEvent,InfoBarTuner, WebScreen):
53         def __init__(self, session,request):
54                 WebScreen.__init__(self, session,request)
55                 InfoBarServiceName.__init__(self)
56                 InfoBarEvent.__init__(self)
57                 InfoBarTuner.__init__(self)
58                 self["CurrentTime"] = Clock()
59 #               self["TVSystem"] = Config(config.av.tvsystem)
60 #               self["OSDLanguage"] = Config(config.osd.language)
61 #               self["FirstRun"] = Config(config.misc.firstrun)
62                 from enigma import eServiceReference
63                 fav = eServiceReference('1:7:1:0:0:0:0:0:0:0:(type == 1) || (type == 17) || (type == 195) || (type == 25) FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
64                 self["ServiceList"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
65                 self["ServiceListBrowse"] = ServiceList(fav, command_func = self.browseTo, validate_commands=False)
66
67                 self["ParentControlList"] = ParentControl(session)
68                 self["SubServices"] = SubServices(session)
69                 self["Volume"] = Volume(session)
70                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
71                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
72                 self["EPGNOW"] = EPG(session,func=EPG.NOW)
73                 self["TimerList"] = Timer(session,func = Timer.LIST)
74                 self["TimerAddEventID"] = Timer(session,func = Timer.ADDBYID)
75                 self["TimerAdd"] = Timer(session,func = Timer.ADD)
76                 self["TimerDel"] = Timer(session,func = Timer.DEL)
77                 self["TimerChange"] = Timer(session,func = Timer.CHANGE)
78                 self["TimerListWrite"] = Timer(session,func = Timer.WRITE)
79                 self["TVBrowser"] = Timer(session,func = Timer.TVBROWSER)
80                 self["RecordNow"] = Timer(session,func = Timer.RECNOW)
81                 self["MovieList"] = Movie(session,func = Movie.LIST)
82                 self["MovieFileDel"] = Movie(session,func = Movie.DEL)
83                 self["Volume"] = Volume(session)
84                 self["Message"] = Message(session)
85                 self["PowerState"] = PowerState(session)
86                 self["RemoteControl"] = RemoteControl(session)
87                 self["Settings"] = Settings(session)
88                 
89                 self["About"] = About(session)
90                 
91         def browseTo(self, reftobrowse):
92                 self["ServiceListBrowse"].root = reftobrowse
93
94         def zapTo(self, reftozap):
95                 from Components.config import config
96                 pc = config.ParentalControl.configured.value
97                 if pc:
98                         config.ParentalControl.configured.value = False
99                 self.session.nav.playService(reftozap)
100                 if pc:
101                         config.ParentalControl.configured.value = pc
102                 """
103                 switsching config.ParentalControl.configured.value
104                 ugly, but necessary :(
105                 """
106
107 # TODO: (really.) put screens into own files.
108 class Streaming(WebScreen):
109         def __init__(self, session,request):
110                 WebScreen.__init__(self, session,request)
111                 from Components.Sources.StreamService import StreamService
112                 self["StreamService"] = StreamService(self.session.nav)
113
114 class StreamingM3U(WebScreen):
115         def __init__(self, session,request):
116                 WebScreen.__init__(self, session,request)
117                 from Components.Sources.StaticText import StaticText
118                 from Components.Sources.Config import Config
119                 from Components.config import config
120                 self["ref"] = StaticText()
121                 self["localip"] = RequestData(request,what=RequestData.HOST)
122
123 class TsM3U(WebScreen):
124         def __init__(self, session,request):
125                 WebScreen.__init__(self, session,request)
126                 from Components.Sources.StaticText import StaticText
127                 from Components.Sources.Config import Config
128                 from Components.config import config
129                 self["file"] = StaticText()
130                 self["localip"] = RequestData(request,what=RequestData.HOST)
131                 
132 class RestartTwisted(WebScreen):
133         def __init__(self, session,request):
134                 WebScreen.__init__(self, session,request)
135                 import plugin
136                 plugin.restartWebserver()
137                 
138 class GetPid(WebScreen):
139       def __init__(self, session,request):
140          WebScreen.__init__(self, session,request)
141          from Components.Sources.StaticText import StaticText
142          from enigma import iServiceInformation
143          pids = self.session.nav.getCurrentService()
144          if pids is not None:
145                  pidinfo = pids.info()
146                  VPID = hex(pidinfo.getInfo(iServiceInformation.sVideoPID))
147                  APID = hex(pidinfo.getInfo(iServiceInformation.sAudioPID))
148                  PPID = hex(pidinfo.getInfo(iServiceInformation.sPMTPID))
149          self["pids"] = StaticText("%s,%s,%s"%(PPID.lstrip("0x"),VPID.lstrip("0x"),APID.lstrip("0x")))
150          self["localip"] = RequestData(request,what=RequestData.HOST)
151
152
153 # implements the 'render'-call.
154 # this will act as a downstream_element, like a renderer.
155 class OneTimeElement(Element):
156         def __init__(self, id):
157                 Element.__init__(self)
158                 self.source_id = id
159
160         # CHECKME: is this ok performance-wise?
161         def handleCommand(self, args):
162                 if self.source_id.find(",") >=0:
163                         paramlist = self.source_id.split(",")
164                         list={}
165                         for key in paramlist:
166                                 arg = args.get(key, [])
167                                 if len(arg) == 0:
168                                         list[key] = None        
169                                 elif len(arg) == 1:
170                                         list[key] = "".join(arg)        
171                                 elif len(arg) == 2:
172                                         list[key] = arg[0]
173                         self.source.handleCommand(list)
174                 else:
175                         for c in args.get(self.source_id, []):
176                                 self.source.handleCommand(c)
177                 
178         def render(self, stream):
179                 t = self.source.getHTML(self.source_id)
180                 stream.write(t)
181
182         def execBegin(self):
183                 pass
184         
185         def execEnd(self):
186                 pass
187         
188         def onShow(self):
189                 pass
190
191         def onHide(self):
192                 pass
193         
194         def destroy(self):
195                 pass
196
197 class StreamingElement(OneTimeElement):
198         def __init__(self, id):
199                 OneTimeElement.__init__(self, id)
200                 self.stream = None
201
202         def changed(self, what):
203                 if self.stream:
204                         self.render(self.stream)
205
206         def setStream(self, stream):
207                 self.stream = stream
208
209 # a to-be-filled list item
210 class ListItem:
211         def __init__(self, name, filternum):
212                 self.name = name
213                 self.filternum = filternum
214         
215 class TextToHTML(Converter):
216         def __init__(self, arg):
217                 Converter.__init__(self, arg)
218
219         def getHTML(self, id):
220                 return self.source.text # encode & etc. here!
221
222 class ReturnEmptyXML(Converter):
223         def __init__(self, arg):
224                 Converter.__init__(self, arg)
225                 
226         def getHTML(self, id):
227                 return "<rootElement></rootElement>"
228
229 # a null-output. Useful if you only want to issue a command.
230 class Null(Converter):
231         def __init__(self, arg):
232                 Converter.__init__(self, arg)
233
234         def getHTML(self, id):
235                 return ""
236
237 class JavascriptUpdate(Converter):
238         def __init__(self, arg):
239                 Converter.__init__(self, arg)
240
241         def getHTML(self, id):
242                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
243                 #                all other will replace this in JS
244                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
245
246 # the performant 'listfiller'-engine (plfe)
247 class ListFiller(Converter):
248         def __init__(self, arg):
249                 Converter.__init__(self, arg)
250
251         def getText(self):
252                 l = self.source.list
253                 lut = self.source.lut
254                 conv_args = self.converter_arguments
255
256                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
257                 # list to avoid lookup of item name for each entry
258                 lutlist = [ isinstance(element, basestring) and (element, None) or (lut[element.name], element.filternum) for element in conv_args ]
259
260                 # now, for the huge list, do:
261                 strlist = [ ]
262                 append = strlist.append
263                 for item in l:
264                         for (element, filternum) in lutlist:
265                                 if not filternum:
266                                         append(element)
267                                 elif filternum == 2:
268                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
269                                 elif filternum == 3:
270                                         append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
271                                 elif filternum == 4:
272                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
273                                 else:
274                                         append(str(item[element]))
275                 # (this will be done in c++ later!)
276                 return ''.join(strlist)
277
278         text = property(getText)
279
280 class webifHandler(ContentHandler):
281         def __init__(self, session,request):
282                 self.res = [ ]
283                 self.mode = 0
284                 self.screen = None
285                 self.session = session
286                 self.screens = [ ]
287                 self.request = request
288         
289         def startElement(self, name, attrs):
290                 if name == "e2:screen":
291                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
292                         self.screens.append(self.screen)
293                         return
294         
295                 if name[:3] == "e2:":
296                         self.mode += 1
297
298                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
299                 tag.insert(0, name)
300                 tag.insert(0, '<')
301                 tag.append('>')
302                 tag = ''.join(tag)#.encode('utf-8')
303
304                 if self.mode == 0:
305                         self.res.append(tag)
306                 elif self.mode == 1: # expect "<e2:element>"
307                         assert name == "e2:element", "found %s instead of e2:element" % name
308                         source = attrs["source"]
309                         self.source_id = str(attrs.get("id", source))
310                         self.source = self.screen[source]
311                         self.is_streaming = "streaming" in attrs
312                 elif self.mode == 2: # expect "<e2:convert>"
313                         if name[:3] == "e2:":
314                                 assert name == "e2:convert"
315                                 
316                                 ctype = attrs["type"]
317                                 
318                                         # TODO: we need something better here
319                                 if ctype[:4] == "web:": # for now
320                                         self.converter = eval(ctype[4:])
321                                 else:
322                                         try:
323                                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
324                                         except ImportError:
325                                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
326                                 self.sub = [ ]
327                         else:
328                                 self.sub.append(tag)
329                 elif self.mode == 3:
330                         assert name == "e2:item", "found %s instead of e2:item!" % name
331                         assert "name" in attrs, "e2:item must have a name= attribute!"
332                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
333                         self.sub.append(ListItem(attrs["name"], filter))
334
335         def endElement(self, name):
336                 if name == "e2:screen":
337                         self.screen = None
338                         return
339
340                 tag = "</" + name + ">"
341                 if self.mode == 0:
342                         self.res.append(tag)
343                 elif self.mode == 2 and name[:3] != "e2:":
344                         self.sub.append(tag)
345                 elif self.mode == 2: # closed 'convert' -> sub
346                         if len(self.sub) == 1:
347                                 self.sub = self.sub[0]
348                         c = self.converter(self.sub)
349                         c.connect(self.source)
350                         self.source = c
351                         del self.sub
352                 elif self.mode == 1: # closed 'element'
353                         # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
354                         if not self.is_streaming:
355                                 c = OneTimeElement(self.source_id)
356                         else:
357                                 c = StreamingElement(self.source_id)
358                         
359                         c.connect(self.source)
360                         self.res.append(c)
361                         self.screen.renderer.append(c)
362                         del self.source
363
364                 if name[:3] == "e2:":
365                         self.mode -= 1
366
367         def processingInstruction(self, target, data):
368                 self.res.append('<?' + target + ' ' + data + '>')
369         
370         def characters(self, ch):
371                 ch = ch.encode('utf-8')
372                 if self.mode == 0:
373                         self.res.append(ch)
374                 elif self.mode == 2:
375                         self.sub.append(ch)
376         
377         def startEntity(self, name):
378                 self.res.append('&' + name + ';');
379
380         def execBegin(self):
381                 for screen in self.screens:
382                         screen.execBegin()
383
384         def cleanup(self):
385                 print "screen cleanup!"
386                 for screen in self.screens:
387                         screen.execEnd()
388                         screen.doClose()
389                 self.screens = [ ]
390
391 def renderPage(stream, path, req, session):
392         
393         # read in the template, create required screens
394         # we don't have persistense yet.
395         # if we had, this first part would only be done once.
396         handler = webifHandler(session,req)
397         parser = make_parser()
398         parser.setFeature(feature_namespaces, 0)
399         parser.setContentHandler(handler)
400         parser.parse(open(util.sibpath(__file__, path)))
401         
402         # by default, we have non-streaming pages
403         finish = True
404         
405         # first, apply "commands" (aka. URL argument)
406         for x in handler.res:
407                 if isinstance(x, Element):
408                         x.handleCommand(req.args)
409
410         handler.execBegin()
411
412         # now, we have a list with static texts mixed
413         # with non-static Elements.
414         # flatten this list, write into the stream.
415         for x in handler.res:
416                 if isinstance(x, Element):
417                         if isinstance(x, StreamingElement):
418                                 finish = False
419                                 x.setStream(stream)
420                         x.render(stream)
421                 else:
422                         stream.write(str(x))
423
424         def ping(s):
425                 from twisted.internet import reactor
426                 s.write("\n");
427                 reactor.callLater(3, ping, s)
428         
429         # if we met a "StreamingElement", there is at least one
430         # element which wants to output data more than once,
431         # i.e. on host-originated changes.
432         # in this case, don't finish yet, don't cleanup yet,
433         # but instead do that when the client disconnects.
434         if finish:
435                 handler.cleanup()
436                 stream.finish()
437         else:
438                 # ok.
439                 # you *need* something which constantly sends something in a regular interval,
440                 # in order to detect disconnected clients.
441                 # i agree that this "ping" sucks terrible, so better be sure to have something 
442                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
443                 ping(stream)
444                 stream.closed_callback = lambda: handler.cleanup()