now using ListFiller to increase performance
[enigma2-plugins.git] / webinterface / src / usr / lib / enigma2 / python / Plugins / Extensions / WebInterface / 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 Components.Sources.Volume import Volume
20 from Components.Sources.EPG import EPG
21 from Components.Sources.FrontendStatus import FrontendStatus
22
23 from Components.Converter.Converter import Converter
24 from Components.Converter.VolumeToText import VolumeToText 
25 from Components.Converter.EPGToText import EPGToText 
26
27 from Components.Element import Element
28
29 from xml.sax import make_parser
30 from xml.sax.handler import ContentHandler, feature_namespaces
31 from twisted.python import util
32 import sys
33 import time
34  
35 # prototype of the new web frontend template system.
36
37 class WebScreen(Screen):
38         def __init__(self, session):
39                 Screen.__init__(self, session)
40                 self.stand_alone = True
41
42 # a test screen
43 class TestScreen(InfoBarServiceName, InfoBarEvent,InfoBarTuner, WebScreen,Volume):
44         def __init__(self, session):
45                 WebScreen.__init__(self, session)
46                 InfoBarServiceName.__init__(self)
47                 InfoBarEvent.__init__(self)
48                 InfoBarTuner.__init__(self)
49                 Volume.__init__(self,session);
50                 self["CurrentTime"] = Clock()
51 #               self["TVSystem"] = Config(config.av.tvsystem)
52 #               self["OSDLanguage"] = Config(config.osd.language)
53 #               self["FirstRun"] = Config(config.misc.firstrun)
54                 from enigma import eServiceReference
55                 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')
56                 self["ServiceList"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
57                 self["ServiceListBrowse"] = ServiceList(fav, command_func = self.browseTo)
58                 self["Volume"] = Volume(session)
59                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
60                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
61                 self["EPGNOWNEXT"] = EPG(session,func=EPG.NOWNEXT)
62
63         def browseTo(self, reftobrowse):
64                 self["ServiceListBrowse"].root = reftobrowse
65
66         def zapTo(self, reftozap):
67                 self.session.nav.playService(reftozap)
68
69 # TODO: (really.) put screens into own files.
70 class Streaming(WebScreen):
71         def __init__(self, session):
72                 WebScreen.__init__(self, session)
73                 from Components.Sources.StreamService import StreamService
74                 self["StreamService"] = StreamService(self.session.nav)
75
76 class StreamingM3U(WebScreen):
77         def __init__(self, session):
78                 WebScreen.__init__(self, session)
79                 from Components.Sources.StaticText import StaticText
80                 from Components.Sources.Config import Config
81                 from Components.config import config
82                 
83                 self["ref"] = StaticText()
84                 self["localip"] = Config(config.network.ip)
85
86 # implements the 'render'-call.
87 # this will act as a downstream_element, like a renderer.
88 class OneTimeElement(Element):
89         def __init__(self, id):
90                 Element.__init__(self)
91                 self.source_id = id
92
93         # CHECKME: is this ok performance-wise?
94         def handleCommand(self, args):
95                 for c in args.get(self.source_id, []):
96                         self.source.handleCommand(c)
97
98         def render(self, stream):
99                 t = self.source.getHTML(self.source_id)
100                 if isinstance(t, unicode):
101                         t = t.encode("utf-8")
102                 stream.write(t)
103
104         def execBegin(self):
105                 pass
106         
107         def execEnd(self):
108                 pass
109         
110         def onShow(self):
111                 pass
112
113         def onHide(self):
114                 pass
115         
116         def destroy(self):
117                 pass
118
119 class StreamingElement(OneTimeElement):
120         def __init__(self, id):
121                 OneTimeElement.__init__(self, id)
122                 self.stream = None
123
124         def changed(self, what):
125                 if self.stream:
126                         self.render(self.stream)
127
128         def setStream(self, stream):
129                 self.stream = stream
130
131 # a to-be-filled list item
132 class ListItem:
133         def __init__(self, name):
134                 self.name = name
135
136 class TextToHTML(Converter):
137         def __init__(self, arg):
138                 Converter.__init__(self, arg)
139
140         def getHTML(self, id):
141                 return self.source.text # encode & etc. here!
142
143 # a null-output. Useful if you only want to issue a command.
144 class Null(Converter):
145         def __init__(self, arg):
146                 Converter.__init__(self, arg)
147
148         def getHTML(self, id):
149                 return ""
150
151 def escape(s):
152         return s.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"')
153
154 class JavascriptUpdate(Converter):
155         def __init__(self, arg):
156                 Converter.__init__(self, arg)
157
158         def getHTML(self, id):
159                 return '<script>set("' + id + '", "' + escape(self.source.text) + '");</script>\n'
160
161 # the performant 'listfiller'-engine (plfe)
162 class ListFiller(Converter):
163         def __init__(self, arg):
164                 Converter.__init__(self, arg)
165
166         def getText(self):
167                 l = self.source.list
168                 lut = self.source.lut
169                 
170                 # now build a ["string", 1, "string", 2]-styled list, with indices into the 
171                 # list to avoid lookup of item name for each entry
172                 lutlist = []
173                 for element in self.converter_arguments:
174                         if isinstance(element, str):
175                                 lutlist.append(element)
176                         elif isinstance(element, ListItem):
177                                 lutlist.append(lut[element.name])
178                 
179                 # now, for the huge list, do:
180                 res = ""
181                 for item in l:
182                         for element in lutlist:
183                                 if isinstance(element, str):
184                                         res += element
185                                 else:
186                                         res += str(item[element])
187                 # (this will be done in c++ later!)
188                 return res
189                 
190         text = property(getText)
191
192 class webifHandler(ContentHandler):
193         def __init__(self, session):
194                 self.res = [ ]
195                 self.mode = 0
196                 self.screen = None
197                 self.session = session
198                 self.screens = [ ]
199         
200         def startElement(self, name, attrs):
201                 if name == "e2:screen":
202                         self.screen = eval(attrs["name"])(self.session) # fixme
203                         self.screens.append(self.screen)
204                         return
205         
206                 if name[:3] == "e2:":
207                         self.mode += 1
208                 
209                 tag = "<" + name + ''.join([' ' + key + '="' + val + '"' for (key, val) in attrs.items()]) + ">"
210                 tag = tag.encode("UTF-8")
211                 
212                 if self.mode == 0:
213                         self.res.append(tag)
214                 elif self.mode == 1: # expect "<e2:element>"
215                         assert name == "e2:element", "found %s instead of e2:element" % name
216                         source = attrs["source"]
217                         self.source_id = str(attrs.get("id", source))
218                         self.source = self.screen[source]
219                         self.is_streaming = "streaming" in attrs
220                 elif self.mode == 2: # expect "<e2:convert>"
221                         if name[:3] == "e2:":
222                                 assert name == "e2:convert"
223                                 
224                                 ctype = attrs["type"]
225                                 if ctype[:4] == "web:": # for now
226                                         self.converter = eval(ctype[4:])
227                                 else:
228                                         self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
229                                 self.sub = [ ]
230                         else:
231                                 self.sub.append(tag)
232                 elif self.mode == 3:
233                         assert name == "e2:item", "found %s instead of e2:item!" % name
234                         assert "name" in attrs, "e2:item must have a name= attribute!"
235                         self.sub.append(ListItem(attrs["name"]))
236
237         def endElement(self, name):
238                 if name == "e2:screen":
239                         self.screen = None
240                         return
241
242                 tag = "</" + name + ">"
243                 if self.mode == 0:
244                         self.res.append(tag)
245                 elif self.mode == 2 and name[:3] != "e2:":
246                         self.sub.append(tag)
247                 elif self.mode == 2: # closed 'convert' -> sub
248                         self.sub = lreduce(self.sub)
249                         if len(self.sub) == 1:
250                                 self.sub = self.sub[0]
251                         c = self.converter(self.sub)
252                         c.connect(self.source)
253                         self.source = c
254                         
255                         del self.sub
256                 elif self.mode == 1: # closed 'element'
257                         # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
258                         if not self.is_streaming:
259                                 c = OneTimeElement(self.source_id)
260                         else:
261                                 c = StreamingElement(self.source_id)
262                         
263                         c.connect(self.source)
264                         self.res.append(c)
265                         self.screen.renderer.append(c)
266                         del self.source
267
268                 if name[:3] == "e2:":
269                         self.mode -= 1
270
271         def processingInstruction(self, target, data):
272                 self.res.append('<?' + target + ' ' + data + '>')
273         
274         def characters(self, ch):
275                 ch = ch.encode("UTF-8")
276                 if self.mode == 0:
277                         self.res.append(ch)
278                 elif self.mode == 2:
279                         self.sub.append(ch)
280         
281         def startEntity(self, name):
282                 self.res.append('&' + name + ';');
283
284         def execBegin(self):
285                 for screen in self.screens:
286                         screen.execBegin()
287
288         def cleanup(self):
289                 print "screen cleanup!"
290                 for screen in self.screens:
291                         screen.execEnd()
292                         screen.doClose()
293                 self.screens = [ ]
294
295 def lreduce(list):
296         # ouch, can be made better
297         res = [ ]
298         string = None
299         for x in list:
300                 if isinstance(x, str) or isinstance(x, unicode):
301                         if isinstance(x, unicode):
302                                 x = x.encode("UTF-8")
303                         if string is None:
304                                 string = x
305                         else:
306                                 string += x
307                 else:
308                         if string is not None:
309                                 res.append(string)
310                                 string = None
311                         res.append(x)
312         if string is not None:
313                 res.append(string)
314                 string = None
315         return res
316
317 def renderPage(stream, path, req, session):
318         
319         # read in the template, create required screens
320         # we don't have persistense yet.
321         # if we had, this first part would only be done once.
322         handler = webifHandler(session)
323         parser = make_parser()
324         parser.setFeature(feature_namespaces, 0)
325         parser.setContentHandler(handler)
326         parser.parse(open(util.sibpath(__file__, path)))
327         
328         # by default, we have non-streaming pages
329         finish = True
330         
331         # first, apply "commands" (aka. URL argument)
332         for x in handler.res:
333                 if isinstance(x, Element):
334                         x.handleCommand(req.args)
335
336         handler.execBegin()
337
338         # now, we have a list with static texts mixed
339         # with non-static Elements.
340         # flatten this list, write into the stream.
341         for x in lreduce(handler.res):
342                 if isinstance(x, Element):
343                         if isinstance(x, StreamingElement):
344                                 finish = False
345                                 x.setStream(stream)
346                         x.render(stream)
347                 else:
348                         stream.write(str(x))
349
350         def ping(s):
351                 from twisted.internet import reactor
352                 s.write("\n");
353                 reactor.callLater(3, ping, s)
354
355         # if we met a "StreamingElement", there is at least one
356         # element which wants to output data more than once,
357         # i.e. on host-originated changes.
358         # in this case, don't finish yet, don't cleanup yet,
359         # but instead do that when the client disconnects.
360         if finish:
361                 handler.cleanup()
362                 stream.finish()
363         else:
364                 # ok.
365                 # you *need* something which constantly sends something in a regular interval,
366                 # in order to detect disconnected clients.
367                 # i agree that this "ping" sucks terrible, so better be sure to have something 
368                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
369                 ping(stream)
370                 stream.closed_callback = lambda: handler.cleanup()