1 from enigma import eEPGCache, getBestPlayableServiceReference, \
2 eServiceReference, iRecordableService, quitMainloop
4 from Components.config import config
5 from Components.UsageConfig import defaultMoviePath
6 from Components.TimerSanityCheck import TimerSanityCheck
8 from Screens.MessageBox import MessageBox
10 from Tools import Directories, Notifications, ASCIItranslit
11 from Tools.XMLTools import stringToXML
14 import xml.etree.cElementTree
15 import NavigationInstance
16 from ServiceReference import ServiceReference
18 from time import localtime, strftime, ctime, time
19 from bisect import insort
21 # ok, for descriptions etc we have:
22 # service reference (to get the service name)
24 # description (description)
25 # event data (ONLY for time adjustments etc.)
28 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
29 # begin and end will be corrected
30 def parseEvent(ev, description = True):
32 name = ev.getEventName()
33 description = ev.getShortDescription()
37 begin = ev.getBeginTime()
38 end = begin + ev.getDuration()
40 begin -= config.recording.margin_before.value * 60
41 end += config.recording.margin_after.value * 60
42 return (begin, end, name, description, eit)
50 # please do not translate log messages
51 class RecordTimerEntry(timer.TimerEntry, object):
52 ######### the following static methods and members are only in use when the box is in (soft) standby
53 receiveRecordEvents = False
60 def staticGotRecordEvent(recservice, event):
61 if event == iRecordableService.evEnd:
62 print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
63 recordings = NavigationInstance.instance.getRecordings()
64 if not recordings: # no more recordings exist
65 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
66 if rec_time > 0 and (rec_time - time()) < 360:
67 print "another recording starts in", rec_time - time(), "seconds... do not shutdown yet"
69 print "no starting records in the next 360 seconds... immediate shutdown"
70 RecordTimerEntry.shutdown() # immediate shutdown
71 elif event == iRecordableService.evStart:
72 print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
75 def stopTryQuitMainloop():
76 print "RecordTimer.stopTryQuitMainloop"
77 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
78 RecordTimerEntry.receiveRecordEvents = False
81 def TryQuitMainloop(default_yes = True):
82 if not RecordTimerEntry.receiveRecordEvents:
83 print "RecordTimer.TryQuitMainloop"
84 NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
85 RecordTimerEntry.receiveRecordEvents = True
86 # send fake event.. to check if another recordings are running or
87 # other timers start in a few seconds
88 RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
89 # send normal notification for the case the user leave the standby now..
90 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop, default_yes = default_yes)
91 #################################################################
93 def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.AUTO, checkOldTimers = False, dirname = None, tags = None):
94 timer.TimerEntry.__init__(self, int(begin), int(end))
96 if checkOldTimers == True:
97 if self.begin < time() - 1209600:
98 self.begin = int(time())
100 if self.end < self.begin:
101 self.end = self.begin
103 assert isinstance(serviceref, ServiceReference)
105 if serviceref.isRecordable():
106 self.service_ref = serviceref
108 self.service_ref = ServiceReference(None)
110 self.dontSave = False
112 self.description = description
113 self.disabled = disabled
115 self.__record_service = None
116 self.start_prepare = 0
117 self.justplay = justplay
118 self.afterEvent = afterEvent
119 self.dirname = dirname
120 self.dirnameHadToFallback = False
121 self.autoincrease = False
122 self.autoincreasetime = 3600 * 24 # 1 day
123 self.tags = tags or []
125 self.log_entries = []
128 def log(self, code, msg):
129 self.log_entries.append((int(time()), code, msg))
132 def calculateFilename(self):
133 service_name = self.service_ref.getServiceName()
134 begin_date = strftime("%Y%m%d %H%M", localtime(self.begin))
135 begin_shortdate = strftime("%Y%m%d", localtime(self.begin))
137 print "begin_date: ", begin_date
138 print "service_name: ", service_name
139 print "name:", self.name
140 print "description: ", self.description
142 filename = begin_date + " - " + service_name
144 if config.usage.setup_level.index >= 2: # expert+
145 if config.recording.filename_composition.value == "short":
146 filename = begin_shortdate + " - " + self.name
147 elif config.recording.filename_composition.value == "long":
148 filename += " - " + self.name + " - " + self.description
150 filename += " - " + self.name # standard
152 filename += " - " + self.name
154 if config.recording.ascii_filenames.value:
155 filename = ASCIItranslit.legacyEncode(filename)
157 if not self.dirname or not Directories.fileExists(self.dirname, 'w'):
159 self.dirnameHadToFallback = True
160 dirname = defaultMoviePath()
162 dirname = self.dirname
163 self.Filename = Directories.getRecordingFilename(filename, dirname)
164 self.log(0, "Filename calculated as: '%s'" % self.Filename)
165 #begin_date + " - " + service_name + description)
167 def tryPrepare(self):
171 self.calculateFilename()
172 rec_ref = self.service_ref and self.service_ref.ref
173 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
174 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
176 self.log(1, "'get best playable service for group... record' failed")
179 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
181 if not self.record_service:
182 self.log(1, "'record service' failed")
186 epgcache = eEPGCache.getInstance()
187 queryTime=self.begin+(self.end-self.begin)/2
188 evt = epgcache.lookupEventTime(rec_ref, queryTime)
190 self.description = evt.getShortDescription()
191 event_id = evt.getEventId()
199 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))
202 self.log(4, "failed to write meta information")
204 self.log(2, "'prepare' failed: error %d" % prep_res)
206 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
207 # the next start time in evEnd event handler...
209 self.start_prepare = time() + self.backoff
211 NavigationInstance.instance.stopRecordService(self.record_service)
212 self.record_service = None
216 def do_backoff(self):
217 if self.backoff == 0:
221 if self.backoff > 100:
223 self.log(10, "backoff: retry in %d seconds" % self.backoff)
226 next_state = self.state + 1
227 self.log(5, "activating state %d" % next_state)
229 if next_state == self.StatePrepared:
230 if self.tryPrepare():
231 self.log(6, "prepare ok, waiting for begin")
232 # create file to "reserve" the filename
233 # because another recording at the same time on another service can try to record the same event
234 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
235 # here than calculateFilename is happy
236 if not self.justplay:
237 open(self.Filename + ".ts", "w").close()
238 # fine. it worked, resources are allocated.
239 self.next_activation = self.begin
243 self.log(7, "prepare failed")
244 if self.first_try_prepare:
245 self.first_try_prepare = False
246 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
247 if cur_ref and not cur_ref.getPath():
248 if not config.recording.asktozap.value:
249 self.log(8, "asking user to zap away")
250 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
251 else: # zap without asking
252 self.log(9, "zap without asking")
253 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
256 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
258 self.log(8, "currently no service running... so we dont need to stop it")
260 elif next_state == self.StateRunning:
261 # if this timer has been cancelled, just go to "end" state.
266 if Screens.Standby.inStandby:
267 self.log(11, "wakeup and zap")
268 #set service to zap after standby
269 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
271 Screens.Standby.inStandby.Power()
273 self.log(11, "zapping")
274 NavigationInstance.instance.playService(self.service_ref.ref)
277 self.log(11, "start recording")
278 record_res = self.record_service.start()
281 self.log(13, "start record returned %d" % record_res)
284 self.begin = time() + self.backoff
288 elif next_state == self.StateEnded:
290 if self.setAutoincreaseEnd():
291 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
294 self.log(12, "stop recording")
295 if not self.justplay:
296 NavigationInstance.instance.stopRecordService(self.record_service)
297 self.record_service = None
298 if self.afterEvent == AFTEREVENT.STANDBY:
299 if not Screens.Standby.inStandby: # not already in standby
300 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
301 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
302 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
303 if Screens.Standby.inStandby: # in standby
304 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
306 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
309 def setAutoincreaseEnd(self, entry = None):
310 if not self.autoincrease:
313 new_end = int(time()) + self.autoincreasetime
315 new_end = entry.begin -30
317 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)
318 dummyentry.disabled = self.disabled
319 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
320 if not timersanitycheck.check():
321 simulTimerList = timersanitycheck.getSimulTimerList()
322 if simulTimerList is not None and len(simulTimerList) > 1:
323 new_end = simulTimerList[1].begin
324 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
325 if new_end <= time():
330 def sendStandbyNotification(self, answer):
332 Notifications.AddNotification(Screens.Standby.Standby)
334 def sendTryQuitMainloopNotification(self, answer):
336 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
338 def getNextActivation(self):
339 if self.state == self.StateEnded:
342 next_state = self.state + 1
344 return {self.StatePrepared: self.start_prepare,
345 self.StateRunning: self.begin,
346 self.StateEnded: self.end }[next_state]
348 def failureCB(self, answer):
350 self.log(13, "ok, zapped away")
351 #NavigationInstance.instance.stopUserServices()
352 NavigationInstance.instance.playService(self.service_ref.ref)
354 self.log(14, "user didn't want to zap away, record will probably fail")
356 def timeChanged(self):
357 old_prepare = self.start_prepare
358 self.start_prepare = self.begin - self.prepare_time
361 if int(old_prepare) != int(self.start_prepare):
362 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
364 def gotRecordEvent(self, record, event):
365 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
366 if self.__record_service.__deref__() != record.__deref__():
368 self.log(16, "record event %d" % event)
369 if event == iRecordableService.evRecordWriteError:
370 print "WRITE ERROR on recording, disk full?"
371 # show notification. the 'id' will make sure that it will be
372 # displayed only once, even if more timers are failing at the
373 # same time. (which is very likely in case of disk fullness)
374 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
375 # ok, the recording has been stopped. we need to properly note
376 # that in our state, with also keeping the possibility to re-try.
377 # TODO: this has to be done.
378 elif event == iRecordableService.evStart:
379 text = _("A record has been started:\n%s") % self.name
380 if self.dirnameHadToFallback:
381 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
383 if config.usage.show_message_when_recording_starts.value:
384 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
386 # we have record_service as property to automatically subscribe to record service events
387 def setRecordService(self, service):
388 if self.__record_service is not None:
389 print "[remove callback]"
390 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
392 self.__record_service = service
394 if self.__record_service is not None:
395 print "[add callback]"
396 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
398 record_service = property(lambda self: self.__record_service, setRecordService)
400 def createTimer(xml):
401 begin = int(xml.get("begin"))
402 end = int(xml.get("end"))
403 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
404 description = xml.get("description").encode("utf-8")
405 repeated = xml.get("repeated").encode("utf-8")
406 disabled = long(xml.get("disabled") or "0")
407 justplay = long(xml.get("justplay") or "0")
408 afterevent = str(xml.get("afterevent") or "nothing")
410 "nothing": AFTEREVENT.NONE,
411 "standby": AFTEREVENT.STANDBY,
412 "deepstandby": AFTEREVENT.DEEPSTANDBY,
413 "auto": AFTEREVENT.AUTO
416 if eit and eit != "None":
420 location = xml.get("location")
421 if location and location != "None":
422 location = location.encode("utf-8")
425 tags = xml.get("tags")
426 if tags and tags != "None":
427 tags = tags.encode("utf-8").split(' ')
431 name = xml.get("name").encode("utf-8")
432 #filename = xml.get("filename").encode("utf-8")
433 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
434 entry.repeated = int(repeated)
436 for l in xml.findall("log"):
437 time = int(l.get("time"))
438 code = int(l.get("code"))
439 msg = l.text.strip().encode("utf-8")
440 entry.log_entries.append((time, code, msg))
444 class RecordTimer(timer.Timer):
446 timer.Timer.__init__(self)
448 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
453 print "unable to load timers from file!"
455 def doActivate(self, w):
456 # when activating a timer which has already passed,
457 # simply abort the timer. don't run trough all the stages.
459 w.state = RecordTimerEntry.StateEnded
461 # when active returns true, this means "accepted".
462 # otherwise, the current state is kept.
463 # the timer entry itself will fix up the delay then.
467 self.timer_list.remove(w)
469 # did this timer reached the last state?
470 if w.state < RecordTimerEntry.StateEnded:
471 # no, sort it into active list
472 insort(self.timer_list, w)
474 # yes. Process repeated, and re-add.
477 w.state = RecordTimerEntry.StateWaiting
478 self.addTimerEntry(w)
480 insort(self.processed_timers, w)
484 def isRecording(self):
486 for timer in self.timer_list:
487 if timer.isRunning() and not timer.justplay:
494 doc = xml.etree.cElementTree.parse(self.Filename)
496 from Tools.Notifications import AddPopup
497 from Screens.MessageBox import MessageBox
499 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
501 print "timers.xml failed to load!"
504 os.rename(self.Filename, self.Filename + "_old")
505 except (IOError, OSError):
506 print "renaming broken timer failed"
509 print "timers.xml not found!"
514 # put out a message when at least one timer overlaps
516 for timer in root.findall("timer"):
517 newTimer = createTimer(timer)
518 if (self.record(newTimer, True, dosave=False) is not None) and (checkit == True):
519 from Tools.Notifications import AddPopup
520 from Screens.MessageBox import MessageBox
521 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
522 checkit = False # at moment it is enough when the message is displayed one time
525 #root_element = xml.etree.cElementTree.Element('timers')
526 #root_element.text = "\n"
528 #for timer in self.timer_list + self.processed_timers:
529 # some timers (instant records) don't want to be saved.
533 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
534 #t.set("begin", str(int(timer.begin)))
535 #t.set("end", str(int(timer.end)))
536 #t.set("serviceref", str(timer.service_ref))
537 #t.set("repeated", str(timer.repeated))
538 #t.set("name", timer.name)
539 #t.set("description", timer.description)
540 #t.set("afterevent", str({
541 # AFTEREVENT.NONE: "nothing",
542 # AFTEREVENT.STANDBY: "standby",
543 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
544 # AFTEREVENT.AUTO: "auto"}))
545 #if timer.eit is not None:
546 # t.set("eit", str(timer.eit))
547 #if timer.dirname is not None:
548 # t.set("location", str(timer.dirname))
549 #t.set("disabled", str(int(timer.disabled)))
550 #t.set("justplay", str(int(timer.justplay)))
554 #for time, code, msg in timer.log_entries:
555 #l = xml.etree.cElementTree.SubElement(t, 'log')
556 #l.set("time", str(time))
557 #l.set("code", str(code))
561 #doc = xml.etree.cElementTree.ElementTree(root_element)
562 #doc.write(self.Filename)
566 list.append('<?xml version="1.0" ?>\n')
567 list.append('<timers>\n')
569 for timer in self.timer_list + self.processed_timers:
573 list.append('<timer')
574 list.append(' begin="' + str(int(timer.begin)) + '"')
575 list.append(' end="' + str(int(timer.end)) + '"')
576 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
577 list.append(' repeated="' + str(int(timer.repeated)) + '"')
578 list.append(' name="' + str(stringToXML(timer.name)) + '"')
579 list.append(' description="' + str(stringToXML(timer.description)) + '"')
580 list.append(' afterevent="' + str(stringToXML({
581 AFTEREVENT.NONE: "nothing",
582 AFTEREVENT.STANDBY: "standby",
583 AFTEREVENT.DEEPSTANDBY: "deepstandby",
584 AFTEREVENT.AUTO: "auto"
585 }[timer.afterEvent])) + '"')
586 if timer.eit is not None:
587 list.append(' eit="' + str(timer.eit) + '"')
588 if timer.dirname is not None:
589 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
590 if timer.tags is not None:
591 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
592 list.append(' disabled="' + str(int(timer.disabled)) + '"')
593 list.append(' justplay="' + str(int(timer.justplay)) + '"')
596 if config.recording.debug.value:
597 for time, code, msg in timer.log_entries:
599 list.append(' code="' + str(code) + '"')
600 list.append(' time="' + str(time) + '"')
602 list.append(str(stringToXML(msg)))
603 list.append('</log>\n')
605 list.append('</timer>\n')
607 list.append('</timers>\n')
609 file = open(self.Filename, "w")
614 def getNextZapTime(self):
616 for timer in self.timer_list:
617 if not timer.justplay or timer.begin < now:
622 def getNextRecordingTime(self):
624 for timer in self.timer_list:
625 next_act = timer.getNextActivation()
626 if timer.justplay or next_act < now:
631 def isNextRecordAfterEventActionAuto(self):
634 for timer in self.timer_list:
635 if timer.justplay or timer.begin < now:
637 if t is None or t.begin == timer.begin:
639 if t.afterEvent == AFTEREVENT.AUTO:
643 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
644 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
645 if not timersanitycheck.check():
646 if ignoreTSC != True:
647 print "timer conflict detected!"
648 print timersanitycheck.getSimulTimerList()
649 return timersanitycheck.getSimulTimerList()
651 print "ignore timer conflict"
652 elif timersanitycheck.doubleCheck():
653 print "ignore double timer"
656 print "[Timer] Record " + str(entry)
658 self.addTimerEntry(entry)
663 def isInTimer(self, eventid, begin, duration, service):
667 chktimecmp_end = None
668 end = begin + duration
669 refstr = str(service)
670 for x in self.timer_list:
671 check = x.service_ref.ref.toString() == refstr
673 sref = x.service_ref.ref
674 parent_sid = sref.getUnsignedData(5)
675 parent_tsid = sref.getUnsignedData(6)
676 if parent_sid and parent_tsid: # check for subservice
677 sid = sref.getUnsignedData(1)
678 tsid = sref.getUnsignedData(2)
679 sref.setUnsignedData(1, parent_sid)
680 sref.setUnsignedData(2, parent_tsid)
681 sref.setUnsignedData(5, 0)
682 sref.setUnsignedData(6, 0)
683 check = sref.toCompareString() == refstr
687 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
688 num = event and event.getNumOfLinkageServices() or 0
689 sref.setUnsignedData(1, sid)
690 sref.setUnsignedData(2, tsid)
691 sref.setUnsignedData(5, parent_sid)
692 sref.setUnsignedData(6, parent_tsid)
693 for cnt in range(num):
694 subservice = event.getLinkageService(sref, cnt)
695 if sref.toCompareString() == subservice.toCompareString():
701 chktime = localtime(begin)
702 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
703 chktimecmp_end = chktimecmp + (duration / 60)
704 time = localtime(x.begin)
705 for y in (0, 1, 2, 3, 4, 5, 6):
706 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
707 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
708 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
709 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
710 elif chktimecmp <= timecmp < chktimecmp_end:
711 time_match = (chktimecmp_end - timecmp) * 60
712 else: #if x.eit is None:
713 if begin <= x.begin <= end:
715 if time_match < diff:
717 elif x.begin <= begin <= x.end:
719 if time_match < diff:
725 def removeEntry(self, entry):
726 print "[Timer] Remove " + str(entry)
729 entry.repeated = False
732 # this sets the end time to current time, so timer will be stopped.
733 entry.autoincrease = False
736 if entry.state != entry.StateEnded:
737 self.timeChanged(entry)
739 print "state: ", entry.state
740 print "in processed: ", entry in self.processed_timers
741 print "in running: ", entry in self.timer_list
742 # autoincrease instanttimer if possible
743 if not entry.dontSave:
744 for x in self.timer_list:
745 if x.setAutoincreaseEnd():
747 # now the timer should be in the processed_timers list. remove it from there.
748 self.processed_timers.remove(entry)