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