2 #from time import datetime
3 from Tools import Directories, Notifications
5 from Components.config import config
9 from enigma import eEPGCache, getBestPlayableServiceReference, \
10 eServiceReference, iRecordableService, quitMainloop
12 from Screens.MessageBox import MessageBox
13 from Components.TimerSanityCheck import TimerSanityCheck
14 import NavigationInstance
16 import Screens.Standby
18 from time import localtime
20 from Tools.XMLTools import elementsWithTag, mergeText, stringToXML
21 from ServiceReference import ServiceReference
23 # ok, for descriptions etc we have:
24 # service reference (to get the service name)
26 # description (description)
27 # event data (ONLY for time adjustments etc.)
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):
34 name = ev.getEventName()
35 description = ev.getShortDescription()
39 begin = ev.getBeginTime()
40 end = begin + ev.getDuration()
42 begin -= config.recording.margin_before.value * 60
43 end += config.recording.margin_after.value * 60
44 return (begin, end, name, description, eit)
52 # please do not translate log messages
53 class RecordTimerEntry(timer.TimerEntry, object):
54 ######### the following static methods and members are only in use when the box is in (soft) standby
55 receiveRecordEvents = False
62 def staticGotRecordEvent(recservice, event):
63 if event == iRecordableService.evEnd:
64 print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
65 recordings = NavigationInstance.instance.getRecordings()
66 if not len(recordings): # no more recordings exist
67 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
68 if rec_time > 0 and (rec_time - time.time()) < 360:
69 print "another recording starts in", rec_time - time.time(), "seconds... do not shutdown yet"
71 print "no starting records in the next 360 seconds... immediate shutdown"
72 RecordTimerEntry.shutdown() # immediate shutdown
73 elif event == iRecordableService.evStart:
74 print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
77 def stopTryQuitMainloop():
78 print "RecordTimer.stopTryQuitMainloop"
79 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
80 RecordTimerEntry.receiveRecordEvents = False
83 def TryQuitMainloop(default_yes = True):
84 if not RecordTimerEntry.receiveRecordEvents:
85 print "RecordTimer.TryQuitMainloop"
86 NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
87 RecordTimerEntry.receiveRecordEvents = True
88 # send fake event.. to check if another recordings are running or
89 # other timers start in a few seconds
90 RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
91 # send normal notification for the case the user leave the standby now..
92 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop, default_yes = default_yes)
93 #################################################################
95 def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.AUTO, checkOldTimers = False, dirname = None, tags = None):
96 timer.TimerEntry.__init__(self, int(begin), int(end))
98 if checkOldTimers == True:
99 if self.begin < time.time() - 1209600:
100 self.begin = int(time.time())
102 if self.end < self.begin:
103 self.end = self.begin
105 assert isinstance(serviceref, ServiceReference)
107 self.service_ref = serviceref
109 self.dontSave = False
111 self.description = description
112 self.disabled = disabled
114 self.__record_service = None
115 self.start_prepare = 0
116 self.justplay = justplay
117 self.afterEvent = afterEvent
118 self.dirname = dirname
119 self.dirnameHadToFallback = False
120 self.autoincrease = False
121 self.tags = tags or []
123 self.log_entries = []
126 def log(self, code, msg):
127 self.log_entries.append((int(time.time()), code, msg))
130 def calculateFilename(self):
131 service_name = self.service_ref.getServiceName()
132 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
134 print "begin_date: ", begin_date
135 print "service_name: ", service_name
136 print "name:", self.name
137 print "description: ", self.description
139 filename = begin_date + " - " + service_name
141 filename += " - " + self.name
143 if self.dirname and not Directories.pathExists(self.dirname):
144 self.dirnameHadToFallback = True
145 self.Filename = Directories.getRecordingFilename(filename, None)
147 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
148 self.log(0, "Filename calculated as: '%s'" % self.Filename)
149 #begin_date + " - " + service_name + description)
151 def tryPrepare(self):
155 self.calculateFilename()
156 rec_ref = self.service_ref and self.service_ref.ref
157 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
158 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
160 self.log(1, "'get best playable service for group... record' failed")
163 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
165 if not self.record_service:
166 self.log(1, "'record service' failed")
170 epgcache = eEPGCache.getInstance()
171 queryTime=self.begin+(self.end-self.begin)/2
172 evt = epgcache.lookupEventTime(rec_ref, queryTime)
174 self.description = evt.getShortDescription()
175 event_id = evt.getEventId()
183 prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
185 self.log(2, "'prepare' failed: error %d" % prep_res)
186 NavigationInstance.instance.stopRecordService(self.record_service)
187 self.record_service = None
190 self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
192 f = open(self.Filename + ".ts.meta", "w")
193 f.write(rec_ref.toString() + "\n")
194 f.write(self.name + "\n")
195 f.write(self.description + "\n")
196 f.write(str(self.begin) + "\n")
197 f.write(' '.join(self.tags))
200 self.log(4, "failed to write meta information")
201 NavigationInstance.instance.stopRecordService(self.record_service)
202 self.record_service = None
206 def do_backoff(self):
207 if self.backoff == 0:
211 if self.backoff > 100:
213 self.log(10, "backoff: retry in %d seconds" % self.backoff)
216 next_state = self.state + 1
217 self.log(5, "activating state %d" % next_state)
219 if next_state == self.StatePrepared:
220 if self.tryPrepare():
221 self.log(6, "prepare ok, waiting for begin")
222 # fine. it worked, resources are allocated.
223 self.next_activation = self.begin
227 self.log(7, "prepare failed")
228 if self.first_try_prepare:
229 self.first_try_prepare = False
230 if not config.recording.asktozap.value:
231 self.log(8, "asking user to zap away")
232 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
233 else: # zap without asking
234 self.log(9, "zap without asking")
235 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
240 self.start_prepare = time.time() + self.backoff
242 elif next_state == self.StateRunning:
243 # if this timer has been cancelled, just go to "end" state.
248 if Screens.Standby.inStandby:
249 self.log(11, "wakeup and zap")
250 #set service to zap after standby
251 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
253 Screens.Standby.inStandby.Power()
255 self.log(11, "zapping")
256 NavigationInstance.instance.playService(self.service_ref.ref)
259 self.log(11, "start recording")
260 record_res = self.record_service.start()
263 self.log(13, "start record returned %d" % record_res)
266 self.begin = time.time() + self.backoff
270 elif next_state == self.StateEnded:
271 self.log(12, "stop recording")
272 if not self.justplay:
273 NavigationInstance.instance.stopRecordService(self.record_service)
274 self.record_service = None
275 if self.afterEvent == AFTEREVENT.STANDBY:
276 if not Screens.Standby.inStandby: # not already in standby
277 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
278 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
279 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
280 if Screens.Standby.inStandby: # in standby
281 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
283 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
286 def sendStandbyNotification(self, answer):
288 Notifications.AddNotification(Screens.Standby.Standby)
290 def sendTryQuitMainloopNotification(self, answer):
292 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
294 def getNextActivation(self):
295 if self.state == self.StateEnded:
298 next_state = self.state + 1
300 return {self.StatePrepared: self.start_prepare,
301 self.StateRunning: self.begin,
302 self.StateEnded: self.end }[next_state]
304 def failureCB(self, answer):
306 self.log(13, "ok, zapped away")
307 #NavigationInstance.instance.stopUserServices()
308 NavigationInstance.instance.playService(self.service_ref.ref)
310 self.log(14, "user didn't want to zap away, record will probably fail")
312 def timeChanged(self):
313 old_prepare = self.start_prepare
314 self.start_prepare = self.begin - self.prepare_time
317 if int(old_prepare) != int(self.start_prepare):
318 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
320 def gotRecordEvent(self, record, event):
321 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
322 if self.__record_service.__deref__() != record.__deref__():
324 self.log(16, "record event %d" % event)
325 if event == iRecordableService.evRecordWriteError:
326 print "WRITE ERROR on recording, disk full?"
327 # show notification. the 'id' will make sure that it will be
328 # displayed only once, even if more timers are failing at the
329 # same time. (which is very likely in case of disk fullness)
330 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
331 # ok, the recording has been stopped. we need to properly note
332 # that in our state, with also keeping the possibility to re-try.
333 # TODO: this has to be done.
334 elif event == iRecordableService.evStart:
335 text = _("A record has been started:\n%s") % self.name
336 if self.dirnameHadToFallback:
337 text = '\n'.join([text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")])
339 # maybe this should be configurable?
340 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
342 # we have record_service as property to automatically subscribe to record service events
343 def setRecordService(self, service):
344 if self.__record_service is not None:
345 print "[remove callback]"
346 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
348 self.__record_service = service
350 if self.__record_service is not None:
351 print "[add callback]"
352 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
354 record_service = property(lambda self: self.__record_service, setRecordService)
356 def createTimer(xml):
357 begin = int(xml.getAttribute("begin"))
358 end = int(xml.getAttribute("end"))
359 serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
360 description = xml.getAttribute("description").encode("utf-8")
361 repeated = xml.getAttribute("repeated").encode("utf-8")
362 disabled = long(xml.getAttribute("disabled") or "0")
363 justplay = long(xml.getAttribute("justplay") or "0")
364 afterevent = str(xml.getAttribute("afterevent") or "nothing")
366 "nothing": AFTEREVENT.NONE,
367 "standby": AFTEREVENT.STANDBY,
368 "deepstandby": AFTEREVENT.DEEPSTANDBY,
369 "auto": AFTEREVENT.AUTO
371 if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
372 eit = long(xml.getAttribute("eit"))
375 if xml.hasAttribute("location") and xml.getAttribute("location") != "None":
376 location = xml.getAttribute("location").encode("utf-8")
379 if xml.hasAttribute("tags") and xml.getAttribute("tags"):
380 tags = xml.getAttribute("tags").encode("utf-8").split(' ')
384 name = xml.getAttribute("name").encode("utf-8")
385 #filename = xml.getAttribute("filename").encode("utf-8")
386 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
387 entry.repeated = int(repeated)
389 for l in elementsWithTag(xml.childNodes, "log"):
390 time = int(l.getAttribute("time"))
391 code = int(l.getAttribute("code"))
392 msg = mergeText(l.childNodes).strip().encode("utf-8")
393 entry.log_entries.append((time, code, msg))
397 class RecordTimer(timer.Timer):
399 timer.Timer.__init__(self)
401 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
406 print "unable to load timers from file!"
408 def isRecording(self):
410 for timer in self.timer_list:
411 if timer.isRunning() and not timer.justplay:
418 doc = xml.dom.minidom.parse(self.Filename)
419 except xml.parsers.expat.ExpatError:
420 from Tools.Notifications import AddPopup
421 from Screens.MessageBox import MessageBox
423 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
425 print "timers.xml failed to load!"
428 os.rename(self.Filename, self.Filename + "_old")
430 print "renaming broken timer failed"
433 root = doc.childNodes[0]
435 # put out a message when at least one timer overlaps
437 for timer in elementsWithTag(root.childNodes, "timer"):
438 newTimer = createTimer(timer)
439 if (self.record(newTimer, True, True) is not None) and (checkit == True):
440 from Tools.Notifications import AddPopup
441 from Screens.MessageBox import MessageBox
442 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
443 checkit = False # at moment it is enough when the message is displayed one time
446 #doc = xml.dom.minidom.Document()
447 #root_element = doc.createElement('timers')
448 #doc.appendChild(root_element)
449 #root_element.appendChild(doc.createTextNode("\n"))
451 #for timer in self.timer_list + self.processed_timers:
452 # some timers (instant records) don't want to be saved.
456 #t = doc.createTextNode("\t")
457 #root_element.appendChild(t)
458 #t = doc.createElement('timer')
459 #t.setAttribute("begin", str(int(timer.begin)))
460 #t.setAttribute("end", str(int(timer.end)))
461 #t.setAttribute("serviceref", str(timer.service_ref))
462 #t.setAttribute("repeated", str(timer.repeated))
463 #t.setAttribute("name", timer.name)
464 #t.setAttribute("description", timer.description)
465 #t.setAttribute("eit", str(timer.eit))
467 #for time, code, msg in timer.log_entries:
468 #t.appendChild(doc.createTextNode("\t\t"))
469 #l = doc.createElement('log')
470 #l.setAttribute("time", str(time))
471 #l.setAttribute("code", str(code))
472 #l.appendChild(doc.createTextNode(msg))
474 #t.appendChild(doc.createTextNode("\n"))
476 #root_element.appendChild(t)
477 #t = doc.createTextNode("\n")
478 #root_element.appendChild(t)
481 #file = open(self.Filename, "w")
488 list.append('<?xml version="1.0" ?>\n')
489 list.append('<timers>\n')
491 for timer in self.timer_list + self.processed_timers:
495 list.append('<timer')
496 list.append(' begin="' + str(int(timer.begin)) + '"')
497 list.append(' end="' + str(int(timer.end)) + '"')
498 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
499 list.append(' repeated="' + str(int(timer.repeated)) + '"')
500 list.append(' name="' + str(stringToXML(timer.name)) + '"')
501 list.append(' description="' + str(stringToXML(timer.description)) + '"')
502 list.append(' afterevent="' + str(stringToXML({
503 AFTEREVENT.NONE: "nothing",
504 AFTEREVENT.STANDBY: "standby",
505 AFTEREVENT.DEEPSTANDBY: "deepstandby",
506 AFTEREVENT.AUTO: "auto"
507 }[timer.afterEvent])) + '"')
508 if timer.eit is not None:
509 list.append(' eit="' + str(timer.eit) + '"')
510 if timer.dirname is not None:
511 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
512 if timer.tags is not None:
513 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
514 list.append(' disabled="' + str(int(timer.disabled)) + '"')
515 list.append(' justplay="' + str(int(timer.justplay)) + '"')
518 if config.recording.debug.value:
519 for time, code, msg in timer.log_entries:
521 list.append(' code="' + str(code) + '"')
522 list.append(' time="' + str(time) + '"')
524 list.append(str(stringToXML(msg)))
525 list.append('</log>\n')
527 list.append('</timer>\n')
529 list.append('</timers>\n')
531 file = open(self.Filename, "w")
536 def getNextZapTime(self):
538 for timer in self.timer_list:
539 if not timer.justplay or timer.begin < now:
544 def getNextRecordingTime(self):
546 for timer in self.timer_list:
547 if timer.justplay or timer.begin < now:
552 def isNextRecordAfterEventActionAuto(self):
555 for timer in self.timer_list:
556 if timer.justplay or timer.begin < now:
558 if t is None or t.begin == timer.begin:
560 if t.afterEvent == AFTEREVENT.AUTO:
564 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
565 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
566 if not timersanitycheck.check():
567 if ignoreTSC != True:
568 print "timer conflict detected!"
569 print timersanitycheck.getSimulTimerList()
570 return timersanitycheck.getSimulTimerList()
572 print "ignore timer conflict"
573 elif timersanitycheck.doubleCheck():
574 print "ignore double timer"
576 print "[Timer] Record " + str(entry)
578 self.addTimerEntry(entry)
583 def isInTimer(self, eventid, begin, duration, service):
587 chktimecmp_end = None
588 end = begin + duration
589 for x in self.timer_list:
590 check = x.service_ref.ref.toCompareString() == str(service)
592 sref = x.service_ref.ref
593 parent_sid = sref.getUnsignedData(5)
594 parent_tsid = sref.getUnsignedData(6)
595 if parent_sid and parent_tsid: # check for subservice
596 sid = sref.getUnsignedData(1)
597 tsid = sref.getUnsignedData(2)
598 sref.setUnsignedData(1, parent_sid)
599 sref.setUnsignedData(2, parent_tsid)
600 sref.setUnsignedData(5, 0)
601 sref.setUnsignedData(6, 0)
602 check = x.service_ref.ref.toCompareString() == str(service)
606 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
607 num = event and event.getNumOfLinkageServices() or 0
608 sref.setUnsignedData(1, sid)
609 sref.setUnsignedData(2, tsid)
610 sref.setUnsignedData(5, parent_sid)
611 sref.setUnsignedData(6, parent_tsid)
612 for cnt in range(num):
613 subservice = event.getLinkageService(sref, cnt)
614 if sref.toCompareString() == subservice.toCompareString():
618 #if x.eit is not None and x.repeated == 0:
619 # if x.eit == eventid:
623 chktime = localtime(begin)
624 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
625 chktimecmp_end = chktimecmp + (duration / 60)
626 time = localtime(x.begin)
628 if x.repeated & (2 ** y):
629 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
630 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
631 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
632 elif chktimecmp <= timecmp < chktimecmp_end:
633 time_match = (chktimecmp_end - timecmp) * 60
634 else: #if x.eit is None:
635 if begin <= x.begin <= end:
637 if time_match < diff:
639 elif x.begin <= begin <= x.end:
641 if time_match < diff:
645 def removeEntry(self, entry):
646 print "[Timer] Remove " + str(entry)
649 entry.repeated = False
652 # this sets the end time to current time, so timer will be stopped.
655 if entry.state != entry.StateEnded:
656 self.timeChanged(entry)
658 print "state: ", entry.state
659 print "in processed: ", entry in self.processed_timers
660 print "in running: ", entry in self.timer_list
661 # now the timer should be in the processed_timers list. remove it from there.
662 self.processed_timers.remove(entry)