show HD services in webif
[vuplus_dvbapp-plugin] / 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 from enigma import eServiceReference
12
13 from Screens.Screen import Screen
14 from Screens.ChannelSelection import service_types_tv, service_types_radio
15 from Tools.Import import my_import
16
17 from Components.Sources.Source import ObsoleteSource
18
19 from Components.Sources.Clock import Clock
20 from Components.Sources.ServiceList import ServiceList
21
22 from WebComponents.Sources.ServiceListRecursive import ServiceListRecursive
23 from WebComponents.Sources.Volume import Volume
24 from WebComponents.Sources.EPG import EPG
25 from WebComponents.Sources.Timer import Timer
26 from WebComponents.Sources.Movie import Movie
27 from WebComponents.Sources.Message import Message
28 from WebComponents.Sources.PowerState import PowerState
29 from WebComponents.Sources.RemoteControl import RemoteControl
30 from WebComponents.Sources.Settings import Settings
31 from WebComponents.Sources.SubServices import SubServices
32 from WebComponents.Sources.ParentControl import ParentControl
33 from WebComponents.Sources.About import About
34 from WebComponents.Sources.RequestData import RequestData
35 from WebComponents.Sources.AudioTracks import AudioTracks
36 from WebComponents.Sources.WAPfunctions import WAPfunctions
37 from WebComponents.Sources.MP import MP
38 from WebComponents.Sources.Files import Files
39 from WebComponents.Sources.ServiceListReload import ServiceListReload
40
41 from Components.Sources.FrontendStatus import FrontendStatus
42
43 from Components.Converter.Converter import Converter
44
45 from Components.Element import Element
46
47 from xml.sax import make_parser
48 from xml.sax.handler import ContentHandler, feature_namespaces
49 from xml.sax.saxutils import escape as escape_xml
50 from twisted.python import util
51
52 # prototype of the new web frontend template system.
53
54 class WebScreen(Screen):
55         def __init__(self, session, request):
56                 Screen.__init__(self, session)
57                 self.stand_alone = True
58                 self.request = request
59                 self.instance = None
60
61 class DummyWebScreen(WebScreen):
62         #use it, if you dont need any source, just to can do a static file with an xml-file
63         def __init__(self, session,request):
64                 WebScreen.__init__(self, session,request)
65
66 class UpdateWebScreen(WebScreen):
67         def __init__(self, session,request):
68                 WebScreen.__init__(self, session,request)
69                 self["CurrentTime"] = Clock()
70                 fav = eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
71
72 class MessageWebScreen(WebScreen):
73         def __init__(self, session,request):
74                 WebScreen.__init__(self, session,request)
75                 self["Message"] = Message(session,func = Message.PRINT)
76                 self["GetAnswer"] = Message(session,func = Message.ANSWER)
77                 
78 class ServiceListReloadWebScreen(WebScreen):
79         def __init__(self, session,request):
80                 WebScreen.__init__(self, session,request)
81                 self["ServiceListReload"] = ServiceListReload(session)
82                 
83 class AudioWebScreen(WebScreen):
84         def __init__(self, session,request):
85                 WebScreen.__init__(self, session,request)
86                 self["AudioTracks"] = AudioTracks(session, func=AudioTracks.GET)
87                 self["SelectAudioTrack"] = AudioTracks(session, func=AudioTracks.SET)   
88
89 class AboutWebScreen(WebScreen):
90         def __init__(self, session,request):
91                 WebScreen.__init__(self, session,request)
92                 self["About"] = About(session)
93                 
94 class VolumeWebScreen(WebScreen):
95         def __init__(self, session,request):
96                 WebScreen.__init__(self, session,request)
97                 self["Volume"] = Volume(session)
98
99 class SettingsWebScreen(WebScreen):
100         def __init__(self, session,request):
101                 WebScreen.__init__(self, session,request)
102                 self["Settings"] = Settings(session)
103
104 class SubServiceWebScreen(WebScreen):
105         def __init__(self, session,request):
106                 WebScreen.__init__(self, session,request)
107                 self["SubServices"] = SubServices(session)
108
109 class ServiceWebScreen(WebScreen):
110         def __init__(self, session,request):
111                 WebScreen.__init__(self, session,request)
112
113                 fav = eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
114                 self["SwitchService"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
115                 self["ServiceList"] = ServiceList(fav, command_func = self.getServiceList, validate_commands=False)
116                 self["ServiceListRecursive"] = ServiceListRecursive(session, func=ServiceListRecursive.FETCH)
117
118         def getServiceList(self, sRef):
119                 self["ServiceList"].root = sRef
120
121         def zapTo(self, reftozap):
122                 from Components.config import config
123                 pc = config.ParentalControl.configured.value
124                 if pc:
125                         config.ParentalControl.configured.value = False
126                 self.session.nav.playService(reftozap)
127                 if pc:
128                         config.ParentalControl.configured.value = pc
129                 """
130                 switching config.ParentalControl.configured.value
131                 ugly, but necessary :(
132                 """
133
134 class EPGWebScreen(WebScreen):
135         def __init__(self, session,request):
136                 WebScreen.__init__(self, session,request)
137                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
138                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
139                 self["EPGNOW"] = EPG(session,func=EPG.NOW)
140                 self["EPGNEXT"] = EPG(session,func=EPG.NEXT)
141
142 class MovieWebScreen(WebScreen):
143         def __init__(self, session,request):
144                 WebScreen.__init__(self, session,request)
145                 from Components.MovieList import MovieList
146                 from Tools.Directories import resolveFilename,SCOPE_HDD
147                 movielist = MovieList(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + resolveFilename(SCOPE_HDD)))
148                 self["MovieList"] = Movie(session,movielist,func = Movie.LIST)
149                 self["MovieFileDel"] = Movie(session,movielist,func = Movie.DEL)
150                 self["MovieTags"] = Movie(session,movielist,func = Movie.TAGS)
151
152 class MediaPlayerWebScreen(WebScreen):
153         def __init__(self, session,request):
154                 WebScreen.__init__(self, session,request)
155                 self["FileList"] = MP(session,func = MP.LIST)
156                 self["PlayFile"] = MP(session,func = MP.PLAY)
157                 self["Command"] = MP(session,func = MP.COMMAND)
158                 self["WritePlaylist"] = MP(session,func = MP.WRITEPLAYLIST)
159                 
160 class FilesWebScreen(WebScreen):
161         def __init__(self, session,request):
162                 WebScreen.__init__(self, session,request)
163                 self["DelFile"] = Files(session,func = Files.DEL)
164                 
165 class TimerWebScreen(WebScreen):
166         def __init__(self, session,request):
167                 WebScreen.__init__(self, session,request)
168                 self["TimerList"] = Timer(session,func = Timer.LIST)
169                 self["TimerAddEventID"] = Timer(session,func = Timer.ADDBYID)
170                 self["TimerAdd"] = Timer(session,func = Timer.ADD)
171                 self["TimerDel"] = Timer(session,func = Timer.DEL)
172                 self["TimerChange"] = Timer(session,func = Timer.CHANGE)
173                 self["TimerListWrite"] = Timer(session,func = Timer.WRITE)
174                 self["TVBrowser"] = Timer(session,func = Timer.TVBROWSER)
175                 self["RecordNow"] = Timer(session,func = Timer.RECNOW)
176
177 class RemoteWebScreen(WebScreen):
178         def __init__(self, session,request):
179                 WebScreen.__init__(self, session,request)
180                 self["RemoteControl"] = RemoteControl(session)
181
182 class PowerWebScreen(WebScreen):
183         def __init__(self, session,request):
184                 WebScreen.__init__(self, session,request)
185                 self["PowerState"] = PowerState(session)
186
187 class ParentControlWebScreen(WebScreen):
188         def __init__(self, session,request):
189                 WebScreen.__init__(self, session,request)
190                 self["ParentControlList"] = ParentControl(session)
191                                 
192 class WAPWebScreen(WebScreen):
193         def __init__(self, session,request):
194                 WebScreen.__init__(self, session,request)
195                 self["WAPFillOptionListSyear"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
196                 self["WAPFillOptionListSday"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
197                 self["WAPFillOptionListSmonth"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
198                 self["WAPFillOptionListShour"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
199                 self["WAPFillOptionListSmin"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
200                 
201                 self["WAPFillOptionListEyear"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
202                 self["WAPFillOptionListEday"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
203                 self["WAPFillOptionListEmonth"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
204                 self["WAPFillOptionListEhour"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
205                 self["WAPFillOptionListEmin"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
206                 
207                 self["WAPFillOptionListRecord"] = WAPfunctions(session,func = WAPfunctions.OPTIONLIST)
208                 self["WAPFillOptionListAfterEvent"] = WAPfunctions(session,func = WAPfunctions.OPTIONLIST)
209                 
210                 self["WAPFillValueName"] = WAPfunctions(session,func = WAPfunctions.FILLVALUE)
211                 self["WAPFillValueDescr"] = WAPfunctions(session,func = WAPfunctions.FILLVALUE)
212
213                 self["WAPFillOptionListRepeated"] = WAPfunctions(session,func = WAPfunctions.REPEATED)
214                 self["WAPServiceList"] = WAPfunctions(session, func = WAPfunctions.SERVICELIST)
215
216                 self["WAPdeleteOldOnSave"] = WAPfunctions(session,func = WAPfunctions.DELETEOLD)
217         
218 class StreamingWebScreen(WebScreen):
219         def __init__(self, session,request):
220                 WebScreen.__init__(self, session,request)
221                 from Components.Sources.StreamService import StreamService
222                 self["StreamService"] = StreamService(self.session.nav)
223
224 class M3UStreamingWebScreen(WebScreen):
225         def __init__(self, session,request):
226                 WebScreen.__init__(self, session,request)
227                 from Components.Sources.StaticText import StaticText
228                 from Components.Sources.Config import Config
229                 from Components.config import config
230                 self["ref"] = StaticText()
231                 self["localip"] = RequestData(request,what=RequestData.HOST)
232
233 class TsM3U(WebScreen):
234         def __init__(self, session,request):
235                 WebScreen.__init__(self, session,request)
236                 from Components.Sources.StaticText import StaticText
237                 from Components.Sources.Config import Config
238                 from Components.config import config
239                 self["file"] = StaticText()
240                 self["localip"] = RequestData(request,what=RequestData.HOST)
241
242 class RestartWebScreen(WebScreen):
243         def __init__(self, session,request):
244                 WebScreen.__init__(self, session,request)
245                 import plugin
246                 plugin.restartWebserver()
247                 
248 class GetPid(WebScreen):
249       def __init__(self, session,request):
250          WebScreen.__init__(self, session,request)
251          from Components.Sources.StaticText import StaticText
252          from enigma import iServiceInformation
253          pids = self.session.nav.getCurrentService()
254          if pids is not None:
255                  pidinfo = pids.info()
256                  VPID = hex(pidinfo.getInfo(iServiceInformation.sVideoPID))
257                  APID = hex(pidinfo.getInfo(iServiceInformation.sAudioPID))
258                  PPID = hex(pidinfo.getInfo(iServiceInformation.sPMTPID))
259          self["pids"] = StaticText("%s,%s,%s"%(PPID.lstrip("0x"),VPID.lstrip("0x"),APID.lstrip("0x")))
260          self["localip"] = RequestData(request,what=RequestData.HOST)
261
262
263 # implements the 'render'-call.
264 # this will act as a downstream_element, like a renderer.
265 class OneTimeElement(Element):
266         def __init__(self, id):
267                 Element.__init__(self)
268                 self.source_id = id
269
270         # CHECKME: is this ok performance-wise?
271         def handleCommand(self, args):
272                 if self.source_id.find(",") >=0:
273                         paramlist = self.source_id.split(",")
274                         list={}
275                         for key in paramlist:
276                                 arg = args.get(key, [])
277                                 if len(arg) == 0:
278                                         list[key] = None        
279                                 elif len(arg) == 1:
280                                         list[key] = "".join(arg)        
281                                 elif len(arg) == 2:
282                                         list[key] = arg[0]
283                         self.source.handleCommand(list)
284                 else:
285                         for c in args.get(self.source_id, []):
286                                 self.source.handleCommand(c)
287
288         def render(self, stream):
289                 t = self.source.getHTML(self.source_id)
290                 stream.write(t)
291
292         def execBegin(self):
293                 pass
294
295         def execEnd(self):
296                 pass
297
298         def onShow(self):
299                 pass
300
301         def onHide(self):
302                 pass
303
304         def destroy(self):
305                 pass
306
307 class MacroElement(OneTimeElement):
308         def __init__(self, id, macro_dict, macro_name):
309                 OneTimeElement.__init__(self, id)
310                 self.macro_dict = macro_dict
311                 self.macro_name = macro_name
312
313         def render(self, stream):
314                 self.macro_dict[self.macro_name] = self.source.getHTML(self.source_id)
315
316 class StreamingElement(OneTimeElement):
317         def __init__(self, id):
318                 OneTimeElement.__init__(self, id)
319                 self.stream = None
320
321         def changed(self, what):
322                 if self.stream:
323                         self.render(self.stream)
324
325         def setStream(self, stream):
326                 self.stream = stream
327
328 # a to-be-filled list item
329 class ListItem:
330         def __init__(self, name, filternum):
331                 self.name = name
332                 self.filternum = filternum
333
334 class ListMacroItem:
335         def __init__(self, macrodict, macroname):
336                 self.macrodict = macrodict
337                 self.macroname = macroname
338
339 class TextToHTML(Converter):
340         def __init__(self, arg):
341                 Converter.__init__(self, arg)
342
343         def getHTML(self, id):
344                 return self.source.text # encode & etc. here!
345
346 class TextToXML(Converter):
347         def __init__(self, arg):
348                 Converter.__init__(self, arg)
349         
350         def getHTML(self, id):
351                 return escape_xml(self.source.text).replace("\x19", "").replace("\x1c", "").replace("\x1e", "")
352
353 class TextToURL(Converter):
354         def __init__(self, arg):
355                 Converter.__init__(self, arg)
356
357         def getHTML(self, id):
358                 return self.source.text.replace(" ","%20")
359
360 class ReturnEmptyXML(Converter):
361         def __init__(self, arg):
362                 Converter.__init__(self, arg)
363
364         def getHTML(self, id):
365                 return "<rootElement></rootElement>"
366
367 # a null-output. Useful if you only want to issue a command.
368 class Null(Converter):
369         def __init__(self, arg):
370                 Converter.__init__(self, arg)
371
372         def getHTML(self, id):
373                 return ""
374
375 class JavascriptUpdate(Converter):
376         def __init__(self, arg):
377                 Converter.__init__(self, arg)
378
379         def getHTML(self, id):
380                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
381                 #                all other will replace this in JS
382                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
383
384 # the performant 'listfiller'-engine (plfe)
385 class ListFiller(Converter):
386         def __init__(self, arg):
387                 Converter.__init__(self, arg)
388 #               print "ListFiller-arg: ",arg
389
390         def getText(self):
391                 l = self.source.list
392                 lut = self.source.lut
393                 conv_args = self.converter_arguments
394
395                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
396                 # list to avoid lookup of item name for each entry
397                 lutlist = [ ]
398                 for element in conv_args:
399                         if isinstance(element, basestring):
400                                 lutlist.append((element, None))
401                         elif isinstance(element, ListItem):
402                                 lutlist.append((lut[element.name], element.filternum))
403                         elif isinstance(element, ListMacroItem):
404                                 lutlist.append((element.macrodict[element.macroname], None))
405                         else:
406                                 raise "neither string, ListItem nor ListMacroItem"
407
408                 # now, for the huge list, do:
409                 strlist = [ ]
410                 append = strlist.append
411                 for item in l:
412                         for (element, filternum) in lutlist:
413                                 if not filternum:
414                                         append(element)
415                                 elif filternum == 2:
416                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
417                                 elif filternum == 3:
418                                         #append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
419                                         append(escape_xml(str(item[element])))
420                                 elif filternum == 4:
421                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
422                                 else:
423                                         append(str(item[element]))
424                 # (this will be done in c++ later!)
425                 return ''.join(strlist)
426
427         text = property(getText)
428
429 class webifHandler(ContentHandler):
430         def __init__(self, session, request):
431                 self.res = [ ]
432                 self.mode = 0
433                 self.screen = None
434                 self.session = session
435                 self.screens = [ ]
436                 self.request = request
437                 self.macros = { }
438
439         def start_element(self, attrs):
440                 scr = self.screen
441
442                 wsource = attrs["source"]
443
444                 path = wsource.split('.')
445                 while len(path) > 1:
446                         scr = self.screen.getRelatedScreen(path[0])
447                         if scr is None:
448                                 print "[webif.py] Parent Screen not found!"
449                                 print wsource
450                         path = path[1:]
451
452                 source = scr.get(path[0])
453
454                 if isinstance(source, ObsoleteSource):
455                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
456                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
457                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
458                         if source.description:
459                                 print source.description
460
461                         wsource = source.new_source
462                 else:
463                         pass
464                         # otherwise, use that source.
465
466                 self.source = source
467                 self.source_id = str(attrs.get("id", wsource))
468                 self.is_streaming = "streaming" in attrs
469                 self.macro_name = attrs.get("macro") or None
470
471         def end_element(self):
472                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
473                 if not self.is_streaming:
474                         if self.macro_name is None:
475                                 c = OneTimeElement(self.source_id)
476                         else:
477                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
478                 else:
479                         assert self.macro_name is None
480                         c = StreamingElement(self.source_id)
481
482                 c.connect(self.source)
483                 self.res.append(c)
484                 self.screen.renderer.append(c)
485                 del self.source
486
487         def start_convert(self, attrs):
488                 ctype = attrs["type"]
489
490                         # TODO: we need something better here
491                 if ctype[:4] == "web:": # for now
492                         self.converter = eval(ctype[4:])
493                 else:
494                         try:
495                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
496                         except ImportError:
497                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
498                 self.sub = [ ]
499
500         def end_convert(self):
501                 if len(self.sub) == 1:
502                         self.sub = self.sub[0]
503                 c = self.converter(self.sub)
504                 c.connect(self.source)
505                 self.source = c
506                 del self.sub
507
508         def parse_item(self, attrs):
509                 if "name" in attrs:
510                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
511                         self.sub.append(ListItem(attrs["name"], filter))
512                 else:
513                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
514                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
515
516         def startElement(self, name, attrs):
517                 if name == "e2:screen":
518                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
519                         self.screens.append(self.screen)
520                         return
521
522                 if name[:3] == "e2:":
523                         self.mode += 1
524
525                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
526                 tag.insert(0, name)
527                 tag.insert(0, '<')
528                 tag.append('>')
529                 tag = ''.join(tag)#.encode('utf-8')
530                 
531                 if self.mode == 0:
532                         self.res.append(tag)
533                 elif self.mode == 1: # expect "<e2:element>"
534                         assert name == "e2:element", "found %s instead of e2:element" % name
535                         self.start_element(attrs)
536                 elif self.mode == 2: # expect "<e2:convert>"
537                         if name[:3] == "e2:":
538                                 assert name == "e2:convert"
539                                 self.start_convert(attrs)
540                         else:
541                                 self.sub.append(tag)
542                 elif self.mode == 3:
543                         assert name == "e2:item", "found %s instead of e2:item!" % name
544                         
545                         self.parse_item(attrs)
546
547         def endElement(self, name):
548                 if name == "e2:screen":
549                         self.screen = None
550                         return
551
552                 tag = "</" + name + ">"
553                 if self.mode == 0:
554                         self.res.append(tag)
555                 elif self.mode == 2 and name[:3] != "e2:":
556                         self.sub.append(tag)
557                 elif self.mode == 2: # closed 'convert' -> sub
558                         self.end_convert()
559                 elif self.mode == 1: # closed 'element'
560                         self.end_element()
561                 if name[:3] == "e2:":
562                         self.mode -= 1
563
564         def processingInstruction(self, target, data):
565                 self.res.append('<?' + target + ' ' + data + '>')
566
567         def characters(self, ch):
568                 ch = ch.encode('utf-8')
569                 if self.mode == 0:
570                         self.res.append(ch)
571                 elif self.mode == 2:
572                         self.sub.append(ch)
573
574         def startEntity(self, name):
575                 self.res.append('&' + name + ';');
576
577         def execBegin(self):
578                 for screen in self.screens:
579                         screen.execBegin()
580
581         def cleanup(self):
582                 print "screen cleanup!"
583                 for screen in self.screens:
584                         screen.execEnd()
585                         screen.doClose()
586                 self.screens = [ ]
587
588 def renderPage(stream, path, req, session):
589         
590         # read in the template, create required screens
591         # we don't have persistense yet.
592         # if we had, this first part would only be done once.
593         handler = webifHandler(session,req)
594         parser = make_parser()
595         parser.setFeature(feature_namespaces, 0)
596         parser.setContentHandler(handler)
597         parser.parse(open(util.sibpath(__file__, path)))
598         
599         # by default, we have non-streaming pages
600         finish = True
601         
602         # first, apply "commands" (aka. URL argument)
603         for x in handler.res:
604                 if isinstance(x, Element):
605                         x.handleCommand(req.args)
606
607         handler.execBegin()
608
609         # now, we have a list with static texts mixed
610         # with non-static Elements.
611         # flatten this list, write into the stream.
612         for x in handler.res:
613                 if isinstance(x, Element):
614                         if isinstance(x, StreamingElement):
615                                 finish = False
616                                 x.setStream(stream)
617                         x.render(stream)
618                 else:
619                         stream.write(str(x))
620
621         def ping(s):
622                 from twisted.internet import reactor
623                 s.write("\n");
624                 reactor.callLater(3, ping, s)
625         
626         # if we met a "StreamingElement", there is at least one
627         # element which wants to output data more than once,
628         # i.e. on host-originated changes.
629         # in this case, don't finish yet, don't cleanup yet,
630         # but instead do that when the client disconnects.
631         if finish:
632                 handler.cleanup()
633                 stream.finish()
634         else:
635                 # ok.
636                 # you *need* something which constantly sends something in a regular interval,
637                 # in order to detect disconnected clients.
638                 # i agree that this "ping" sucks terrible, so better be sure to have something 
639                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
640                 ping(stream)
641                 stream.closed_callback = lambda: handler.cleanup()