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