2 #from time import datetime
3 from Tools import Directories, Notifications, ASCIItranslit
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 config.recording.ascii_filenames.value:
145 filename = ASCIItranslit.legacyEncode(filename)
147 if self.dirname and not Directories.fileExists(self.dirname, 'w'):
148 self.dirnameHadToFallback = True
149 self.Filename = Directories.getRecordingFilename(filename, None)
151 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
152 self.log(0, "Filename calculated as: '%s'" % self.Filename)
153 #begin_date + " - " + service_name + description)
155 def tryPrepare(self):
159 self.calculateFilename()
160 rec_ref = self.service_ref and self.service_ref.ref
161 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
162 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
164 self.log(1, "'get best playable service for group... record' failed")
167 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
169 if not self.record_service:
170 self.log(1, "'record service' failed")
174 epgcache = eEPGCache.getInstance()
175 queryTime=self.begin+(self.end-self.begin)/2
176 evt = epgcache.lookupEventTime(rec_ref, queryTime)
178 self.description = evt.getShortDescription()
179 event_id = evt.getEventId()
187 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))
190 self.log(4, "failed to write meta information")
192 self.log(2, "'prepare' failed: error %d" % prep_res)
194 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
195 # the next start time in evEnd event handler...
197 self.start_prepare = time.time() + self.backoff
199 NavigationInstance.instance.stopRecordService(self.record_service)
200 self.record_service = None
204 def do_backoff(self):
205 if self.backoff == 0:
209 if self.backoff > 100:
211 self.log(10, "backoff: retry in %d seconds" % self.backoff)
214 next_state = self.state + 1
215 self.log(5, "activating state %d" % next_state)
217 if next_state == self.StatePrepared:
218 if self.tryPrepare():
219 self.log(6, "prepare ok, waiting for begin")
220 # create file to "reserve" the filename
221 # because another recording at the same time on another service can try to record the same event
222 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
223 # here than calculateFilename is happy
224 open(self.Filename + ".ts", "w").close()
225 # fine. it worked, resources are allocated.
226 self.next_activation = self.begin
230 self.log(7, "prepare failed")
231 if self.first_try_prepare:
232 self.first_try_prepare = False
233 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
234 if cur_ref and not cur_ref.getPath():
235 if not config.recording.asktozap.value:
236 self.log(8, "asking user to zap away")
237 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
238 else: # zap without asking
239 self.log(9, "zap without asking")
240 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
243 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
245 self.log(8, "currently no service running... so we dont need to stop it")
247 elif next_state == self.StateRunning:
248 # if this timer has been cancelled, just go to "end" state.
253 if Screens.Standby.inStandby:
254 self.log(11, "wakeup and zap")
255 #set service to zap after standby
256 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
258 Screens.Standby.inStandby.Power()
260 self.log(11, "zapping")
261 NavigationInstance.instance.playService(self.service_ref.ref)
264 self.log(11, "start recording")
265 record_res = self.record_service.start()
268 self.log(13, "start record returned %d" % record_res)
271 self.begin = time.time() + self.backoff
275 elif next_state == self.StateEnded:
277 if self.setAutoincreaseEnd():
278 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
281 self.log(12, "stop recording")
282 if not self.justplay:
283 NavigationInstance.instance.stopRecordService(self.record_service)
284 self.record_service = None
285 if self.afterEvent == AFTEREVENT.STANDBY:
286 if not Screens.Standby.inStandby: # not already in standby
287 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
288 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
289 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
290 if Screens.Standby.inStandby: # in standby
291 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
293 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
296 def setAutoincreaseEnd(self, entry = None):
297 if not self.autoincrease:
300 new_end = int(time.time()) + self.autoincreasetime
302 new_end = entry.begin -30
304 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)
305 dummyentry.disabled = self.disabled
306 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
307 if not timersanitycheck.check():
308 simulTimerList = timersanitycheck.getSimulTimerList()
309 new_end = simulTimerList[1].begin
311 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
313 if new_end <= time.time():
319 def sendStandbyNotification(self, answer):
321 Notifications.AddNotification(Screens.Standby.Standby)
323 def sendTryQuitMainloopNotification(self, answer):
325 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
327 def getNextActivation(self):
328 if self.state == self.StateEnded:
331 next_state = self.state + 1
333 return {self.StatePrepared: self.start_prepare,
334 self.StateRunning: self.begin,
335 self.StateEnded: self.end }[next_state]
337 def failureCB(self, answer):
339 self.log(13, "ok, zapped away")
340 #NavigationInstance.instance.stopUserServices()
341 NavigationInstance.instance.playService(self.service_ref.ref)
343 self.log(14, "user didn't want to zap away, record will probably fail")
345 def timeChanged(self):
346 old_prepare = self.start_prepare
347 self.start_prepare = self.begin - self.prepare_time
350 if int(old_prepare) != int(self.start_prepare):
351 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
353 def gotRecordEvent(self, record, event):
354 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
355 if self.__record_service.__deref__() != record.__deref__():
357 self.log(16, "record event %d" % event)
358 if event == iRecordableService.evRecordWriteError:
359 print "WRITE ERROR on recording, disk full?"
360 # show notification. the 'id' will make sure that it will be
361 # displayed only once, even if more timers are failing at the
362 # same time. (which is very likely in case of disk fullness)
363 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
364 # ok, the recording has been stopped. we need to properly note
365 # that in our state, with also keeping the possibility to re-try.
366 # TODO: this has to be done.
367 elif event == iRecordableService.evStart:
368 text = _("A record has been started:\n%s") % self.name
369 if self.dirnameHadToFallback:
370 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
372 if config.usage.show_message_when_recording_starts.value:
373 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
375 # we have record_service as property to automatically subscribe to record service events
376 def setRecordService(self, service):
377 if self.__record_service is not None:
378 print "[remove callback]"
379 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
381 self.__record_service = service
383 if self.__record_service is not None:
384 print "[add callback]"
385 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
387 record_service = property(lambda self: self.__record_service, setRecordService)
389 def createTimer(xml):
390 begin = int(xml.get("begin"))
391 end = int(xml.get("end"))
392 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
393 description = xml.get("description").encode("utf-8")
394 repeated = xml.get("repeated").encode("utf-8")
395 disabled = long(xml.get("disabled") or "0")
396 justplay = long(xml.get("justplay") or "0")
397 afterevent = str(xml.get("afterevent") or "nothing")
399 "nothing": AFTEREVENT.NONE,
400 "standby": AFTEREVENT.STANDBY,
401 "deepstandby": AFTEREVENT.DEEPSTANDBY,
402 "auto": AFTEREVENT.AUTO
405 if eit and eit != "None":
409 location = xml.get("location")
410 if location and location != "None":
411 location = location.encode("utf-8")
414 tags = xml.get("tags")
415 if tags and tags != "None":
416 tags = tags.encode("utf-8").split(' ')
420 name = xml.get("name").encode("utf-8")
421 #filename = xml.get("filename").encode("utf-8")
422 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
423 entry.repeated = int(repeated)
425 for l in xml.findall("log"):
426 time = int(l.get("time"))
427 code = int(l.get("code"))
428 msg = l.text.strip().encode("utf-8")
429 entry.log_entries.append((time, code, msg))
433 class RecordTimer(timer.Timer):
435 timer.Timer.__init__(self)
437 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
442 print "unable to load timers from file!"
444 def doActivate(self, w):
445 # when activating a timer which has already passed,
446 # simply abort the timer. don't run trough all the stages.
448 w.state = RecordTimerEntry.StateEnded
450 # when active returns true, this means "accepted".
451 # otherwise, the current state is kept.
452 # the timer entry itself will fix up the delay then.
456 self.timer_list.remove(w)
458 # did this timer reached the last state?
459 if w.state < RecordTimerEntry.StateEnded:
460 # no, sort it into active list
461 insort(self.timer_list, w)
463 # yes. Process repeated, and re-add.
466 w.state = RecordTimerEntry.StateWaiting
467 self.addTimerEntry(w)
469 insort(self.processed_timers, w)
473 def isRecording(self):
475 for timer in self.timer_list:
476 if timer.isRunning() and not timer.justplay:
483 doc = xml.etree.cElementTree.parse(self.Filename)
485 from Tools.Notifications import AddPopup
486 from Screens.MessageBox import MessageBox
488 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
490 print "timers.xml failed to load!"
493 os.rename(self.Filename, self.Filename + "_old")
494 except (IOError, OSError):
495 print "renaming broken timer failed"
498 print "timers.xml not found!"
503 # put out a message when at least one timer overlaps
505 for timer in root.findall("timer"):
506 newTimer = createTimer(timer)
507 if (self.record(newTimer, True, True) is not None) and (checkit == True):
508 from Tools.Notifications import AddPopup
509 from Screens.MessageBox import MessageBox
510 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
511 checkit = False # at moment it is enough when the message is displayed one time
514 #root_element = xml.etree.cElementTree.Element('timers')
515 #root_element.text = "\n"
517 #for timer in self.timer_list + self.processed_timers:
518 # some timers (instant records) don't want to be saved.
522 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
523 #t.set("begin", str(int(timer.begin)))
524 #t.set("end", str(int(timer.end)))
525 #t.set("serviceref", str(timer.service_ref))
526 #t.set("repeated", str(timer.repeated))
527 #t.set("name", timer.name)
528 #t.set("description", timer.description)
529 #t.set("afterevent", str({
530 # AFTEREVENT.NONE: "nothing",
531 # AFTEREVENT.STANDBY: "standby",
532 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
533 # AFTEREVENT.AUTO: "auto"}))
534 #if timer.eit is not None:
535 # t.set("eit", str(timer.eit))
536 #if timer.dirname is not None:
537 # t.set("location", str(timer.dirname))
538 #t.set("disabled", str(int(timer.disabled)))
539 #t.set("justplay", str(int(timer.justplay)))
543 #for time, code, msg in timer.log_entries:
544 #l = xml.etree.cElementTree.SubElement(t, 'log')
545 #l.set("time", str(time))
546 #l.set("code", str(code))
550 #doc = xml.etree.cElementTree.ElementTree(root_element)
551 #doc.write(self.Filename)
555 list.append('<?xml version="1.0" ?>\n')
556 list.append('<timers>\n')
558 for timer in self.timer_list + self.processed_timers:
562 list.append('<timer')
563 list.append(' begin="' + str(int(timer.begin)) + '"')
564 list.append(' end="' + str(int(timer.end)) + '"')
565 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
566 list.append(' repeated="' + str(int(timer.repeated)) + '"')
567 list.append(' name="' + str(stringToXML(timer.name)) + '"')
568 list.append(' description="' + str(stringToXML(timer.description)) + '"')
569 list.append(' afterevent="' + str(stringToXML({
570 AFTEREVENT.NONE: "nothing",
571 AFTEREVENT.STANDBY: "standby",
572 AFTEREVENT.DEEPSTANDBY: "deepstandby",
573 AFTEREVENT.AUTO: "auto"
574 }[timer.afterEvent])) + '"')
575 if timer.eit is not None:
576 list.append(' eit="' + str(timer.eit) + '"')
577 if timer.dirname is not None:
578 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
579 if timer.tags is not None:
580 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
581 list.append(' disabled="' + str(int(timer.disabled)) + '"')
582 list.append(' justplay="' + str(int(timer.justplay)) + '"')
585 if config.recording.debug.value:
586 for time, code, msg in timer.log_entries:
588 list.append(' code="' + str(code) + '"')
589 list.append(' time="' + str(time) + '"')
591 list.append(str(stringToXML(msg)))
592 list.append('</log>\n')
594 list.append('</timer>\n')
596 list.append('</timers>\n')
598 file = open(self.Filename, "w")
603 def getNextZapTime(self):
605 for timer in self.timer_list:
606 if not timer.justplay or timer.begin < now:
611 def getNextRecordingTime(self):
613 for timer in self.timer_list:
614 next_act = timer.getNextActivation()
615 if timer.justplay or next_act < now:
620 def isNextRecordAfterEventActionAuto(self):
623 for timer in self.timer_list:
624 if timer.justplay or timer.begin < now:
626 if t is None or t.begin == timer.begin:
628 if t.afterEvent == AFTEREVENT.AUTO:
632 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
633 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
634 if not timersanitycheck.check():
635 if ignoreTSC != True:
636 print "timer conflict detected!"
637 print timersanitycheck.getSimulTimerList()
638 return timersanitycheck.getSimulTimerList()
640 print "ignore timer conflict"
641 elif timersanitycheck.doubleCheck():
642 print "ignore double timer"
645 print "[Timer] Record " + str(entry)
647 self.addTimerEntry(entry)
652 def isInTimer(self, eventid, begin, duration, service):
656 chktimecmp_end = None
657 end = begin + duration
658 refstr = str(service)
659 for x in self.timer_list:
660 check = x.service_ref.ref.toString() == refstr
662 sref = x.service_ref.ref
663 parent_sid = sref.getUnsignedData(5)
664 parent_tsid = sref.getUnsignedData(6)
665 if parent_sid and parent_tsid: # check for subservice
666 sid = sref.getUnsignedData(1)
667 tsid = sref.getUnsignedData(2)
668 sref.setUnsignedData(1, parent_sid)
669 sref.setUnsignedData(2, parent_tsid)
670 sref.setUnsignedData(5, 0)
671 sref.setUnsignedData(6, 0)
672 check = sref.toCompareString() == refstr
676 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
677 num = event and event.getNumOfLinkageServices() or 0
678 sref.setUnsignedData(1, sid)
679 sref.setUnsignedData(2, tsid)
680 sref.setUnsignedData(5, parent_sid)
681 sref.setUnsignedData(6, parent_tsid)
682 for cnt in range(num):
683 subservice = event.getLinkageService(sref, cnt)
684 if sref.toCompareString() == subservice.toCompareString():
690 chktime = localtime(begin)
691 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
692 chktimecmp_end = chktimecmp + (duration / 60)
693 time = localtime(x.begin)
694 for y in (0, 1, 2, 3, 4, 5, 6):
695 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
696 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
697 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
698 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
699 elif chktimecmp <= timecmp < chktimecmp_end:
700 time_match = (chktimecmp_end - timecmp) * 60
701 else: #if x.eit is None:
702 if begin <= x.begin <= end:
704 if time_match < diff:
706 elif x.begin <= begin <= x.end:
708 if time_match < diff:
714 def removeEntry(self, entry):
715 print "[Timer] Remove " + str(entry)
718 entry.repeated = False
721 # this sets the end time to current time, so timer will be stopped.
722 entry.autoincrease = False
725 if entry.state != entry.StateEnded:
726 self.timeChanged(entry)
728 print "state: ", entry.state
729 print "in processed: ", entry in self.processed_timers
730 print "in running: ", entry in self.timer_list
731 # autoincrease instanttimer if possible
732 if not entry.dontSave:
733 for x in self.timer_list:
734 if x.setAutoincreaseEnd():
736 # now the timer should be in the processed_timers list. remove it from there.
737 self.processed_timers.remove(entry)