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