2 #from time import datetime
3 from Tools import Directories, Notifications
5 from Components.config import config
7 import xml.etree.cElementTree
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 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 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.autoincreasetime = 3600 * 24 # 1 day
122 self.tags = tags or []
124 self.log_entries = []
127 def log(self, code, msg):
128 self.log_entries.append((int(time.time()), code, msg))
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))
135 print "begin_date: ", begin_date
136 print "service_name: ", service_name
137 print "name:", self.name
138 print "description: ", self.description
140 filename = begin_date + " - " + service_name
142 filename += " - " + self.name
144 if self.dirname and not Directories.fileExists(self.dirname, 'w'):
145 self.dirnameHadToFallback = True
146 self.Filename = Directories.getRecordingFilename(filename, None)
148 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
149 self.log(0, "Filename calculated as: '%s'" % self.Filename)
150 #begin_date + " - " + service_name + description)
152 def tryPrepare(self):
156 self.calculateFilename()
157 rec_ref = self.service_ref and self.service_ref.ref
158 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
159 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
161 self.log(1, "'get best playable service for group... record' failed")
164 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
166 if not self.record_service:
167 self.log(1, "'record service' failed")
171 epgcache = eEPGCache.getInstance()
172 queryTime=self.begin+(self.end-self.begin)/2
173 evt = epgcache.lookupEventTime(rec_ref, queryTime)
175 self.description = evt.getShortDescription()
176 event_id = evt.getEventId()
184 prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id, self.name.replace("\n", ""), self.description.replace("\n", ""), ' '.join(self.tags))
187 self.log(4, "failed to write meta information")
189 self.log(2, "'prepare' failed: error %d" % prep_res)
190 NavigationInstance.instance.stopRecordService(self.record_service)
191 self.record_service = None
195 def do_backoff(self):
196 if self.backoff == 0:
200 if self.backoff > 100:
202 self.log(10, "backoff: retry in %d seconds" % self.backoff)
205 next_state = self.state + 1
206 self.log(5, "activating state %d" % next_state)
208 if next_state == self.StatePrepared:
209 if self.tryPrepare():
210 self.log(6, "prepare ok, waiting for begin")
211 # fine. it worked, resources are allocated.
212 self.next_activation = self.begin
216 self.log(7, "prepare failed")
217 if self.first_try_prepare:
218 self.first_try_prepare = False
219 if not config.recording.asktozap.value:
220 self.log(8, "asking user to zap away")
221 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
222 else: # zap without asking
223 self.log(9, "zap without asking")
224 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
229 self.start_prepare = time.time() + self.backoff
231 elif next_state == self.StateRunning:
232 # if this timer has been cancelled, just go to "end" state.
237 if Screens.Standby.inStandby:
238 self.log(11, "wakeup and zap")
239 #set service to zap after standby
240 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
242 Screens.Standby.inStandby.Power()
244 self.log(11, "zapping")
245 NavigationInstance.instance.playService(self.service_ref.ref)
248 self.log(11, "start recording")
249 record_res = self.record_service.start()
252 self.log(13, "start record returned %d" % record_res)
255 self.begin = time.time() + self.backoff
259 elif next_state == self.StateEnded:
261 if self.setAutoincreaseEnd():
262 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
265 self.log(12, "stop recording")
266 if not self.justplay:
267 NavigationInstance.instance.stopRecordService(self.record_service)
268 self.record_service = None
269 if self.afterEvent == AFTEREVENT.STANDBY:
270 if not Screens.Standby.inStandby: # not already in standby
271 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
272 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
273 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
274 if Screens.Standby.inStandby: # in standby
275 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
277 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
280 def setAutoincreaseEnd(self, entry = None):
281 if not self.autoincrease:
284 new_end = int(time.time()) + self.autoincreasetime
286 new_end = entry.begin -30
288 dummyentry = RecordTimerEntry(self.service_ref, self.begin, new_end, self.name, self.description, self.eit, disabled=True, justplay = self.justplay, afterEvent = self.afterEvent, dirname = self.dirname, tags = self.tags)
289 dummyentry.disabled = self.disabled
290 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
291 if not timersanitycheck.check():
292 simulTimerList = timersanitycheck.getSimulTimerList()
293 new_end = simulTimerList[1].begin
295 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
297 if new_end <= time.time():
303 def sendStandbyNotification(self, answer):
305 Notifications.AddNotification(Screens.Standby.Standby)
307 def sendTryQuitMainloopNotification(self, answer):
309 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
311 def getNextActivation(self):
312 if self.state == self.StateEnded:
315 next_state = self.state + 1
317 return {self.StatePrepared: self.start_prepare,
318 self.StateRunning: self.begin,
319 self.StateEnded: self.end }[next_state]
321 def failureCB(self, answer):
323 self.log(13, "ok, zapped away")
324 #NavigationInstance.instance.stopUserServices()
325 NavigationInstance.instance.playService(self.service_ref.ref)
327 self.log(14, "user didn't want to zap away, record will probably fail")
329 def timeChanged(self):
330 old_prepare = self.start_prepare
331 self.start_prepare = self.begin - self.prepare_time
334 if int(old_prepare) != int(self.start_prepare):
335 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
337 def gotRecordEvent(self, record, event):
338 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
339 if self.__record_service.__deref__() != record.__deref__():
341 self.log(16, "record event %d" % event)
342 if event == iRecordableService.evRecordWriteError:
343 print "WRITE ERROR on recording, disk full?"
344 # show notification. the 'id' will make sure that it will be
345 # displayed only once, even if more timers are failing at the
346 # same time. (which is very likely in case of disk fullness)
347 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
348 # ok, the recording has been stopped. we need to properly note
349 # that in our state, with also keeping the possibility to re-try.
350 # TODO: this has to be done.
351 elif event == iRecordableService.evStart:
352 text = _("A record has been started:\n%s") % self.name
353 if self.dirnameHadToFallback:
354 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
356 # maybe this should be configurable?
357 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
359 # we have record_service as property to automatically subscribe to record service events
360 def setRecordService(self, service):
361 if self.__record_service is not None:
362 print "[remove callback]"
363 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
365 self.__record_service = service
367 if self.__record_service is not None:
368 print "[add callback]"
369 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
371 record_service = property(lambda self: self.__record_service, setRecordService)
373 def createTimer(xml):
374 begin = int(xml.get("begin"))
375 end = int(xml.get("end"))
376 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
377 description = xml.get("description").encode("utf-8")
378 repeated = xml.get("repeated").encode("utf-8")
379 disabled = long(xml.get("disabled") or "0")
380 justplay = long(xml.get("justplay") or "0")
381 afterevent = str(xml.get("afterevent") or "nothing")
383 "nothing": AFTEREVENT.NONE,
384 "standby": AFTEREVENT.STANDBY,
385 "deepstandby": AFTEREVENT.DEEPSTANDBY,
386 "auto": AFTEREVENT.AUTO
389 if eit and eit != "None":
393 location = xml.get("location")
394 if location and location != "None":
395 location = location.encode("utf-8")
398 tags = xml.get("tags")
399 if tags and tags != "None":
400 tags = tags.encode("utf-8").split(' ')
404 name = xml.get("name").encode("utf-8")
405 #filename = xml.get("filename").encode("utf-8")
406 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
407 entry.repeated = int(repeated)
409 for l in xml.findall("log"):
410 time = int(l.get("time"))
411 code = int(l.get("code"))
412 msg = l.text.strip().encode("utf-8")
413 entry.log_entries.append((time, code, msg))
417 class RecordTimer(timer.Timer):
419 timer.Timer.__init__(self)
421 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
426 print "unable to load timers from file!"
428 def isRecording(self):
430 for timer in self.timer_list:
431 if timer.isRunning() and not timer.justplay:
438 doc = xml.etree.cElementTree.parse(self.Filename)
440 from Tools.Notifications import AddPopup
441 from Screens.MessageBox import MessageBox
443 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
445 print "timers.xml failed to load!"
448 os.rename(self.Filename, self.Filename + "_old")
449 except (IOError, OSError):
450 print "renaming broken timer failed"
453 print "timers.xml not found!"
458 # put out a message when at least one timer overlaps
460 for timer in root.findall("timer"):
461 newTimer = createTimer(timer)
462 if (self.record(newTimer, True, True) is not None) and (checkit == True):
463 from Tools.Notifications import AddPopup
464 from Screens.MessageBox import MessageBox
465 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
466 checkit = False # at moment it is enough when the message is displayed one time
469 #root_element = xml.etree.cElementTree.Element('timers')
470 #root_element.text = "\n"
472 #for timer in self.timer_list + self.processed_timers:
473 # some timers (instant records) don't want to be saved.
477 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
478 #t.set("begin", str(int(timer.begin)))
479 #t.set("end", str(int(timer.end)))
480 #t.set("serviceref", str(timer.service_ref))
481 #t.set("repeated", str(timer.repeated))
482 #t.set("name", timer.name)
483 #t.set("description", timer.description)
484 #t.set("afterevent", str({
485 # AFTEREVENT.NONE: "nothing",
486 # AFTEREVENT.STANDBY: "standby",
487 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
488 # AFTEREVENT.AUTO: "auto"}))
489 #if timer.eit is not None:
490 # t.set("eit", str(timer.eit))
491 #if timer.dirname is not None:
492 # t.set("location", str(timer.dirname))
493 #t.set("disabled", str(int(timer.disabled)))
494 #t.set("justplay", str(int(timer.justplay)))
498 #for time, code, msg in timer.log_entries:
499 #l = xml.etree.cElementTree.SubElement(t, 'log')
500 #l.set("time", str(time))
501 #l.set("code", str(code))
505 #doc = xml.etree.cElementTree.ElementTree(root_element)
506 #doc.write(self.Filename)
510 list.append('<?xml version="1.0" ?>\n')
511 list.append('<timers>\n')
513 for timer in self.timer_list + self.processed_timers:
517 list.append('<timer')
518 list.append(' begin="' + str(int(timer.begin)) + '"')
519 list.append(' end="' + str(int(timer.end)) + '"')
520 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
521 list.append(' repeated="' + str(int(timer.repeated)) + '"')
522 list.append(' name="' + str(stringToXML(timer.name)) + '"')
523 list.append(' description="' + str(stringToXML(timer.description)) + '"')
524 list.append(' afterevent="' + str(stringToXML({
525 AFTEREVENT.NONE: "nothing",
526 AFTEREVENT.STANDBY: "standby",
527 AFTEREVENT.DEEPSTANDBY: "deepstandby",
528 AFTEREVENT.AUTO: "auto"
529 }[timer.afterEvent])) + '"')
530 if timer.eit is not None:
531 list.append(' eit="' + str(timer.eit) + '"')
532 if timer.dirname is not None:
533 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
534 if timer.tags is not None:
535 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
536 list.append(' disabled="' + str(int(timer.disabled)) + '"')
537 list.append(' justplay="' + str(int(timer.justplay)) + '"')
540 if config.recording.debug.value:
541 for time, code, msg in timer.log_entries:
543 list.append(' code="' + str(code) + '"')
544 list.append(' time="' + str(time) + '"')
546 list.append(str(stringToXML(msg)))
547 list.append('</log>\n')
549 list.append('</timer>\n')
551 list.append('</timers>\n')
553 file = open(self.Filename, "w")
558 def getNextZapTime(self):
560 for timer in self.timer_list:
561 if not timer.justplay or timer.begin < now:
566 def getNextRecordingTime(self):
568 for timer in self.timer_list:
569 if timer.justplay or timer.begin < now:
574 def isNextRecordAfterEventActionAuto(self):
577 for timer in self.timer_list:
578 if timer.justplay or timer.begin < now:
580 if t is None or t.begin == timer.begin:
582 if t.afterEvent == AFTEREVENT.AUTO:
586 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
587 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
588 if not timersanitycheck.check():
589 if ignoreTSC != True:
590 print "timer conflict detected!"
591 print timersanitycheck.getSimulTimerList()
592 return timersanitycheck.getSimulTimerList()
594 print "ignore timer conflict"
595 elif timersanitycheck.doubleCheck():
596 print "ignore double timer"
599 print "[Timer] Record " + str(entry)
601 self.addTimerEntry(entry)
606 def isInTimer(self, eventid, begin, duration, service):
610 chktimecmp_end = None
611 end = begin + duration
612 refstr = str(service)
613 for x in self.timer_list:
614 check = x.service_ref.ref.toString() == refstr
616 sref = x.service_ref.ref
617 parent_sid = sref.getUnsignedData(5)
618 parent_tsid = sref.getUnsignedData(6)
619 if parent_sid and parent_tsid: # check for subservice
620 sid = sref.getUnsignedData(1)
621 tsid = sref.getUnsignedData(2)
622 sref.setUnsignedData(1, parent_sid)
623 sref.setUnsignedData(2, parent_tsid)
624 sref.setUnsignedData(5, 0)
625 sref.setUnsignedData(6, 0)
626 check = sref.toCompareString() == refstr
630 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
631 num = event and event.getNumOfLinkageServices() or 0
632 sref.setUnsignedData(1, sid)
633 sref.setUnsignedData(2, tsid)
634 sref.setUnsignedData(5, parent_sid)
635 sref.setUnsignedData(6, parent_tsid)
636 for cnt in range(num):
637 subservice = event.getLinkageService(sref, cnt)
638 if sref.toCompareString() == subservice.toCompareString():
644 chktime = localtime(begin)
645 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
646 chktimecmp_end = chktimecmp + (duration / 60)
647 time = localtime(x.begin)
648 for y in (0, 1, 2, 3, 4, 5, 6):
649 if x.repeated & (2 ** y):
650 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
651 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
652 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
653 elif chktimecmp <= timecmp < chktimecmp_end:
654 time_match = (chktimecmp_end - timecmp) * 60
655 else: #if x.eit is None:
656 if begin <= x.begin <= end:
658 if time_match < diff:
660 elif x.begin <= begin <= x.end:
662 if time_match < diff:
668 def removeEntry(self, entry):
669 print "[Timer] Remove " + str(entry)
672 entry.repeated = False
675 # this sets the end time to current time, so timer will be stopped.
676 entry.autoincrease = False
679 if entry.state != entry.StateEnded:
680 self.timeChanged(entry)
682 print "state: ", entry.state
683 print "in processed: ", entry in self.processed_timers
684 print "in running: ", entry in self.timer_list
685 # autoincrease instanttimer if possible
686 if not entry.dontSave:
687 for x in self.timer_list:
688 if x.setAutoincreaseEnd():
690 # now the timer should be in the processed_timers list. remove it from there.
691 self.processed_timers.remove(entry)