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