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