2c8d52a38f2d5261f46add37531bbdecd582cc44
[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 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 def filter_none(string):
149         return string
150
151 def filter_xml(s):
152         try:
153                 return s.replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;")
154         except AttributeError:
155                 # if s is None
156                 return s
157
158 def filter_javascript_escape(s):
159         return s.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"')
160
161 def filter_uri(s):
162         return s.replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+')
163
164 # a to-be-filled list item
165 class ListItem:
166         def __init__(self, name, filterfnc):
167                 self.name = name
168                 self.filterfnc = filterfnc
169
170 class TextToHTML(Converter):
171         def __init__(self, arg):
172                 Converter.__init__(self, arg)
173
174         def getHTML(self, id):
175                 return self.source.text # encode & etc. here!
176
177 # a null-output. Useful if you only want to issue a command.
178 class Null(Converter):
179         def __init__(self, arg):
180                 Converter.__init__(self, arg)
181
182         def getHTML(self, id):
183                 return ""
184
185 class JavascriptUpdate(Converter):
186         def __init__(self, arg):
187                 Converter.__init__(self, arg)
188
189         def getHTML(self, id):
190                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
191                 #                all other will replace this in JS
192                 return '<script>parent.set("' + id + '", "' + filter_javascript_escape(self.source.text) + '");</script>\n'
193         
194 # the performant 'listfiller'-engine (plfe)
195 class ListFiller(Converter):
196         def __init__(self, arg):
197                 Converter.__init__(self, arg)
198
199         def getText(self):
200                 l = self.source.list
201                 lut = self.source.lut
202                 
203                 # now build a ["string", 1, "string", 2]-styled list, with indices into the 
204                 # list to avoid lookup of item name for each entry
205                 lutlist = []
206                 for element in self.converter_arguments:
207                         if isinstance(element, str):
208                                 lutlist.append(element)
209                         elif isinstance(element, ListItem):
210                                 lutlist.append((lut[element.name], element.filterfnc))
211                 
212                 # now, for the huge list, do:
213                 res = ""
214                 for item in l:
215                         for element in lutlist:
216                                 if isinstance(element, str):
217                                         res += element
218                                 else:
219                                         res += str(element[1](item[element[0]]))
220                 # (this will be done in c++ later!)
221                 return res
222
223         text = property(getText)
224
225 class webifHandler(ContentHandler):
226         def __init__(self, session):
227                 self.res = [ ]
228                 self.mode = 0
229                 self.screen = None
230                 self.session = session
231                 self.screens = [ ]
232         
233         def startElement(self, name, attrs):
234                 if name == "e2:screen":
235                         self.screen = eval(attrs["name"])(self.session) # fixme
236                         self.screens.append(self.screen)
237                         return
238         
239                 if name[:3] == "e2:":
240                         self.mode += 1
241                 
242                 tag = "<" + name + ''.join([' ' + key + '="' + val + '"' for (key, val) in attrs.items()]) + ">"
243                 tag = tag.encode("UTF-8")
244                 
245                 if self.mode == 0:
246                         self.res.append(tag)
247                 elif self.mode == 1: # expect "<e2:element>"
248                         assert name == "e2:element", "found %s instead of e2:element" % name
249                         source = attrs["source"]
250                         self.source_id = str(attrs.get("id", source))
251                         self.source = self.screen[source]
252                         self.is_streaming = "streaming" in attrs
253                 elif self.mode == 2: # expect "<e2:convert>"
254                         if name[:3] == "e2:":
255                                 assert name == "e2:convert"
256                                 
257                                 ctype = attrs["type"]
258                                 
259                                         # TODO: we need something better here
260                                 if ctype[:4] == "web:": # for now
261                                         self.converter = eval(ctype[4:])
262                                 else:
263                                         try:
264                                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
265                                         except ImportError:
266                                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
267                                 self.sub = [ ]
268                         else:
269                                 self.sub.append(tag)
270                 elif self.mode == 3:
271                         assert name == "e2:item", "found %s instead of e2:item!" % name
272                         assert "name" in attrs, "e2:item must have a name= attribute!"
273                         
274                         filter = {"": filter_none, "javascript_escape": filter_javascript_escape, "xml": filter_xml, "uri": filter_uri}[attrs.get("filter", "")]
275                         
276                         self.sub.append(ListItem(attrs["name"], filter))
277
278         def endElement(self, name):
279                 if name == "e2:screen":
280                         self.screen = None
281                         return
282
283                 tag = "</" + name + ">"
284                 if self.mode == 0:
285                         self.res.append(tag)
286                 elif self.mode == 2 and name[:3] != "e2:":
287                         self.sub.append(tag)
288                 elif self.mode == 2: # closed 'convert' -> sub
289                         self.sub = lreduce(self.sub)
290                         if len(self.sub) == 1:
291                                 self.sub = self.sub[0]
292                         c = self.converter(self.sub)
293                         c.connect(self.source)
294                         self.source = c
295                         
296                         del self.sub
297                 elif self.mode == 1: # closed 'element'
298                         # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
299                         if not self.is_streaming:
300                                 c = OneTimeElement(self.source_id)
301                         else:
302                                 c = StreamingElement(self.source_id)
303                         
304                         c.connect(self.source)
305                         self.res.append(c)
306                         self.screen.renderer.append(c)
307                         del self.source
308
309                 if name[:3] == "e2:":
310                         self.mode -= 1
311
312         def processingInstruction(self, target, data):
313                 self.res.append('<?' + target + ' ' + data + '>')
314         
315         def characters(self, ch):
316                 ch = ch.encode("UTF-8")
317                 if self.mode == 0:
318                         self.res.append(ch)
319                 elif self.mode == 2:
320                         self.sub.append(ch)
321         
322         def startEntity(self, name):
323                 self.res.append('&' + name + ';');
324
325         def execBegin(self):
326                 for screen in self.screens:
327                         screen.execBegin()
328
329         def cleanup(self):
330                 print "screen cleanup!"
331                 for screen in self.screens:
332                         screen.execEnd()
333                         screen.doClose()
334                 self.screens = [ ]
335
336 def lreduce(list):
337         # ouch, can be made better
338         res = [ ]
339         string = None
340         for x in list:
341                 if isinstance(x, str) or isinstance(x, unicode):
342                         if isinstance(x, unicode):
343                                 x = x.encode("UTF-8")
344                         if string is None:
345                                 string = x
346                         else:
347                                 string += x
348                 else:
349                         if string is not None:
350                                 res.append(string)
351                                 string = None
352                         res.append(x)
353         if string is not None:
354                 res.append(string)
355                 string = None
356         return res
357
358 def renderPage(stream, path, req, session):
359         
360         # read in the template, create required screens
361         # we don't have persistense yet.
362         # if we had, this first part would only be done once.
363         handler = webifHandler(session)
364         parser = make_parser()
365         parser.setFeature(feature_namespaces, 0)
366         parser.setContentHandler(handler)
367         parser.parse(open(util.sibpath(__file__, path)))
368         
369         # by default, we have non-streaming pages
370         finish = True
371         
372         # first, apply "commands" (aka. URL argument)
373         for x in handler.res:
374                 if isinstance(x, Element):
375                         x.handleCommand(req.args)
376
377         handler.execBegin()
378
379         # now, we have a list with static texts mixed
380         # with non-static Elements.
381         # flatten this list, write into the stream.
382         for x in lreduce(handler.res):
383                 if isinstance(x, Element):
384                         if isinstance(x, StreamingElement):
385                                 finish = False
386                                 x.setStream(stream)
387                         x.render(stream)
388                 else:
389                         stream.write(str(x))
390
391         def ping(s):
392                 from twisted.internet import reactor
393                 s.write("\n");
394                 reactor.callLater(3, ping, s)
395
396         # if we met a "StreamingElement", there is at least one
397         # element which wants to output data more than once,
398         # i.e. on host-originated changes.
399         # in this case, don't finish yet, don't cleanup yet,
400         # but instead do that when the client disconnects.
401         if finish:
402                 handler.cleanup()
403                 stream.finish()
404         else:
405                 # ok.
406                 # you *need* something which constantly sends something in a regular interval,
407                 # in order to detect disconnected clients.
408                 # i agree that this "ping" sucks terrible, so better be sure to have something 
409                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
410                 ping(stream)
411                 stream.closed_callback = lambda: handler.cleanup()