add support for cyclic garbage collection to eTimer and eSocketNotifier
[enigma2.git] / RecordTimer.py
1 import time
2 #from time import datetime
3 from Tools import Directories, Notifications
4
5 from Components.config import config
6 import timer
7 import xml.dom.minidom
8
9 from enigma import eEPGCache, getBestPlayableServiceReference, \
10         eServiceReference, iRecordableService, quitMainloop
11
12 from Screens.MessageBox import MessageBox
13
14 import NavigationInstance
15
16 import Screens.Standby
17
18 from time import localtime
19
20 from Tools.XMLTools import elementsWithTag, mergeText, stringToXML
21 from ServiceReference import ServiceReference
22
23 # ok, for descriptions etc we have:
24 # service reference  (to get the service name)
25 # name               (title)
26 # description        (description)
27 # event data         (ONLY for time adjustments etc.)
28
29
30 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
31 # begin and end will be corrected
32 def parseEvent(ev, description = True):
33         if description:
34                 name = ev.getEventName()
35                 description = ev.getShortDescription()
36         else:
37                 name = ""
38                 description = ""
39         begin = ev.getBeginTime()
40         end = begin + ev.getDuration()
41         eit = ev.getEventId()
42         begin -= config.recording.margin_before.value * 60
43         end += config.recording.margin_after.value * 60
44         return (begin, end, name, description, eit)
45
46 class AFTEREVENT:
47         NONE = 0
48         STANDBY = 1
49         DEEPSTANDBY = 2
50
51 # please do not translate log messages
52 class RecordTimerEntry(timer.TimerEntry, object):
53 ######### the following static methods and members are only in use when the box is in (soft) standby
54         receiveRecordEvents = False
55
56         @staticmethod
57         def shutdown():
58                 quitMainloop(1)
59
60         @staticmethod
61         def staticGotRecordEvent(recservice, event):
62                 if event == iRecordableService.evEnd:
63                         print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
64                         recordings = NavigationInstance.instance.getRecordings()
65                         if not len(recordings): # no more recordings exist
66                                 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
67                                 if rec_time > 0 and (rec_time - time.time()) < 360:
68                                         print "another recording starts in", rec_time - time.time(), "seconds... do not shutdown yet"
69                                 else:
70                                         print "no starting records in the next 360 seconds... immediate shutdown"
71                                         RecordTimerEntry.shutdown() # immediate shutdown
72                 elif event == iRecordableService.evStart:
73                         print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
74
75         @staticmethod
76         def stopTryQuitMainloop():
77                 print "RecordTimer.stopTryQuitMainloop"
78                 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
79                 RecordTimerEntry.receiveRecordEvents = False
80
81         @staticmethod
82         def TryQuitMainloop():
83                 if not RecordTimerEntry.receiveRecordEvents:
84                         print "RecordTimer.TryQuitMainloop"
85                         NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
86                         RecordTimerEntry.receiveRecordEvents = True
87                         # send fake event.. to check if another recordings are running or
88                         # other timers start in a few seconds
89                         RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
90                         # send normal notification for the case the user leave the standby now..
91                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop)
92 #################################################################
93
94         def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.NONE, checkOldTimers = False):
95                 timer.TimerEntry.__init__(self, int(begin), int(end))
96
97                 if checkOldTimers == True:
98                         if self.begin < time.time() - 1209600:
99                                 self.begin = int(time.time())
100                 
101                 if self.end < self.begin:
102                         self.end = self.begin
103                 
104                 assert isinstance(serviceref, ServiceReference)
105                 
106                 self.service_ref = serviceref
107                 self.eit = eit
108                 self.dontSave = False
109                 self.name = name
110                 self.description = description
111                 self.disabled = disabled
112                 self.timer = None
113                 self.__record_service = None
114                 self.start_prepare = 0
115                 self.justplay = justplay
116                 self.afterEvent = afterEvent
117                 
118                 self.log_entries = []
119                 self.resetState()
120         
121         def log(self, code, msg):
122                 self.log_entries.append((int(time.time()), code, msg))
123                 print "[TIMER]", msg
124         
125         def resetState(self):
126                 self.state = self.StateWaiting
127                 self.cancelled = False
128                 self.first_try_prepare = True
129                 self.timeChanged()
130         
131         def calculateFilename(self):
132                 service_name = self.service_ref.getServiceName()
133                 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
134                 
135                 print "begin_date: ", begin_date
136                 print "service_name: ", service_name
137                 print "name:", self.name
138                 print "description: ", self.description
139                 
140                 filename = begin_date + " - " + service_name
141                 if self.name:
142                         filename += " - " + self.name
143
144                 self.Filename = Directories.getRecordingFilename(filename)
145                 self.log(0, "Filename calculated as: '%s'" % self.Filename)
146                 #begin_date + " - " + service_name + description)
147
148         def tryPrepare(self):
149                 if self.justplay:
150                         return True
151                 else:
152                         self.calculateFilename()
153                         rec_ref = self.service_ref and self.service_ref.ref
154                         if rec_ref and rec_ref.flags & eServiceReference.isGroup:
155                                 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
156                                 if not rec_ref:
157                                         self.log(1, "'get best playable service for group... record' failed")
158                                         return False
159                                 
160                         self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
161
162                         if not self.record_service:
163                                 self.log(1, "'record service' failed")
164                                 return False
165
166                         if self.repeated:
167                                 epgcache = eEPGCache.getInstance()
168                                 queryTime=self.begin+(self.end-self.begin)/2
169                                 evt = epgcache.lookupEventTime(rec_ref, queryTime)
170                                 if evt:
171                                         self.description = evt.getShortDescription()
172                                         event_id = evt.getEventId()
173                                 else:
174                                         event_id = -1
175                         else:
176                                 event_id = self.eit
177                                 if event_id is None:
178                                         event_id = -1
179
180                         prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
181                         if prep_res:
182                                 self.log(2, "'prepare' failed: error %d" % prep_res)
183                                 NavigationInstance.instance.stopRecordService(self.record_service)
184                                 self.record_service = None
185                                 return False
186
187                         self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
188                         try:
189                                 f = open(self.Filename + ".ts.meta", "w")
190                                 f.write(rec_ref.toString() + "\n")
191                                 f.write(self.name + "\n")
192                                 f.write(self.description + "\n")
193                                 f.write(str(self.begin) + "\n")
194                                 f.close()
195                         except IOError:
196                                 self.log(4, "failed to write meta information")
197                                 NavigationInstance.instance.stopRecordService(self.record_service)
198                                 self.record_service = None
199                                 return False
200                         return True
201
202         def do_backoff(self):
203                 if self.backoff == 0:
204                         self.backoff = 5
205                 else:
206                         self.backoff *= 2
207                         if self.backoff > 100:
208                                 self.backoff = 100
209                 self.log(10, "backoff: retry in %d seconds" % self.backoff)
210
211         def activate(self):
212                 next_state = self.state + 1
213                 self.log(5, "activating state %d" % next_state)
214                 
215                 if next_state == self.StatePrepared:
216                         if self.tryPrepare():
217                                 self.log(6, "prepare ok, waiting for begin")
218                                 # fine. it worked, resources are allocated.
219                                 self.next_activation = self.begin
220                                 self.backoff = 0
221                                 return True
222                         
223                         self.log(7, "prepare failed")
224                         if self.first_try_prepare:
225                                 self.first_try_prepare = False
226                                 if not config.recording.asktozap.value:
227                                         self.log(8, "asking user to zap away")
228                                         Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
229                                 else: # zap without asking
230                                         self.log(9, "zap without asking")
231                                         Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
232                                         self.failureCB(True)
233
234                         self.do_backoff()
235                         # retry
236                         self.start_prepare = time.time() + self.backoff
237                         return False
238                 elif next_state == self.StateRunning:
239                         # if this timer has been cancelled, just go to "end" state.
240                         if self.cancelled:
241                                 return True
242
243                         if self.justplay:
244                                 if Screens.Standby.inStandby:
245                                         self.log(11, "wakeup and zap")
246                                         #set service to zap after standby
247                                         Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
248                                         #wakeup standby
249                                         Screens.Standby.inStandby.Power()
250                                 else:
251                                         self.log(11, "zapping")
252                                         NavigationInstance.instance.playService(self.service_ref.ref)
253                                 return True
254                         else:
255                                 self.log(11, "start recording")
256                                 record_res = self.record_service.start()
257                                 
258                                 if record_res:
259                                         self.log(13, "start record returned %d" % record_res)
260                                         self.do_backoff()
261                                         # retry
262                                         self.begin = time.time() + self.backoff
263                                         return False
264
265                                 return True
266                 elif next_state == self.StateEnded:
267                         self.log(12, "stop recording")
268                         if not self.justplay:
269                                 NavigationInstance.instance.stopRecordService(self.record_service)
270                                 self.record_service = None
271                         if self.afterEvent == AFTEREVENT.STANDBY:
272                                 if not Screens.Standby.inStandby: # not already in standby
273                                         Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
274                         if self.afterEvent == AFTEREVENT.DEEPSTANDBY:
275                                 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
276                                         if Screens.Standby.inStandby: # not in standby
277                                                 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
278                                         else:
279                                                 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
280                         return True
281
282         def sendStandbyNotification(self, answer):
283                 if answer:
284                         Notifications.AddNotification(Screens.Standby.Standby)
285
286         def sendTryQuitMainloopNotification(self, answer):
287                 if answer:
288                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
289
290         def getNextActivation(self):
291                 if self.state == self.StateEnded:
292                         return self.end
293                 
294                 next_state = self.state + 1
295                 
296                 return {self.StatePrepared: self.start_prepare, 
297                                 self.StateRunning: self.begin, 
298                                 self.StateEnded: self.end }[next_state]
299
300         def failureCB(self, answer):
301                 if answer == True:
302                         self.log(13, "ok, zapped away")
303                         #NavigationInstance.instance.stopUserServices()
304                         NavigationInstance.instance.playService(self.service_ref.ref)
305                 else:
306                         self.log(14, "user didn't want to zap away, record will probably fail")
307
308         def timeChanged(self):
309                 old_prepare = self.start_prepare
310                 self.start_prepare = self.begin - self.prepare_time
311                 self.backoff = 0
312                 
313                 if int(old_prepare) != int(self.start_prepare):
314                         self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
315
316         def gotRecordEvent(self, record, event):
317                 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
318                 if self.__record_service.__deref__() != record.__deref__():
319                         return
320                 self.log(16, "record event %d" % event)
321                 if event == iRecordableService.evRecordWriteError:
322                         print "WRITE ERROR on recording, disk full?"
323                         # show notification. the 'id' will make sure that it will be
324                         # displayed only once, even if more timers are failing at the
325                         # same time. (which is very likely in case of disk fullness)
326                         Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
327                         # ok, the recording has been stopped. we need to properly note 
328                         # that in our state, with also keeping the possibility to re-try.
329                         # TODO: this has to be done.
330                 elif event == iRecordableService.evStart:
331                         # maybe this should be configurable?
332                         Notifications.AddPopup(text = _("A record has been started:\n%s") % self.name, type = MessageBox.TYPE_INFO, timeout = 3)
333
334         # we have record_service as property to automatically subscribe to record service events
335         def setRecordService(self, service):
336                 if self.__record_service is not None:
337                         print "[remove callback]"
338                         NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
339
340                 self.__record_service = service
341
342                 if self.__record_service is not None:
343                         print "[add callback]"
344                         NavigationInstance.instance.record_event.append(self.gotRecordEvent)
345
346         record_service = property(lambda self: self.__record_service, setRecordService)
347
348 def createTimer(xml):
349         begin = int(xml.getAttribute("begin"))
350         end = int(xml.getAttribute("end"))
351         serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
352         description = xml.getAttribute("description").encode("utf-8")
353         repeated = xml.getAttribute("repeated").encode("utf-8")
354         disabled = long(xml.getAttribute("disabled") or "0")
355         justplay = long(xml.getAttribute("justplay") or "0")
356         afterevent = str(xml.getAttribute("afterevent") or "nothing")
357         afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
358         if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
359                 eit = long(xml.getAttribute("eit"))
360         else:
361                 eit = None
362         
363         name = xml.getAttribute("name").encode("utf-8")
364         #filename = xml.getAttribute("filename").encode("utf-8")
365         entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent)
366         entry.repeated = int(repeated)
367         
368         for l in elementsWithTag(xml.childNodes, "log"):
369                 time = int(l.getAttribute("time"))
370                 code = int(l.getAttribute("code"))
371                 msg = mergeText(l.childNodes).strip().encode("utf-8")
372                 entry.log_entries.append((time, code, msg))
373         
374         return entry
375
376 class RecordTimer(timer.Timer):
377         def __init__(self):
378                 timer.Timer.__init__(self)
379                 
380                 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
381                 
382                 try:
383                         self.loadTimer()
384                 except IOError:
385                         print "unable to load timers from file!"
386                         
387         def isRecording(self):
388                 isRunning = False
389                 for timer in self.timer_list:
390                         if timer.isRunning() and not timer.justplay:
391                                 isRunning = True
392                 return isRunning
393         
394         def loadTimer(self):
395                 # TODO: PATH!
396                 try:
397                         doc = xml.dom.minidom.parse(self.Filename)
398                 except xml.parsers.expat.ExpatError:
399                         from Tools.Notifications import AddPopup
400                         from Screens.MessageBox import MessageBox
401
402                         AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
403
404                         print "timers.xml failed to load!"
405                         try:
406                                 import os
407                                 os.rename(self.Filename, self.Filename + "_old")
408                         except IOError:
409                                 print "renaming broken timer failed"
410                         return
411
412                 root = doc.childNodes[0]
413                 for timer in elementsWithTag(root.childNodes, "timer"):
414                         self.record(createTimer(timer))
415
416         def saveTimer(self):
417                 #doc = xml.dom.minidom.Document()
418                 #root_element = doc.createElement('timers')
419                 #doc.appendChild(root_element)
420                 #root_element.appendChild(doc.createTextNode("\n"))
421                 
422                 #for timer in self.timer_list + self.processed_timers:
423                         # some timers (instant records) don't want to be saved.
424                         # skip them
425                         #if timer.dontSave:
426                                 #continue
427                         #t = doc.createTextNode("\t")
428                         #root_element.appendChild(t)
429                         #t = doc.createElement('timer')
430                         #t.setAttribute("begin", str(int(timer.begin)))
431                         #t.setAttribute("end", str(int(timer.end)))
432                         #t.setAttribute("serviceref", str(timer.service_ref))
433                         #t.setAttribute("repeated", str(timer.repeated))                        
434                         #t.setAttribute("name", timer.name)
435                         #t.setAttribute("description", timer.description)
436                         #t.setAttribute("eit", str(timer.eit))
437                         
438                         #for time, code, msg in timer.log_entries:
439                                 #t.appendChild(doc.createTextNode("\t\t"))
440                                 #l = doc.createElement('log')
441                                 #l.setAttribute("time", str(time))
442                                 #l.setAttribute("code", str(code))
443                                 #l.appendChild(doc.createTextNode(msg))
444                                 #t.appendChild(l)
445                                 #t.appendChild(doc.createTextNode("\n"))
446
447                         #root_element.appendChild(t)
448                         #t = doc.createTextNode("\n")
449                         #root_element.appendChild(t)
450
451
452                 #file = open(self.Filename, "w")
453                 #doc.writexml(file)
454                 #file.write("\n")
455                 #file.close()
456
457                 list = []
458
459                 list.append('<?xml version="1.0" ?>\n')
460                 list.append('<timers>\n')
461                 
462                 for timer in self.timer_list + self.processed_timers:
463                         if timer.dontSave:
464                                 continue
465
466                         list.append('<timer')
467                         list.append(' begin="' + str(int(timer.begin)) + '"')
468                         list.append(' end="' + str(int(timer.end)) + '"')
469                         list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
470                         list.append(' repeated="' + str(int(timer.repeated)) + '"')
471                         list.append(' name="' + str(stringToXML(timer.name)) + '"')
472                         list.append(' description="' + str(stringToXML(timer.description)) + '"')
473                         list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
474                         if timer.eit is not None:
475                                 list.append(' eit="' + str(timer.eit) + '"')
476                         list.append(' disabled="' + str(int(timer.disabled)) + '"')
477                         list.append(' justplay="' + str(int(timer.justplay)) + '"')
478                         list.append('>\n')
479                         
480                         if config.recording.debug.value:
481                                 for time, code, msg in timer.log_entries:
482                                         list.append('<log')
483                                         list.append(' code="' + str(code) + '"')
484                                         list.append(' time="' + str(time) + '"')
485                                         list.append('>')
486                                         list.append(str(stringToXML(msg)))
487                                         list.append('</log>\n')
488                         
489                         list.append('</timer>\n')
490
491                 list.append('</timers>\n')
492
493                 file = open(self.Filename, "w")
494                 for x in list:
495                         file.write(x)
496                 file.close()
497
498         def getNextZapTime(self):
499                 now = time.time()
500                 for timer in self.timer_list:
501                         if not timer.justplay or timer.begin < now:
502                                 continue
503                         return timer.begin
504                 return -1
505
506         def getNextRecordingTime(self):
507                 now = time.time()
508                 for timer in self.timer_list:
509                         if timer.justplay or timer.begin < now:
510                                 continue
511                         return timer.begin
512                 return -1
513
514         def record(self, entry):
515                 entry.timeChanged()
516                 print "[Timer] Record " + str(entry)
517                 entry.Timer = self
518                 self.addTimerEntry(entry)
519                 self.saveTimer()
520                 
521         def isInTimer(self, eventid, begin, duration, service):
522                 time_match = 0
523                 chktime = None
524                 chktimecmp = None
525                 chktimecmp_end = None
526                 end = begin + duration
527                 for x in self.timer_list:
528                         check = x.service_ref.ref.toCompareString() == str(service)
529                         if not check:
530                                 sref = x.service_ref.ref
531                                 parent_sid = sref.getUnsignedData(5)
532                                 parent_tsid = sref.getUnsignedData(6)
533                                 if parent_sid and parent_tsid: # check for subservice
534                                         sid = sref.getUnsignedData(1)
535                                         tsid = sref.getUnsignedData(2)
536                                         sref.setUnsignedData(1, parent_sid)
537                                         sref.setUnsignedData(2, parent_tsid)
538                                         sref.setUnsignedData(5, 0)
539                                         sref.setUnsignedData(6, 0)
540                                         check = x.service_ref.ref.toCompareString() == str(service)
541                                         num = 0
542                                         if check:
543                                                 check = False
544                                                 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
545                                                 num = event and event.getNumOfLinkageServices() or 0
546                                         sref.setUnsignedData(1, sid)
547                                         sref.setUnsignedData(2, tsid)
548                                         sref.setUnsignedData(5, parent_sid)
549                                         sref.setUnsignedData(6, parent_tsid)
550                                         for cnt in range(num):
551                                                 subservice = event.getLinkageService(sref, cnt)
552                                                 if sref.toCompareString() == subservice.toCompareString():
553                                                         check = True
554                                                         break
555                         if check:
556                                 #if x.eit is not None and x.repeated == 0:
557                                 #       if x.eit == eventid:
558                                 #               return duration
559                                 if x.repeated != 0:
560                                         if chktime is None:
561                                                 chktime = localtime(begin)
562                                                 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
563                                                 chktimecmp_end = chktimecmp + (duration / 60)
564                                         time = localtime(x.begin)
565                                         for y in range(7):
566                                                 if x.repeated & (2 ** y):
567                                                         timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
568                                                         if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
569                                                                 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
570                                                         elif chktimecmp <= timecmp < chktimecmp_end:
571                                                                 time_match = (chktimecmp_end - timecmp) * 60
572                                 else: #if x.eit is None:
573                                         if begin <= x.begin <= end:
574                                                 diff = end - x.begin
575                                                 if time_match < diff:
576                                                         time_match = diff
577                                         elif x.begin <= begin <= x.end:
578                                                 diff = x.end - begin
579                                                 if time_match < diff:
580                                                         time_match = diff
581                 return time_match
582
583         def removeEntry(self, entry):
584                 print "[Timer] Remove " + str(entry)
585                 
586                 # avoid re-enqueuing
587                 entry.repeated = False
588
589                 # abort timer.
590                 # this sets the end time to current time, so timer will be stopped.
591                 entry.abort()
592                 
593                 if entry.state != entry.StateEnded:
594                         self.timeChanged(entry)
595                 
596                 print "state: ", entry.state
597                 print "in processed: ", entry in self.processed_timers
598                 print "in running: ", entry in self.timer_list
599                 # now the timer should be in the processed_timers list. remove it from there.
600                 self.processed_timers.remove(entry)
601                 self.saveTimer()
602
603         def shutdown(self):
604                 self.saveTimer()