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 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.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)
186 self.log(2, "'prepare' failed: error %d" % prep_res)
187 NavigationInstance.instance.stopRecordService(self.record_service)
188 self.record_service = None
191 self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
193 f = open(self.Filename + ".ts.meta", "w")
194 f.write(rec_ref.toString() + "\n")
195 f.write(self.name + "\n")
196 f.write(self.description + "\n")
197 f.write(str(self.begin) + "\n")
198 f.write(' '.join(self.tags))
201 self.log(4, "failed to write meta information")
202 NavigationInstance.instance.stopRecordService(self.record_service)
203 self.record_service = None
207 def do_backoff(self):
208 if self.backoff == 0:
212 if self.backoff > 100:
214 self.log(10, "backoff: retry in %d seconds" % self.backoff)
217 next_state = self.state + 1
218 self.log(5, "activating state %d" % next_state)
220 if next_state == self.StatePrepared:
221 if self.tryPrepare():
222 self.log(6, "prepare ok, waiting for begin")
223 # fine. it worked, resources are allocated.
224 self.next_activation = self.begin
228 self.log(7, "prepare failed")
229 if self.first_try_prepare:
230 self.first_try_prepare = False
231 if not config.recording.asktozap.value:
232 self.log(8, "asking user to zap away")
233 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
234 else: # zap without asking
235 self.log(9, "zap without asking")
236 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
241 self.start_prepare = time.time() + self.backoff
243 elif next_state == self.StateRunning:
244 # if this timer has been cancelled, just go to "end" state.
249 if Screens.Standby.inStandby:
250 self.log(11, "wakeup and zap")
251 #set service to zap after standby
252 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
254 Screens.Standby.inStandby.Power()
256 self.log(11, "zapping")
257 NavigationInstance.instance.playService(self.service_ref.ref)
260 self.log(11, "start recording")
261 record_res = self.record_service.start()
264 self.log(13, "start record returned %d" % record_res)
267 self.begin = time.time() + self.backoff
271 elif next_state == self.StateEnded:
273 if self.setAutoincreaseEnd():
274 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
277 self.log(12, "stop recording")
278 if not self.justplay:
279 NavigationInstance.instance.stopRecordService(self.record_service)
280 self.record_service = None
281 if self.afterEvent == AFTEREVENT.STANDBY:
282 if not Screens.Standby.inStandby: # not already in standby
283 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
284 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
285 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
286 if Screens.Standby.inStandby: # in standby
287 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
289 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
292 def setAutoincreaseEnd(self, entry = None):
293 if not self.autoincrease:
296 new_end = int(time.time()) + self.autoincreasetime
298 new_end = entry.begin -30
300 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)
301 dummyentry.disabled = self.disabled
302 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
303 if not timersanitycheck.check():
304 simulTimerList = timersanitycheck.getSimulTimerList()
305 new_end = simulTimerList[1].begin
307 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
309 if new_end <= time.time():
315 def sendStandbyNotification(self, answer):
317 Notifications.AddNotification(Screens.Standby.Standby)
319 def sendTryQuitMainloopNotification(self, answer):
321 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
323 def getNextActivation(self):
324 if self.state == self.StateEnded:
327 next_state = self.state + 1
329 return {self.StatePrepared: self.start_prepare,
330 self.StateRunning: self.begin,
331 self.StateEnded: self.end }[next_state]
333 def failureCB(self, answer):
335 self.log(13, "ok, zapped away")
336 #NavigationInstance.instance.stopUserServices()
337 NavigationInstance.instance.playService(self.service_ref.ref)
339 self.log(14, "user didn't want to zap away, record will probably fail")
341 def timeChanged(self):
342 old_prepare = self.start_prepare
343 self.start_prepare = self.begin - self.prepare_time
346 if int(old_prepare) != int(self.start_prepare):
347 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
349 def gotRecordEvent(self, record, event):
350 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
351 if self.__record_service.__deref__() != record.__deref__():
353 self.log(16, "record event %d" % event)
354 if event == iRecordableService.evRecordWriteError:
355 print "WRITE ERROR on recording, disk full?"
356 # show notification. the 'id' will make sure that it will be
357 # displayed only once, even if more timers are failing at the
358 # same time. (which is very likely in case of disk fullness)
359 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
360 # ok, the recording has been stopped. we need to properly note
361 # that in our state, with also keeping the possibility to re-try.
362 # TODO: this has to be done.
363 elif event == iRecordableService.evStart:
364 text = _("A record has been started:\n%s") % self.name
365 if self.dirnameHadToFallback:
366 text = '\n'.join([text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")])
368 # maybe this should be configurable?
369 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
371 # we have record_service as property to automatically subscribe to record service events
372 def setRecordService(self, service):
373 if self.__record_service is not None:
374 print "[remove callback]"
375 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
377 self.__record_service = service
379 if self.__record_service is not None:
380 print "[add callback]"
381 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
383 record_service = property(lambda self: self.__record_service, setRecordService)
385 def createTimer(xml):
386 begin = int(xml.get("begin"))
387 end = int(xml.get("end"))
388 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
389 description = xml.get("description").encode("utf-8")
390 repeated = xml.get("repeated").encode("utf-8")
391 disabled = long(xml.get("disabled") or "0")
392 justplay = long(xml.get("justplay") or "0")
393 afterevent = str(xml.get("afterevent") or "nothing")
395 "nothing": AFTEREVENT.NONE,
396 "standby": AFTEREVENT.STANDBY,
397 "deepstandby": AFTEREVENT.DEEPSTANDBY,
398 "auto": AFTEREVENT.AUTO
401 if eit and eit != "None":
405 location = xml.get("location")
406 if location and location != "None":
407 location = location.encode("utf-8")
410 tags = xml.get("tags")
411 if tags and tags != "None":
412 tags = tags.encode("utf-8").split(' ')
416 name = xml.get("name").encode("utf-8")
417 #filename = xml.get("filename").encode("utf-8")
418 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
419 entry.repeated = int(repeated)
421 for l in xml.findall("log"):
422 time = int(l.get("time"))
423 code = int(l.get("code"))
424 msg = l.text.strip().encode("utf-8")
425 entry.log_entries.append((time, code, msg))
429 class RecordTimer(timer.Timer):
431 timer.Timer.__init__(self)
433 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
438 print "unable to load timers from file!"
440 def isRecording(self):
442 for timer in self.timer_list:
443 if timer.isRunning() and not timer.justplay:
450 doc = xml.etree.cElementTree.parse(self.Filename)
452 from Tools.Notifications import AddPopup
453 from Screens.MessageBox import MessageBox
455 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
457 print "timers.xml failed to load!"
460 os.rename(self.Filename, self.Filename + "_old")
461 except (IOError, OSError):
462 print "renaming broken timer failed"
465 print "timers.xml not found!"
470 # put out a message when at least one timer overlaps
472 for timer in root.findall("timer"):
473 newTimer = createTimer(timer)
474 if (self.record(newTimer, True, True) is not None) and (checkit == True):
475 from Tools.Notifications import AddPopup
476 from Screens.MessageBox import MessageBox
477 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
478 checkit = False # at moment it is enough when the message is displayed one time
481 #root_element = xml.etree.cElementTree.Element('timers')
482 #root_element.text = "\n"
484 #for timer in self.timer_list + self.processed_timers:
485 # some timers (instant records) don't want to be saved.
489 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
490 #t.set("begin", str(int(timer.begin)))
491 #t.set("end", str(int(timer.end)))
492 #t.set("serviceref", str(timer.service_ref))
493 #t.set("repeated", str(timer.repeated))
494 #t.set("name", timer.name)
495 #t.set("description", timer.description)
496 #t.set("afterevent", str({
497 # AFTEREVENT.NONE: "nothing",
498 # AFTEREVENT.STANDBY: "standby",
499 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
500 # AFTEREVENT.AUTO: "auto"}))
501 #if timer.eit is not None:
502 # t.set("eit", str(timer.eit))
503 #if timer.dirname is not None:
504 # t.set("location", str(timer.dirname))
505 #t.set("disabled", str(int(timer.disabled)))
506 #t.set("justplay", str(int(timer.justplay)))
510 #for time, code, msg in timer.log_entries:
511 #l = xml.etree.cElementTree.SubElement(t, 'log')
512 #l.set("time", str(time))
513 #l.set("code", str(code))
517 #doc = xml.etree.cElementTree.ElementTree(root_element)
518 #doc.write(self.Filename)
522 list.append('<?xml version="1.0" ?>\n')
523 list.append('<timers>\n')
525 for timer in self.timer_list + self.processed_timers:
529 list.append('<timer')
530 list.append(' begin="' + str(int(timer.begin)) + '"')
531 list.append(' end="' + str(int(timer.end)) + '"')
532 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
533 list.append(' repeated="' + str(int(timer.repeated)) + '"')
534 list.append(' name="' + str(stringToXML(timer.name)) + '"')
535 list.append(' description="' + str(stringToXML(timer.description)) + '"')
536 list.append(' afterevent="' + str(stringToXML({
537 AFTEREVENT.NONE: "nothing",
538 AFTEREVENT.STANDBY: "standby",
539 AFTEREVENT.DEEPSTANDBY: "deepstandby",
540 AFTEREVENT.AUTO: "auto"
541 }[timer.afterEvent])) + '"')
542 if timer.eit is not None:
543 list.append(' eit="' + str(timer.eit) + '"')
544 if timer.dirname is not None:
545 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
546 if timer.tags is not None:
547 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
548 list.append(' disabled="' + str(int(timer.disabled)) + '"')
549 list.append(' justplay="' + str(int(timer.justplay)) + '"')
552 if config.recording.debug.value:
553 for time, code, msg in timer.log_entries:
555 list.append(' code="' + str(code) + '"')
556 list.append(' time="' + str(time) + '"')
558 list.append(str(stringToXML(msg)))
559 list.append('</log>\n')
561 list.append('</timer>\n')
563 list.append('</timers>\n')
565 file = open(self.Filename, "w")
570 def getNextZapTime(self):
572 for timer in self.timer_list:
573 if not timer.justplay or timer.begin < now:
578 def getNextRecordingTime(self):
580 for timer in self.timer_list:
581 if timer.justplay or timer.begin < now:
586 def isNextRecordAfterEventActionAuto(self):
589 for timer in self.timer_list:
590 if timer.justplay or timer.begin < now:
592 if t is None or t.begin == timer.begin:
594 if t.afterEvent == AFTEREVENT.AUTO:
598 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
599 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
600 if not timersanitycheck.check():
601 if ignoreTSC != True:
602 print "timer conflict detected!"
603 print timersanitycheck.getSimulTimerList()
604 return timersanitycheck.getSimulTimerList()
606 print "ignore timer conflict"
607 elif timersanitycheck.doubleCheck():
608 print "ignore double timer"
611 print "[Timer] Record " + str(entry)
613 self.addTimerEntry(entry)
618 def isInTimer(self, eventid, begin, duration, service):
622 chktimecmp_end = None
623 end = begin + duration
624 refstr = str(service)
625 for x in self.timer_list:
626 check = x.service_ref.ref.toString() == refstr
628 sref = x.service_ref.ref
629 parent_sid = sref.getUnsignedData(5)
630 parent_tsid = sref.getUnsignedData(6)
631 if parent_sid and parent_tsid: # check for subservice
632 sid = sref.getUnsignedData(1)
633 tsid = sref.getUnsignedData(2)
634 sref.setUnsignedData(1, parent_sid)
635 sref.setUnsignedData(2, parent_tsid)
636 sref.setUnsignedData(5, 0)
637 sref.setUnsignedData(6, 0)
638 check = sref.toCompareString() == refstr
642 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
643 num = event and event.getNumOfLinkageServices() or 0
644 sref.setUnsignedData(1, sid)
645 sref.setUnsignedData(2, tsid)
646 sref.setUnsignedData(5, parent_sid)
647 sref.setUnsignedData(6, parent_tsid)
648 for cnt in range(num):
649 subservice = event.getLinkageService(sref, cnt)
650 if sref.toCompareString() == subservice.toCompareString():
656 chktime = localtime(begin)
657 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
658 chktimecmp_end = chktimecmp + (duration / 60)
659 time = localtime(x.begin)
661 if x.repeated & (2 ** y):
662 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
663 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
664 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
665 elif chktimecmp <= timecmp < chktimecmp_end:
666 time_match = (chktimecmp_end - timecmp) * 60
667 else: #if x.eit is None:
668 if begin <= x.begin <= end:
670 if time_match < diff:
672 elif x.begin <= begin <= x.end:
674 if time_match < diff:
680 def removeEntry(self, entry):
681 print "[Timer] Remove " + str(entry)
684 entry.repeated = False
687 # this sets the end time to current time, so timer will be stopped.
688 entry.autoincrease = False
691 if entry.state != entry.StateEnded:
692 self.timeChanged(entry)
694 print "state: ", entry.state
695 print "in processed: ", entry in self.processed_timers
696 print "in running: ", entry in self.timer_list
697 # autoincrease instanttimer if possible
698 if not entry.dontSave:
699 for x in self.timer_list:
700 if x.setAutoincreaseEnd():
702 # now the timer should be in the processed_timers list. remove it from there.
703 self.processed_timers.remove(entry)