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)
193 NavigationInstance.instance.stopRecordService(self.record_service)
194 self.record_service = None
198 def do_backoff(self):
199 if self.backoff == 0:
203 if self.backoff > 100:
205 self.log(10, "backoff: retry in %d seconds" % self.backoff)
208 next_state = self.state + 1
209 self.log(5, "activating state %d" % next_state)
211 if next_state == self.StatePrepared:
212 if self.tryPrepare():
213 self.log(6, "prepare ok, waiting for begin")
214 # fine. it worked, resources are allocated.
215 self.next_activation = self.begin
219 self.log(7, "prepare failed")
220 if self.first_try_prepare:
221 self.first_try_prepare = False
222 if not config.recording.asktozap.value:
223 self.log(8, "asking user to zap away")
224 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
225 else: # zap without asking
226 self.log(9, "zap without asking")
227 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
232 self.start_prepare = time.time() + self.backoff
234 elif next_state == self.StateRunning:
235 # if this timer has been cancelled, just go to "end" state.
240 if Screens.Standby.inStandby:
241 self.log(11, "wakeup and zap")
242 #set service to zap after standby
243 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
245 Screens.Standby.inStandby.Power()
247 self.log(11, "zapping")
248 NavigationInstance.instance.playService(self.service_ref.ref)
251 self.log(11, "start recording")
252 record_res = self.record_service.start()
255 self.log(13, "start record returned %d" % record_res)
258 self.begin = time.time() + self.backoff
262 elif next_state == self.StateEnded:
264 if self.setAutoincreaseEnd():
265 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
268 self.log(12, "stop recording")
269 if not self.justplay:
270 NavigationInstance.instance.stopRecordService(self.record_service)
271 self.record_service = None
272 if self.afterEvent == AFTEREVENT.STANDBY:
273 if not Screens.Standby.inStandby: # not already in standby
274 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
275 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
276 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
277 if Screens.Standby.inStandby: # in standby
278 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
280 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
283 def setAutoincreaseEnd(self, entry = None):
284 if not self.autoincrease:
287 new_end = int(time.time()) + self.autoincreasetime
289 new_end = entry.begin -30
291 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)
292 dummyentry.disabled = self.disabled
293 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
294 if not timersanitycheck.check():
295 simulTimerList = timersanitycheck.getSimulTimerList()
296 new_end = simulTimerList[1].begin
298 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
300 if new_end <= time.time():
306 def sendStandbyNotification(self, answer):
308 Notifications.AddNotification(Screens.Standby.Standby)
310 def sendTryQuitMainloopNotification(self, answer):
312 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
314 def getNextActivation(self):
315 if self.state == self.StateEnded:
318 next_state = self.state + 1
320 return {self.StatePrepared: self.start_prepare,
321 self.StateRunning: self.begin,
322 self.StateEnded: self.end }[next_state]
324 def failureCB(self, answer):
326 self.log(13, "ok, zapped away")
327 #NavigationInstance.instance.stopUserServices()
328 NavigationInstance.instance.playService(self.service_ref.ref)
330 self.log(14, "user didn't want to zap away, record will probably fail")
332 def timeChanged(self):
333 old_prepare = self.start_prepare
334 self.start_prepare = self.begin - self.prepare_time
337 if int(old_prepare) != int(self.start_prepare):
338 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
340 def gotRecordEvent(self, record, event):
341 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
342 if self.__record_service.__deref__() != record.__deref__():
344 self.log(16, "record event %d" % event)
345 if event == iRecordableService.evRecordWriteError:
346 print "WRITE ERROR on recording, disk full?"
347 # show notification. the 'id' will make sure that it will be
348 # displayed only once, even if more timers are failing at the
349 # same time. (which is very likely in case of disk fullness)
350 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
351 # ok, the recording has been stopped. we need to properly note
352 # that in our state, with also keeping the possibility to re-try.
353 # TODO: this has to be done.
354 elif event == iRecordableService.evStart:
355 text = _("A record has been started:\n%s") % self.name
356 if self.dirnameHadToFallback:
357 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
359 # maybe this should be configurable?
360 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
362 # we have record_service as property to automatically subscribe to record service events
363 def setRecordService(self, service):
364 if self.__record_service is not None:
365 print "[remove callback]"
366 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
368 self.__record_service = service
370 if self.__record_service is not None:
371 print "[add callback]"
372 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
374 record_service = property(lambda self: self.__record_service, setRecordService)
376 def createTimer(xml):
377 begin = int(xml.get("begin"))
378 end = int(xml.get("end"))
379 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
380 description = xml.get("description").encode("utf-8")
381 repeated = xml.get("repeated").encode("utf-8")
382 disabled = long(xml.get("disabled") or "0")
383 justplay = long(xml.get("justplay") or "0")
384 afterevent = str(xml.get("afterevent") or "nothing")
386 "nothing": AFTEREVENT.NONE,
387 "standby": AFTEREVENT.STANDBY,
388 "deepstandby": AFTEREVENT.DEEPSTANDBY,
389 "auto": AFTEREVENT.AUTO
392 if eit and eit != "None":
396 location = xml.get("location")
397 if location and location != "None":
398 location = location.encode("utf-8")
401 tags = xml.get("tags")
402 if tags and tags != "None":
403 tags = tags.encode("utf-8").split(' ')
407 name = xml.get("name").encode("utf-8")
408 #filename = xml.get("filename").encode("utf-8")
409 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
410 entry.repeated = int(repeated)
412 for l in xml.findall("log"):
413 time = int(l.get("time"))
414 code = int(l.get("code"))
415 msg = l.text.strip().encode("utf-8")
416 entry.log_entries.append((time, code, msg))
420 class RecordTimer(timer.Timer):
422 timer.Timer.__init__(self)
424 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
429 print "unable to load timers from file!"
431 def isRecording(self):
433 for timer in self.timer_list:
434 if timer.isRunning() and not timer.justplay:
441 doc = xml.etree.cElementTree.parse(self.Filename)
443 from Tools.Notifications import AddPopup
444 from Screens.MessageBox import MessageBox
446 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
448 print "timers.xml failed to load!"
451 os.rename(self.Filename, self.Filename + "_old")
452 except (IOError, OSError):
453 print "renaming broken timer failed"
456 print "timers.xml not found!"
461 # put out a message when at least one timer overlaps
463 for timer in root.findall("timer"):
464 newTimer = createTimer(timer)
465 if (self.record(newTimer, True, True) is not None) and (checkit == True):
466 from Tools.Notifications import AddPopup
467 from Screens.MessageBox import MessageBox
468 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
469 checkit = False # at moment it is enough when the message is displayed one time
472 #root_element = xml.etree.cElementTree.Element('timers')
473 #root_element.text = "\n"
475 #for timer in self.timer_list + self.processed_timers:
476 # some timers (instant records) don't want to be saved.
480 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
481 #t.set("begin", str(int(timer.begin)))
482 #t.set("end", str(int(timer.end)))
483 #t.set("serviceref", str(timer.service_ref))
484 #t.set("repeated", str(timer.repeated))
485 #t.set("name", timer.name)
486 #t.set("description", timer.description)
487 #t.set("afterevent", str({
488 # AFTEREVENT.NONE: "nothing",
489 # AFTEREVENT.STANDBY: "standby",
490 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
491 # AFTEREVENT.AUTO: "auto"}))
492 #if timer.eit is not None:
493 # t.set("eit", str(timer.eit))
494 #if timer.dirname is not None:
495 # t.set("location", str(timer.dirname))
496 #t.set("disabled", str(int(timer.disabled)))
497 #t.set("justplay", str(int(timer.justplay)))
501 #for time, code, msg in timer.log_entries:
502 #l = xml.etree.cElementTree.SubElement(t, 'log')
503 #l.set("time", str(time))
504 #l.set("code", str(code))
508 #doc = xml.etree.cElementTree.ElementTree(root_element)
509 #doc.write(self.Filename)
513 list.append('<?xml version="1.0" ?>\n')
514 list.append('<timers>\n')
516 for timer in self.timer_list + self.processed_timers:
520 list.append('<timer')
521 list.append(' begin="' + str(int(timer.begin)) + '"')
522 list.append(' end="' + str(int(timer.end)) + '"')
523 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
524 list.append(' repeated="' + str(int(timer.repeated)) + '"')
525 list.append(' name="' + str(stringToXML(timer.name)) + '"')
526 list.append(' description="' + str(stringToXML(timer.description)) + '"')
527 list.append(' afterevent="' + str(stringToXML({
528 AFTEREVENT.NONE: "nothing",
529 AFTEREVENT.STANDBY: "standby",
530 AFTEREVENT.DEEPSTANDBY: "deepstandby",
531 AFTEREVENT.AUTO: "auto"
532 }[timer.afterEvent])) + '"')
533 if timer.eit is not None:
534 list.append(' eit="' + str(timer.eit) + '"')
535 if timer.dirname is not None:
536 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
537 if timer.tags is not None:
538 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
539 list.append(' disabled="' + str(int(timer.disabled)) + '"')
540 list.append(' justplay="' + str(int(timer.justplay)) + '"')
543 if config.recording.debug.value:
544 for time, code, msg in timer.log_entries:
546 list.append(' code="' + str(code) + '"')
547 list.append(' time="' + str(time) + '"')
549 list.append(str(stringToXML(msg)))
550 list.append('</log>\n')
552 list.append('</timer>\n')
554 list.append('</timers>\n')
556 file = open(self.Filename, "w")
561 def getNextZapTime(self):
563 for timer in self.timer_list:
564 if not timer.justplay or timer.begin < now:
569 def getNextRecordingTime(self):
571 for timer in self.timer_list:
572 if timer.justplay or timer.begin < now:
577 def isNextRecordAfterEventActionAuto(self):
580 for timer in self.timer_list:
581 if timer.justplay or timer.begin < now:
583 if t is None or t.begin == timer.begin:
585 if t.afterEvent == AFTEREVENT.AUTO:
589 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
590 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
591 if not timersanitycheck.check():
592 if ignoreTSC != True:
593 print "timer conflict detected!"
594 print timersanitycheck.getSimulTimerList()
595 return timersanitycheck.getSimulTimerList()
597 print "ignore timer conflict"
598 elif timersanitycheck.doubleCheck():
599 print "ignore double timer"
602 print "[Timer] Record " + str(entry)
604 self.addTimerEntry(entry)
609 def isInTimer(self, eventid, begin, duration, service):
613 chktimecmp_end = None
614 end = begin + duration
615 refstr = str(service)
616 for x in self.timer_list:
617 check = x.service_ref.ref.toString() == refstr
619 sref = x.service_ref.ref
620 parent_sid = sref.getUnsignedData(5)
621 parent_tsid = sref.getUnsignedData(6)
622 if parent_sid and parent_tsid: # check for subservice
623 sid = sref.getUnsignedData(1)
624 tsid = sref.getUnsignedData(2)
625 sref.setUnsignedData(1, parent_sid)
626 sref.setUnsignedData(2, parent_tsid)
627 sref.setUnsignedData(5, 0)
628 sref.setUnsignedData(6, 0)
629 check = sref.toCompareString() == refstr
633 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
634 num = event and event.getNumOfLinkageServices() or 0
635 sref.setUnsignedData(1, sid)
636 sref.setUnsignedData(2, tsid)
637 sref.setUnsignedData(5, parent_sid)
638 sref.setUnsignedData(6, parent_tsid)
639 for cnt in range(num):
640 subservice = event.getLinkageService(sref, cnt)
641 if sref.toCompareString() == subservice.toCompareString():
647 chktime = localtime(begin)
648 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
649 chktimecmp_end = chktimecmp + (duration / 60)
650 time = localtime(x.begin)
651 for y in (0, 1, 2, 3, 4, 5, 6):
652 if x.repeated & (2 ** y):
653 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
654 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
655 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
656 elif chktimecmp <= timecmp < chktimecmp_end:
657 time_match = (chktimecmp_end - timecmp) * 60
658 else: #if x.eit is None:
659 if begin <= x.begin <= end:
661 if time_match < diff:
663 elif x.begin <= begin <= x.end:
665 if time_match < diff:
671 def removeEntry(self, entry):
672 print "[Timer] Remove " + str(entry)
675 entry.repeated = False
678 # this sets the end time to current time, so timer will be stopped.
679 entry.autoincrease = False
682 if entry.state != entry.StateEnded:
683 self.timeChanged(entry)
685 print "state: ", entry.state
686 print "in processed: ", entry in self.processed_timers
687 print "in running: ", entry in self.timer_list
688 # autoincrease instanttimer if possible
689 if not entry.dontSave:
690 for x in self.timer_list:
691 if x.setAutoincreaseEnd():
693 # now the timer should be in the processed_timers list. remove it from there.
694 self.processed_timers.remove(entry)