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