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