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