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