Add web/epgnext - returns epgnext of bouquet (similar to epgnow)
[enigma2-plugins.git] / 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 Tools.Import import my_import
15
16 from Screens.InfoBarGenerics import InfoBarServiceName, InfoBarEvent, InfoBarTuner
17
18 from Components.Sources.Source import ObsoleteSource
19
20 from Components.Sources.Clock import Clock
21 from Components.Sources.ServiceList import ServiceList
22
23 from WebComponents.Sources.ServiceListRecursive import ServiceListRecursive
24 from WebComponents.Sources.Volume import Volume
25 from WebComponents.Sources.EPG import EPG
26 from WebComponents.Sources.Timer import Timer
27 from WebComponents.Sources.Movie import Movie
28 from WebComponents.Sources.Message import Message
29 from WebComponents.Sources.PowerState import PowerState
30 from WebComponents.Sources.RemoteControl import RemoteControl
31 from WebComponents.Sources.Settings import Settings
32 from WebComponents.Sources.SubServices import SubServices
33 from WebComponents.Sources.ParentControl import ParentControl
34 from WebComponents.Sources.About import About
35 from WebComponents.Sources.RequestData import RequestData
36 from WebComponents.Sources.AudioTracks import AudioTracks
37 from WebComponents.Sources.WAPfunctions import WAPfunctions
38 from WebComponents.Sources.MP import MP
39 from WebComponents.Sources.Files import Files
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
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(InfoBarServiceName, InfoBarEvent,InfoBarTuner,WebScreen):
67         def __init__(self, session,request):
68                 WebScreen.__init__(self, session,request)
69                 InfoBarServiceName.__init__(self)
70                 InfoBarEvent.__init__(self)
71                 InfoBarTuner.__init__(self)
72                 self["CurrentTime"] = Clock()
73                 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')
74                 #CurrentService
75                 #Event_Now
76                 #Event_Next
77                 #FrontendStatus
78
79 class MessageWebScreen(WebScreen):
80         def __init__(self, session,request):
81                 WebScreen.__init__(self, session,request)
82                 self["Message"] = Message(session,func = Message.PRINT)
83                 self["GetAnswer"] = Message(session,func = Message.ANSWER)
84
85 class AudioWebScreen(WebScreen):
86         def __init__(self, session,request):
87                 WebScreen.__init__(self, session,request)
88                 self["AudioTracks"] = AudioTracks(session)              
89
90 class AboutWebScreen(WebScreen):
91         def __init__(self, session,request):
92                 WebScreen.__init__(self, session,request)
93                 self["About"] = About(session)
94                 
95 class VolumeWebScreen(WebScreen):
96         def __init__(self, session,request):
97                 WebScreen.__init__(self, session,request)
98                 self["Volume"] = Volume(session)
99
100 class SettingsWebScreen(WebScreen):
101         def __init__(self, session,request):
102                 WebScreen.__init__(self, session,request)
103                 self["Settings"] = Settings(session)
104
105 class SubServiceWebScreen(WebScreen):
106         def __init__(self, session,request):
107                 WebScreen.__init__(self, session,request)
108                 self["SubServices"] = SubServices(session)
109
110 class ServiceWebScreen(WebScreen):
111         def __init__(self, session,request):
112                 WebScreen.__init__(self, session,request)
113                 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')
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 TextToURL(Converter):
347         def __init__(self, arg):
348                 Converter.__init__(self, arg)
349
350         def getHTML(self, id):
351                 return self.source.text.replace(" ","%20")
352
353 class ReturnEmptyXML(Converter):
354         def __init__(self, arg):
355                 Converter.__init__(self, arg)
356
357         def getHTML(self, id):
358                 return "<rootElement></rootElement>"
359
360 # a null-output. Useful if you only want to issue a command.
361 class Null(Converter):
362         def __init__(self, arg):
363                 Converter.__init__(self, arg)
364
365         def getHTML(self, id):
366                 return ""
367
368 class JavascriptUpdate(Converter):
369         def __init__(self, arg):
370                 Converter.__init__(self, arg)
371
372         def getHTML(self, id):
373                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
374                 #                all other will replace this in JS
375                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
376
377 # the performant 'listfiller'-engine (plfe)
378 class ListFiller(Converter):
379         def __init__(self, arg):
380                 Converter.__init__(self, arg)
381 #               print "ListFiller-arg: ",arg
382
383         def getText(self):
384                 l = self.source.list
385                 lut = self.source.lut
386                 conv_args = self.converter_arguments
387
388                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
389                 # list to avoid lookup of item name for each entry
390                 lutlist = [ ]
391                 for element in conv_args:
392                         if isinstance(element, basestring):
393                                 lutlist.append((element, None))
394                         elif isinstance(element, ListItem):
395                                 lutlist.append((lut[element.name], element.filternum))
396                         elif isinstance(element, ListMacroItem):
397                                 lutlist.append((element.macrodict[element.macroname], None))
398                         else:
399                                 raise "neither string, ListItem nor ListMacroItem"
400
401                 # now, for the huge list, do:
402                 strlist = [ ]
403                 append = strlist.append
404                 for item in l:
405                         for (element, filternum) in lutlist:
406                                 if not filternum:
407                                         append(element)
408                                 elif filternum == 2:
409                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
410                                 elif filternum == 3:
411                                         append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
412                                 elif filternum == 4:
413                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
414                                 else:
415                                         append(str(item[element]))
416                 # (this will be done in c++ later!)
417                 return ''.join(strlist)
418
419         text = property(getText)
420
421 class webifHandler(ContentHandler):
422         def __init__(self, session, request):
423                 self.res = [ ]
424                 self.mode = 0
425                 self.screen = None
426                 self.session = session
427                 self.screens = [ ]
428                 self.request = request
429                 self.macros = { }
430
431         def start_element(self, attrs):
432                 scr = self.screen
433
434                 wsource = attrs["source"]
435
436                 path = wsource.split('.')
437                 while len(path) > 1:
438                         scr = self.screen.getRelatedScreen(path[0])
439                         if scr is None:
440                                 print "[webif.py] Parent Screen not found!"
441                                 print wsource
442                         path = path[1:]
443
444                 source = scr.get(path[0])
445
446                 if isinstance(source, ObsoleteSource):
447                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
448                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
449                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
450                         if source.description:
451                                 print source.description
452
453                         wsource = source.new_source
454                 else:
455                         pass
456                         # otherwise, use that source.
457
458                 self.source = source            
459                 self.source_id = str(attrs.get("id", wsource))
460                 self.is_streaming = "streaming" in attrs
461                 self.macro_name = attrs.get("macro") or None
462
463         def end_element(self):
464                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
465                 if not self.is_streaming:
466                         if self.macro_name is None:
467                                 c = OneTimeElement(self.source_id)
468                         else:
469                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
470                 else:
471                         assert self.macro_name is None
472                         c = StreamingElement(self.source_id)
473
474                 c.connect(self.source)
475                 self.res.append(c)
476                 self.screen.renderer.append(c)
477                 del self.source
478
479         def start_convert(self, attrs):
480                 ctype = attrs["type"]
481
482                         # TODO: we need something better here
483                 if ctype[:4] == "web:": # for now
484                         self.converter = eval(ctype[4:])
485                 else:
486                         try:
487                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
488                         except ImportError:
489                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
490                 self.sub = [ ]
491
492         def end_convert(self):
493                 if len(self.sub) == 1:
494                         self.sub = self.sub[0]
495                 c = self.converter(self.sub)
496                 c.connect(self.source)
497                 self.source = c
498                 del self.sub
499
500         def parse_item(self, attrs):
501                 if "name" in attrs:
502                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
503                         self.sub.append(ListItem(attrs["name"], filter))
504                 else:
505                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
506                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
507
508         def startElement(self, name, attrs):
509                 if name == "e2:screen":
510                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
511                         self.screens.append(self.screen)
512                         return
513
514                 if name[:3] == "e2:":
515                         self.mode += 1
516
517                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
518                 tag.insert(0, name)
519                 tag.insert(0, '<')
520                 tag.append('>')
521                 tag = ''.join(tag)#.encode('utf-8')
522
523                 if self.mode == 0:
524                         self.res.append(tag)
525                 elif self.mode == 1: # expect "<e2:element>"
526                         assert name == "e2:element", "found %s instead of e2:element" % name
527                         self.start_element(attrs)
528                 elif self.mode == 2: # expect "<e2:convert>"
529                         if name[:3] == "e2:":
530                                 assert name == "e2:convert"
531                                 self.start_convert(attrs)
532                         else:
533                                 self.sub.append(tag)
534                 elif self.mode == 3:
535                         assert name == "e2:item", "found %s instead of e2:item!" % name
536                         self.parse_item(attrs)
537
538         def endElement(self, name):
539                 if name == "e2:screen":
540                         self.screen = None
541                         return
542
543                 tag = "</" + name + ">"
544                 if self.mode == 0:
545                         self.res.append(tag)
546                 elif self.mode == 2 and name[:3] != "e2:":
547                         self.sub.append(tag)
548                 elif self.mode == 2: # closed 'convert' -> sub
549                         self.end_convert()
550                 elif self.mode == 1: # closed 'element'
551                         self.end_element()
552                 if name[:3] == "e2:":
553                         self.mode -= 1
554
555         def processingInstruction(self, target, data):
556                 self.res.append('<?' + target + ' ' + data + '>')
557
558         def characters(self, ch):
559                 ch = ch.encode('utf-8')
560                 if self.mode == 0:
561                         self.res.append(ch)
562                 elif self.mode == 2:
563                         self.sub.append(ch)
564
565         def startEntity(self, name):
566                 self.res.append('&' + name + ';');
567
568         def execBegin(self):
569                 for screen in self.screens:
570                         screen.execBegin()
571
572         def cleanup(self):
573                 print "screen cleanup!"
574                 for screen in self.screens:
575                         screen.execEnd()
576                         screen.doClose()
577                 self.screens = [ ]
578
579 def renderPage(stream, path, req, session):
580         
581         # read in the template, create required screens
582         # we don't have persistense yet.
583         # if we had, this first part would only be done once.
584         handler = webifHandler(session,req)
585         parser = make_parser()
586         parser.setFeature(feature_namespaces, 0)
587         parser.setContentHandler(handler)
588         parser.parse(open(util.sibpath(__file__, path)))
589         
590         # by default, we have non-streaming pages
591         finish = True
592         
593         # first, apply "commands" (aka. URL argument)
594         for x in handler.res:
595                 if isinstance(x, Element):
596                         x.handleCommand(req.args)
597
598         handler.execBegin()
599
600         # now, we have a list with static texts mixed
601         # with non-static Elements.
602         # flatten this list, write into the stream.
603         for x in handler.res:
604                 if isinstance(x, Element):
605                         if isinstance(x, StreamingElement):
606                                 finish = False
607                                 x.setStream(stream)
608                         x.render(stream)
609                 else:
610                         stream.write(str(x))
611
612         def ping(s):
613                 from twisted.internet import reactor
614                 s.write("\n");
615                 reactor.callLater(3, ping, s)
616         
617         # if we met a "StreamingElement", there is at least one
618         # element which wants to output data more than once,
619         # i.e. on host-originated changes.
620         # in this case, don't finish yet, don't cleanup yet,
621         # but instead do that when the client disconnects.
622         if finish:
623                 handler.cleanup()
624                 stream.finish()
625         else:
626                 # ok.
627                 # you *need* something which constantly sends something in a regular interval,
628                 # in order to detect disconnected clients.
629                 # i agree that this "ping" sucks terrible, so better be sure to have something 
630                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
631                 ping(stream)
632                 stream.closed_callback = lambda: handler.cleanup()