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 isRecording(self):
446 for timer in self.timer_list:
447 if timer.isRunning() and not timer.justplay:
454 doc = xml.etree.cElementTree.parse(self.Filename)
456 from Tools.Notifications import AddPopup
457 from Screens.MessageBox import MessageBox
459 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
461 print "timers.xml failed to load!"
464 os.rename(self.Filename, self.Filename + "_old")
465 except (IOError, OSError):
466 print "renaming broken timer failed"
469 print "timers.xml not found!"
474 # put out a message when at least one timer overlaps
476 for timer in root.findall("timer"):
477 newTimer = createTimer(timer)
478 if (self.record(newTimer, True, True) is not None) and (checkit == True):
479 from Tools.Notifications import AddPopup
480 from Screens.MessageBox import MessageBox
481 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
482 checkit = False # at moment it is enough when the message is displayed one time
485 #root_element = xml.etree.cElementTree.Element('timers')
486 #root_element.text = "\n"
488 #for timer in self.timer_list + self.processed_timers:
489 # some timers (instant records) don't want to be saved.
493 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
494 #t.set("begin", str(int(timer.begin)))
495 #t.set("end", str(int(timer.end)))
496 #t.set("serviceref", str(timer.service_ref))
497 #t.set("repeated", str(timer.repeated))
498 #t.set("name", timer.name)
499 #t.set("description", timer.description)
500 #t.set("afterevent", str({
501 # AFTEREVENT.NONE: "nothing",
502 # AFTEREVENT.STANDBY: "standby",
503 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
504 # AFTEREVENT.AUTO: "auto"}))
505 #if timer.eit is not None:
506 # t.set("eit", str(timer.eit))
507 #if timer.dirname is not None:
508 # t.set("location", str(timer.dirname))
509 #t.set("disabled", str(int(timer.disabled)))
510 #t.set("justplay", str(int(timer.justplay)))
514 #for time, code, msg in timer.log_entries:
515 #l = xml.etree.cElementTree.SubElement(t, 'log')
516 #l.set("time", str(time))
517 #l.set("code", str(code))
521 #doc = xml.etree.cElementTree.ElementTree(root_element)
522 #doc.write(self.Filename)
526 list.append('<?xml version="1.0" ?>\n')
527 list.append('<timers>\n')
529 for timer in self.timer_list + self.processed_timers:
533 list.append('<timer')
534 list.append(' begin="' + str(int(timer.begin)) + '"')
535 list.append(' end="' + str(int(timer.end)) + '"')
536 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
537 list.append(' repeated="' + str(int(timer.repeated)) + '"')
538 list.append(' name="' + str(stringToXML(timer.name)) + '"')
539 list.append(' description="' + str(stringToXML(timer.description)) + '"')
540 list.append(' afterevent="' + str(stringToXML({
541 AFTEREVENT.NONE: "nothing",
542 AFTEREVENT.STANDBY: "standby",
543 AFTEREVENT.DEEPSTANDBY: "deepstandby",
544 AFTEREVENT.AUTO: "auto"
545 }[timer.afterEvent])) + '"')
546 if timer.eit is not None:
547 list.append(' eit="' + str(timer.eit) + '"')
548 if timer.dirname is not None:
549 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
550 if timer.tags is not None:
551 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
552 list.append(' disabled="' + str(int(timer.disabled)) + '"')
553 list.append(' justplay="' + str(int(timer.justplay)) + '"')
556 if config.recording.debug.value:
557 for time, code, msg in timer.log_entries:
559 list.append(' code="' + str(code) + '"')
560 list.append(' time="' + str(time) + '"')
562 list.append(str(stringToXML(msg)))
563 list.append('</log>\n')
565 list.append('</timer>\n')
567 list.append('</timers>\n')
569 file = open(self.Filename, "w")
574 def getNextZapTime(self):
576 for timer in self.timer_list:
577 if not timer.justplay or timer.begin < now:
582 def getNextRecordingTime(self):
584 for timer in self.timer_list:
585 next_act = timer.getNextActivation()
586 if timer.justplay or next_act < now:
591 def isNextRecordAfterEventActionAuto(self):
594 for timer in self.timer_list:
595 if timer.justplay or timer.begin < now:
597 if t is None or t.begin == timer.begin:
599 if t.afterEvent == AFTEREVENT.AUTO:
603 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
604 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
605 if not timersanitycheck.check():
606 if ignoreTSC != True:
607 print "timer conflict detected!"
608 print timersanitycheck.getSimulTimerList()
609 return timersanitycheck.getSimulTimerList()
611 print "ignore timer conflict"
612 elif timersanitycheck.doubleCheck():
613 print "ignore double timer"
616 print "[Timer] Record " + str(entry)
618 self.addTimerEntry(entry)
623 def isInTimer(self, eventid, begin, duration, service):
627 chktimecmp_end = None
628 end = begin + duration
629 refstr = str(service)
630 for x in self.timer_list:
631 check = x.service_ref.ref.toString() == refstr
633 sref = x.service_ref.ref
634 parent_sid = sref.getUnsignedData(5)
635 parent_tsid = sref.getUnsignedData(6)
636 if parent_sid and parent_tsid: # check for subservice
637 sid = sref.getUnsignedData(1)
638 tsid = sref.getUnsignedData(2)
639 sref.setUnsignedData(1, parent_sid)
640 sref.setUnsignedData(2, parent_tsid)
641 sref.setUnsignedData(5, 0)
642 sref.setUnsignedData(6, 0)
643 check = sref.toCompareString() == refstr
647 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
648 num = event and event.getNumOfLinkageServices() or 0
649 sref.setUnsignedData(1, sid)
650 sref.setUnsignedData(2, tsid)
651 sref.setUnsignedData(5, parent_sid)
652 sref.setUnsignedData(6, parent_tsid)
653 for cnt in range(num):
654 subservice = event.getLinkageService(sref, cnt)
655 if sref.toCompareString() == subservice.toCompareString():
661 chktime = localtime(begin)
662 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
663 chktimecmp_end = chktimecmp + (duration / 60)
664 time = localtime(x.begin)
665 for y in (0, 1, 2, 3, 4, 5, 6):
666 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
667 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
668 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
669 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
670 elif chktimecmp <= timecmp < chktimecmp_end:
671 time_match = (chktimecmp_end - timecmp) * 60
672 else: #if x.eit is None:
673 if begin <= x.begin <= end:
675 if time_match < diff:
677 elif x.begin <= begin <= x.end:
679 if time_match < diff:
685 def removeEntry(self, entry):
686 print "[Timer] Remove " + str(entry)
689 entry.repeated = False
692 # this sets the end time to current time, so timer will be stopped.
693 entry.autoincrease = False
696 if entry.state != entry.StateEnded:
697 self.timeChanged(entry)
699 print "state: ", entry.state
700 print "in processed: ", entry in self.processed_timers
701 print "in running: ", entry in self.timer_list
702 # autoincrease instanttimer if possible
703 if not entry.dontSave:
704 for x in self.timer_list:
705 if x.setAutoincreaseEnd():
707 # now the timer should be in the processed_timers list. remove it from there.
708 self.processed_timers.remove(entry)