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