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