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)
189 self.log(2, "'prepare' failed: error %d" % prep_res)
190 NavigationInstance.instance.stopRecordService(self.record_service)
191 self.record_service = None
194 self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
196 f = open(self.Filename + ".ts.meta", "w")
197 f.write(rec_ref.toString() + "\n")
198 f.write(self.name.replace("\n", "") + "\n")
199 f.write(self.description.replace("\n", "") + "\n")
200 f.write(str(self.begin) + "\n")
201 f.write(' '.join(self.tags))
204 self.log(4, "failed to write meta information")
205 NavigationInstance.instance.stopRecordService(self.record_service)
206 self.record_service = None
210 def do_backoff(self):
211 if self.backoff == 0:
215 if self.backoff > 100:
217 self.log(10, "backoff: retry in %d seconds" % self.backoff)
220 next_state = self.state + 1
221 self.log(5, "activating state %d" % next_state)
223 if next_state == self.StatePrepared:
224 if self.tryPrepare():
225 self.log(6, "prepare ok, waiting for begin")
226 # fine. it worked, resources are allocated.
227 self.next_activation = self.begin
231 self.log(7, "prepare failed")
232 if self.first_try_prepare:
233 self.first_try_prepare = False
234 if not config.recording.asktozap.value:
235 self.log(8, "asking user to zap away")
236 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
237 else: # zap without asking
238 self.log(9, "zap without asking")
239 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
244 self.start_prepare = time.time() + self.backoff
246 elif next_state == self.StateRunning:
247 # if this timer has been cancelled, just go to "end" state.
252 if Screens.Standby.inStandby:
253 self.log(11, "wakeup and zap")
254 #set service to zap after standby
255 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
257 Screens.Standby.inStandby.Power()
259 self.log(11, "zapping")
260 NavigationInstance.instance.playService(self.service_ref.ref)
263 self.log(11, "start recording")
264 record_res = self.record_service.start()
267 self.log(13, "start record returned %d" % record_res)
270 self.begin = time.time() + self.backoff
274 elif next_state == self.StateEnded:
276 if self.setAutoincreaseEnd():
277 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
280 self.log(12, "stop recording")
281 if not self.justplay:
282 NavigationInstance.instance.stopRecordService(self.record_service)
283 self.record_service = None
284 if self.afterEvent == AFTEREVENT.STANDBY:
285 if not Screens.Standby.inStandby: # not already in standby
286 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
287 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
288 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
289 if Screens.Standby.inStandby: # in standby
290 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
292 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
295 def setAutoincreaseEnd(self, entry = None):
296 if not self.autoincrease:
299 new_end = int(time.time()) + self.autoincreasetime
301 new_end = entry.begin -30
303 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)
304 dummyentry.disabled = self.disabled
305 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
306 if not timersanitycheck.check():
307 simulTimerList = timersanitycheck.getSimulTimerList()
308 new_end = simulTimerList[1].begin
310 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
312 if new_end <= time.time():
318 def sendStandbyNotification(self, answer):
320 Notifications.AddNotification(Screens.Standby.Standby)
322 def sendTryQuitMainloopNotification(self, answer):
324 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
326 def getNextActivation(self):
327 if self.state == self.StateEnded:
330 next_state = self.state + 1
332 return {self.StatePrepared: self.start_prepare,
333 self.StateRunning: self.begin,
334 self.StateEnded: self.end }[next_state]
336 def failureCB(self, answer):
338 self.log(13, "ok, zapped away")
339 #NavigationInstance.instance.stopUserServices()
340 NavigationInstance.instance.playService(self.service_ref.ref)
342 self.log(14, "user didn't want to zap away, record will probably fail")
344 def timeChanged(self):
345 old_prepare = self.start_prepare
346 self.start_prepare = self.begin - self.prepare_time
349 if int(old_prepare) != int(self.start_prepare):
350 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
352 def gotRecordEvent(self, record, event):
353 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
354 if self.__record_service.__deref__() != record.__deref__():
356 self.log(16, "record event %d" % event)
357 if event == iRecordableService.evRecordWriteError:
358 print "WRITE ERROR on recording, disk full?"
359 # show notification. the 'id' will make sure that it will be
360 # displayed only once, even if more timers are failing at the
361 # same time. (which is very likely in case of disk fullness)
362 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
363 # ok, the recording has been stopped. we need to properly note
364 # that in our state, with also keeping the possibility to re-try.
365 # TODO: this has to be done.
366 elif event == iRecordableService.evStart:
367 text = _("A record has been started:\n%s") % self.name
368 if self.dirnameHadToFallback:
369 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
371 # maybe this should be configurable?
372 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
374 # we have record_service as property to automatically subscribe to record service events
375 def setRecordService(self, service):
376 if self.__record_service is not None:
377 print "[remove callback]"
378 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
380 self.__record_service = service
382 if self.__record_service is not None:
383 print "[add callback]"
384 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
386 record_service = property(lambda self: self.__record_service, setRecordService)
388 def createTimer(xml):
389 begin = int(xml.get("begin"))
390 end = int(xml.get("end"))
391 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
392 description = xml.get("description").encode("utf-8")
393 repeated = xml.get("repeated").encode("utf-8")
394 disabled = long(xml.get("disabled") or "0")
395 justplay = long(xml.get("justplay") or "0")
396 afterevent = str(xml.get("afterevent") or "nothing")
398 "nothing": AFTEREVENT.NONE,
399 "standby": AFTEREVENT.STANDBY,
400 "deepstandby": AFTEREVENT.DEEPSTANDBY,
401 "auto": AFTEREVENT.AUTO
404 if eit and eit != "None":
408 location = xml.get("location")
409 if location and location != "None":
410 location = location.encode("utf-8")
413 tags = xml.get("tags")
414 if tags and tags != "None":
415 tags = tags.encode("utf-8").split(' ')
419 name = xml.get("name").encode("utf-8")
420 #filename = xml.get("filename").encode("utf-8")
421 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
422 entry.repeated = int(repeated)
424 for l in xml.findall("log"):
425 time = int(l.get("time"))
426 code = int(l.get("code"))
427 msg = l.text.strip().encode("utf-8")
428 entry.log_entries.append((time, code, msg))
432 class RecordTimer(timer.Timer):
434 timer.Timer.__init__(self)
436 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
441 print "unable to load timers from file!"
443 def isRecording(self):
445 for timer in self.timer_list:
446 if timer.isRunning() and not timer.justplay:
453 doc = xml.etree.cElementTree.parse(self.Filename)
455 from Tools.Notifications import AddPopup
456 from Screens.MessageBox import MessageBox
458 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
460 print "timers.xml failed to load!"
463 os.rename(self.Filename, self.Filename + "_old")
464 except (IOError, OSError):
465 print "renaming broken timer failed"
468 print "timers.xml not found!"
473 # put out a message when at least one timer overlaps
475 for timer in root.findall("timer"):
476 newTimer = createTimer(timer)
477 if (self.record(newTimer, True, True) is not None) and (checkit == True):
478 from Tools.Notifications import AddPopup
479 from Screens.MessageBox import MessageBox
480 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
481 checkit = False # at moment it is enough when the message is displayed one time
484 #root_element = xml.etree.cElementTree.Element('timers')
485 #root_element.text = "\n"
487 #for timer in self.timer_list + self.processed_timers:
488 # some timers (instant records) don't want to be saved.
492 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
493 #t.set("begin", str(int(timer.begin)))
494 #t.set("end", str(int(timer.end)))
495 #t.set("serviceref", str(timer.service_ref))
496 #t.set("repeated", str(timer.repeated))
497 #t.set("name", timer.name)
498 #t.set("description", timer.description)
499 #t.set("afterevent", str({
500 # AFTEREVENT.NONE: "nothing",
501 # AFTEREVENT.STANDBY: "standby",
502 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
503 # AFTEREVENT.AUTO: "auto"}))
504 #if timer.eit is not None:
505 # t.set("eit", str(timer.eit))
506 #if timer.dirname is not None:
507 # t.set("location", str(timer.dirname))
508 #t.set("disabled", str(int(timer.disabled)))
509 #t.set("justplay", str(int(timer.justplay)))
513 #for time, code, msg in timer.log_entries:
514 #l = xml.etree.cElementTree.SubElement(t, 'log')
515 #l.set("time", str(time))
516 #l.set("code", str(code))
520 #doc = xml.etree.cElementTree.ElementTree(root_element)
521 #doc.write(self.Filename)
525 list.append('<?xml version="1.0" ?>\n')
526 list.append('<timers>\n')
528 for timer in self.timer_list + self.processed_timers:
532 list.append('<timer')
533 list.append(' begin="' + str(int(timer.begin)) + '"')
534 list.append(' end="' + str(int(timer.end)) + '"')
535 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
536 list.append(' repeated="' + str(int(timer.repeated)) + '"')
537 list.append(' name="' + str(stringToXML(timer.name)) + '"')
538 list.append(' description="' + str(stringToXML(timer.description)) + '"')
539 list.append(' afterevent="' + str(stringToXML({
540 AFTEREVENT.NONE: "nothing",
541 AFTEREVENT.STANDBY: "standby",
542 AFTEREVENT.DEEPSTANDBY: "deepstandby",
543 AFTEREVENT.AUTO: "auto"
544 }[timer.afterEvent])) + '"')
545 if timer.eit is not None:
546 list.append(' eit="' + str(timer.eit) + '"')
547 if timer.dirname is not None:
548 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
549 if timer.tags is not None:
550 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
551 list.append(' disabled="' + str(int(timer.disabled)) + '"')
552 list.append(' justplay="' + str(int(timer.justplay)) + '"')
555 if config.recording.debug.value:
556 for time, code, msg in timer.log_entries:
558 list.append(' code="' + str(code) + '"')
559 list.append(' time="' + str(time) + '"')
561 list.append(str(stringToXML(msg)))
562 list.append('</log>\n')
564 list.append('</timer>\n')
566 list.append('</timers>\n')
568 file = open(self.Filename, "w")
573 def getNextZapTime(self):
575 for timer in self.timer_list:
576 if not timer.justplay or timer.begin < now:
581 def getNextRecordingTime(self):
583 for timer in self.timer_list:
584 if timer.justplay or timer.begin < now:
589 def isNextRecordAfterEventActionAuto(self):
592 for timer in self.timer_list:
593 if timer.justplay or timer.begin < now:
595 if t is None or t.begin == timer.begin:
597 if t.afterEvent == AFTEREVENT.AUTO:
601 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
602 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
603 if not timersanitycheck.check():
604 if ignoreTSC != True:
605 print "timer conflict detected!"
606 print timersanitycheck.getSimulTimerList()
607 return timersanitycheck.getSimulTimerList()
609 print "ignore timer conflict"
610 elif timersanitycheck.doubleCheck():
611 print "ignore double timer"
614 print "[Timer] Record " + str(entry)
616 self.addTimerEntry(entry)
621 def isInTimer(self, eventid, begin, duration, service):
625 chktimecmp_end = None
626 end = begin + duration
627 refstr = str(service)
628 for x in self.timer_list:
629 check = x.service_ref.ref.toString() == refstr
631 sref = x.service_ref.ref
632 parent_sid = sref.getUnsignedData(5)
633 parent_tsid = sref.getUnsignedData(6)
634 if parent_sid and parent_tsid: # check for subservice
635 sid = sref.getUnsignedData(1)
636 tsid = sref.getUnsignedData(2)
637 sref.setUnsignedData(1, parent_sid)
638 sref.setUnsignedData(2, parent_tsid)
639 sref.setUnsignedData(5, 0)
640 sref.setUnsignedData(6, 0)
641 check = sref.toCompareString() == refstr
645 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
646 num = event and event.getNumOfLinkageServices() or 0
647 sref.setUnsignedData(1, sid)
648 sref.setUnsignedData(2, tsid)
649 sref.setUnsignedData(5, parent_sid)
650 sref.setUnsignedData(6, parent_tsid)
651 for cnt in range(num):
652 subservice = event.getLinkageService(sref, cnt)
653 if sref.toCompareString() == subservice.toCompareString():
659 chktime = localtime(begin)
660 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
661 chktimecmp_end = chktimecmp + (duration / 60)
662 time = localtime(x.begin)
663 for y in (0, 1, 2, 3, 4, 5, 6):
664 if x.repeated & (2 ** y):
665 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
666 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
667 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
668 elif chktimecmp <= timecmp < chktimecmp_end:
669 time_match = (chktimecmp_end - timecmp) * 60
670 else: #if x.eit is None:
671 if begin <= x.begin <= end:
673 if time_match < diff:
675 elif x.begin <= begin <= x.end:
677 if time_match < diff:
683 def removeEntry(self, entry):
684 print "[Timer] Remove " + str(entry)
687 entry.repeated = False
690 # this sets the end time to current time, so timer will be stopped.
691 entry.autoincrease = False
694 if entry.state != entry.StateEnded:
695 self.timeChanged(entry)
697 print "state: ", entry.state
698 print "in processed: ", entry in self.processed_timers
699 print "in running: ", entry in self.timer_list
700 # autoincrease instanttimer if possible
701 if not entry.dontSave:
702 for x in self.timer_list:
703 if x.setAutoincreaseEnd():
705 # now the timer should be in the processed_timers list. remove it from there.
706 self.processed_timers.remove(entry)