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