take care of hotplug events just when the infobar is execing (no other menu
[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, dirname = None):
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                 self.dirname = dirname
118                 
119                 self.log_entries = []
120                 self.resetState()
121         
122         def log(self, code, msg):
123                 self.log_entries.append((int(time.time()), code, msg))
124                 print "[TIMER]", msg
125
126         def calculateFilename(self):
127                 service_name = self.service_ref.getServiceName()
128                 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
129                 
130                 print "begin_date: ", begin_date
131                 print "service_name: ", service_name
132                 print "name:", self.name
133                 print "description: ", self.description
134                 
135                 filename = begin_date + " - " + service_name
136                 if self.name:
137                         filename += " - " + self.name
138
139                 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
140                 self.log(0, "Filename calculated as: '%s'" % self.Filename)
141                 #begin_date + " - " + service_name + description)
142
143         def tryPrepare(self):
144                 if self.justplay:
145                         return True
146                 else:
147                         self.calculateFilename()
148                         rec_ref = self.service_ref and self.service_ref.ref
149                         if rec_ref and rec_ref.flags & eServiceReference.isGroup:
150                                 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
151                                 if not rec_ref:
152                                         self.log(1, "'get best playable service for group... record' failed")
153                                         return False
154                                 
155                         self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
156
157                         if not self.record_service:
158                                 self.log(1, "'record service' failed")
159                                 return False
160
161                         if self.repeated:
162                                 epgcache = eEPGCache.getInstance()
163                                 queryTime=self.begin+(self.end-self.begin)/2
164                                 evt = epgcache.lookupEventTime(rec_ref, queryTime)
165                                 if evt:
166                                         self.description = evt.getShortDescription()
167                                         event_id = evt.getEventId()
168                                 else:
169                                         event_id = -1
170                         else:
171                                 event_id = self.eit
172                                 if event_id is None:
173                                         event_id = -1
174
175                         prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
176                         if prep_res:
177                                 self.log(2, "'prepare' failed: error %d" % prep_res)
178                                 NavigationInstance.instance.stopRecordService(self.record_service)
179                                 self.record_service = None
180                                 return False
181
182                         self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
183                         try:
184                                 f = open(self.Filename + ".ts.meta", "w")
185                                 f.write(rec_ref.toString() + "\n")
186                                 f.write(self.name + "\n")
187                                 f.write(self.description + "\n")
188                                 f.write(str(self.begin) + "\n")
189                                 f.close()
190                         except IOError:
191                                 self.log(4, "failed to write meta information")
192                                 NavigationInstance.instance.stopRecordService(self.record_service)
193                                 self.record_service = None
194                                 return False
195                         return True
196
197         def do_backoff(self):
198                 if self.backoff == 0:
199                         self.backoff = 5
200                 else:
201                         self.backoff *= 2
202                         if self.backoff > 100:
203                                 self.backoff = 100
204                 self.log(10, "backoff: retry in %d seconds" % self.backoff)
205
206         def activate(self):
207                 next_state = self.state + 1
208                 self.log(5, "activating state %d" % next_state)
209                 
210                 if next_state == self.StatePrepared:
211                         if self.tryPrepare():
212                                 self.log(6, "prepare ok, waiting for begin")
213                                 # fine. it worked, resources are allocated.
214                                 self.next_activation = self.begin
215                                 self.backoff = 0
216                                 return True
217                         
218                         self.log(7, "prepare failed")
219                         if self.first_try_prepare:
220                                 self.first_try_prepare = False
221                                 if not config.recording.asktozap.value:
222                                         self.log(8, "asking user to zap away")
223                                         Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
224                                 else: # zap without asking
225                                         self.log(9, "zap without asking")
226                                         Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
227                                         self.failureCB(True)
228
229                         self.do_backoff()
230                         # retry
231                         self.start_prepare = time.time() + self.backoff
232                         return False
233                 elif next_state == self.StateRunning:
234                         # if this timer has been cancelled, just go to "end" state.
235                         if self.cancelled:
236                                 return True
237
238                         if self.justplay:
239                                 if Screens.Standby.inStandby:
240                                         self.log(11, "wakeup and zap")
241                                         #set service to zap after standby
242                                         Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
243                                         #wakeup standby
244                                         Screens.Standby.inStandby.Power()
245                                 else:
246                                         self.log(11, "zapping")
247                                         NavigationInstance.instance.playService(self.service_ref.ref)
248                                 return True
249                         else:
250                                 self.log(11, "start recording")
251                                 record_res = self.record_service.start()
252                                 
253                                 if record_res:
254                                         self.log(13, "start record returned %d" % record_res)
255                                         self.do_backoff()
256                                         # retry
257                                         self.begin = time.time() + self.backoff
258                                         return False
259
260                                 return True
261                 elif next_state == self.StateEnded:
262                         self.log(12, "stop recording")
263                         if not self.justplay:
264                                 NavigationInstance.instance.stopRecordService(self.record_service)
265                                 self.record_service = None
266                         if self.afterEvent == AFTEREVENT.STANDBY:
267                                 if not Screens.Standby.inStandby: # not already in standby
268                                         Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
269                         if self.afterEvent == AFTEREVENT.DEEPSTANDBY:
270                                 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
271                                         if Screens.Standby.inStandby: # not in standby
272                                                 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
273                                         else:
274                                                 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
275                         return True
276
277         def sendStandbyNotification(self, answer):
278                 if answer:
279                         Notifications.AddNotification(Screens.Standby.Standby)
280
281         def sendTryQuitMainloopNotification(self, answer):
282                 if answer:
283                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
284
285         def getNextActivation(self):
286                 if self.state == self.StateEnded:
287                         return self.end
288                 
289                 next_state = self.state + 1
290                 
291                 return {self.StatePrepared: self.start_prepare, 
292                                 self.StateRunning: self.begin, 
293                                 self.StateEnded: self.end }[next_state]
294
295         def failureCB(self, answer):
296                 if answer == True:
297                         self.log(13, "ok, zapped away")
298                         #NavigationInstance.instance.stopUserServices()
299                         NavigationInstance.instance.playService(self.service_ref.ref)
300                 else:
301                         self.log(14, "user didn't want to zap away, record will probably fail")
302
303         def timeChanged(self):
304                 old_prepare = self.start_prepare
305                 self.start_prepare = self.begin - self.prepare_time
306                 self.backoff = 0
307                 
308                 if int(old_prepare) != int(self.start_prepare):
309                         self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
310
311         def gotRecordEvent(self, record, event):
312                 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
313                 if self.__record_service.__deref__() != record.__deref__():
314                         return
315                 self.log(16, "record event %d" % event)
316                 if event == iRecordableService.evRecordWriteError:
317                         print "WRITE ERROR on recording, disk full?"
318                         # show notification. the 'id' will make sure that it will be
319                         # displayed only once, even if more timers are failing at the
320                         # same time. (which is very likely in case of disk fullness)
321                         Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
322                         # ok, the recording has been stopped. we need to properly note 
323                         # that in our state, with also keeping the possibility to re-try.
324                         # TODO: this has to be done.
325                 elif event == iRecordableService.evStart:
326                         # maybe this should be configurable?
327                         Notifications.AddPopup(text = _("A record has been started:\n%s") % self.name, type = MessageBox.TYPE_INFO, timeout = 3)
328
329         # we have record_service as property to automatically subscribe to record service events
330         def setRecordService(self, service):
331                 if self.__record_service is not None:
332                         print "[remove callback]"
333                         NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
334
335                 self.__record_service = service
336
337                 if self.__record_service is not None:
338                         print "[add callback]"
339                         NavigationInstance.instance.record_event.append(self.gotRecordEvent)
340
341         record_service = property(lambda self: self.__record_service, setRecordService)
342
343 def createTimer(xml):
344         begin = int(xml.getAttribute("begin"))
345         end = int(xml.getAttribute("end"))
346         serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
347         description = xml.getAttribute("description").encode("utf-8")
348         repeated = xml.getAttribute("repeated").encode("utf-8")
349         disabled = long(xml.getAttribute("disabled") or "0")
350         justplay = long(xml.getAttribute("justplay") or "0")
351         afterevent = str(xml.getAttribute("afterevent") or "nothing")
352         afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
353         if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
354                 eit = long(xml.getAttribute("eit"))
355         else:
356                 eit = None
357         if xml.hasAttribute("location") and xml.getAttribute("location") != "None":
358                 location = str(xml.getAttribute("location")).encode("utf-8")
359         else:
360                 location = None
361
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, dirname = location)
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                         if timer.dirname is not None:
477                                 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
478                         list.append(' disabled="' + str(int(timer.disabled)) + '"')
479                         list.append(' justplay="' + str(int(timer.justplay)) + '"')
480                         list.append('>\n')
481                         
482                         if config.recording.debug.value:
483                                 for time, code, msg in timer.log_entries:
484                                         list.append('<log')
485                                         list.append(' code="' + str(code) + '"')
486                                         list.append(' time="' + str(time) + '"')
487                                         list.append('>')
488                                         list.append(str(stringToXML(msg)))
489                                         list.append('</log>\n')
490                         
491                         list.append('</timer>\n')
492
493                 list.append('</timers>\n')
494
495                 file = open(self.Filename, "w")
496                 for x in list:
497                         file.write(x)
498                 file.close()
499
500         def getNextZapTime(self):
501                 now = time.time()
502                 for timer in self.timer_list:
503                         if not timer.justplay or timer.begin < now:
504                                 continue
505                         return timer.begin
506                 return -1
507
508         def getNextRecordingTime(self):
509                 now = time.time()
510                 for timer in self.timer_list:
511                         if timer.justplay or timer.begin < now:
512                                 continue
513                         return timer.begin
514                 return -1
515
516         def record(self, entry):
517                 entry.timeChanged()
518                 print "[Timer] Record " + str(entry)
519                 entry.Timer = self
520                 self.addTimerEntry(entry)
521                 self.saveTimer()
522                 
523         def isInTimer(self, eventid, begin, duration, service):
524                 time_match = 0
525                 chktime = None
526                 chktimecmp = None
527                 chktimecmp_end = None
528                 end = begin + duration
529                 for x in self.timer_list:
530                         check = x.service_ref.ref.toCompareString() == str(service)
531                         if not check:
532                                 sref = x.service_ref.ref
533                                 parent_sid = sref.getUnsignedData(5)
534                                 parent_tsid = sref.getUnsignedData(6)
535                                 if parent_sid and parent_tsid: # check for subservice
536                                         sid = sref.getUnsignedData(1)
537                                         tsid = sref.getUnsignedData(2)
538                                         sref.setUnsignedData(1, parent_sid)
539                                         sref.setUnsignedData(2, parent_tsid)
540                                         sref.setUnsignedData(5, 0)
541                                         sref.setUnsignedData(6, 0)
542                                         check = x.service_ref.ref.toCompareString() == str(service)
543                                         num = 0
544                                         if check:
545                                                 check = False
546                                                 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
547                                                 num = event and event.getNumOfLinkageServices() or 0
548                                         sref.setUnsignedData(1, sid)
549                                         sref.setUnsignedData(2, tsid)
550                                         sref.setUnsignedData(5, parent_sid)
551                                         sref.setUnsignedData(6, parent_tsid)
552                                         for cnt in range(num):
553                                                 subservice = event.getLinkageService(sref, cnt)
554                                                 if sref.toCompareString() == subservice.toCompareString():
555                                                         check = True
556                                                         break
557                         if check:
558                                 #if x.eit is not None and x.repeated == 0:
559                                 #       if x.eit == eventid:
560                                 #               return duration
561                                 if x.repeated != 0:
562                                         if chktime is None:
563                                                 chktime = localtime(begin)
564                                                 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
565                                                 chktimecmp_end = chktimecmp + (duration / 60)
566                                         time = localtime(x.begin)
567                                         for y in range(7):
568                                                 if x.repeated & (2 ** y):
569                                                         timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
570                                                         if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
571                                                                 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
572                                                         elif chktimecmp <= timecmp < chktimecmp_end:
573                                                                 time_match = (chktimecmp_end - timecmp) * 60
574                                 else: #if x.eit is None:
575                                         if begin <= x.begin <= end:
576                                                 diff = end - x.begin
577                                                 if time_match < diff:
578                                                         time_match = diff
579                                         elif x.begin <= begin <= x.end:
580                                                 diff = x.end - begin
581                                                 if time_match < diff:
582                                                         time_match = diff
583                 return time_match
584
585         def removeEntry(self, entry):
586                 print "[Timer] Remove " + str(entry)
587                 
588                 # avoid re-enqueuing
589                 entry.repeated = False
590
591                 # abort timer.
592                 # this sets the end time to current time, so timer will be stopped.
593                 entry.abort()
594                 
595                 if entry.state != entry.StateEnded:
596                         self.timeChanged(entry)
597                 
598                 print "state: ", entry.state
599                 print "in processed: ", entry in self.processed_timers
600                 print "in running: ", entry in self.timer_list
601                 # now the timer should be in the processed_timers list. remove it from there.
602                 self.processed_timers.remove(entry)
603                 self.saveTimer()
604
605         def shutdown(self):
606                 self.saveTimer()