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