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