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