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 self.service_ref = serviceref
107 self.dontSave = False
109 self.description = description
110 self.disabled = disabled
112 self.__record_service = None
113 self.start_prepare = 0
114 self.justplay = justplay
115 self.afterEvent = afterEvent
116 self.dirname = dirname
117 self.dirnameHadToFallback = False
118 self.autoincrease = False
119 self.autoincreasetime = 3600 * 24 # 1 day
120 self.tags = tags or []
122 self.log_entries = []
125 def log(self, code, msg):
126 self.log_entries.append((int(time()), code, msg))
129 def calculateFilename(self):
130 service_name = self.service_ref.getServiceName()
131 begin_date = strftime("%Y%m%d %H%M", localtime(self.begin))
132 begin_shortdate = strftime("%Y%m%d", localtime(self.begin))
134 print "begin_date: ", begin_date
135 print "service_name: ", service_name
136 print "name:", self.name
137 print "description: ", self.description
139 filename = begin_date + " - " + service_name
141 filename += " - " + self.name
142 if config.usage.setup_level.index >= 2: # expert+
143 if config.recording.filename_composition.value == "short":
144 filename = begin_shortdate + " - " + self.name
145 elif config.recording.filename_composition.value == "long":
146 filename = begin_date + " - " + service_name + " - " + self.name + " - " + self.description
148 filename += " - " + self.name # standard
150 if config.recording.ascii_filenames.value:
151 filename = ASCIItranslit.legacyEncode(filename)
153 if not self.dirname or not Directories.fileExists(self.dirname, 'w'):
155 self.dirnameHadToFallback = True
156 dirname = defaultMoviePath()
158 dirname = self.dirname
159 self.Filename = Directories.getRecordingFilename(filename, dirname)
160 self.log(0, "Filename calculated as: '%s'" % self.Filename)
161 #begin_date + " - " + service_name + description)
163 def tryPrepare(self):
167 self.calculateFilename()
168 rec_ref = self.service_ref and self.service_ref.ref
169 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
170 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
172 self.log(1, "'get best playable service for group... record' failed")
175 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
177 if not self.record_service:
178 self.log(1, "'record service' failed")
182 epgcache = eEPGCache.getInstance()
183 queryTime=self.begin+(self.end-self.begin)/2
184 evt = epgcache.lookupEventTime(rec_ref, queryTime)
186 self.description = evt.getShortDescription()
187 event_id = evt.getEventId()
195 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))
198 self.log(4, "failed to write meta information")
200 self.log(2, "'prepare' failed: error %d" % prep_res)
202 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
203 # the next start time in evEnd event handler...
205 self.start_prepare = time() + self.backoff
207 NavigationInstance.instance.stopRecordService(self.record_service)
208 self.record_service = None
212 def do_backoff(self):
213 if self.backoff == 0:
217 if self.backoff > 100:
219 self.log(10, "backoff: retry in %d seconds" % self.backoff)
222 next_state = self.state + 1
223 self.log(5, "activating state %d" % next_state)
225 if next_state == self.StatePrepared:
226 if self.tryPrepare():
227 self.log(6, "prepare ok, waiting for begin")
228 # create file to "reserve" the filename
229 # because another recording at the same time on another service can try to record the same event
230 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
231 # here than calculateFilename is happy
232 if not self.justplay:
233 open(self.Filename + ".ts", "w").close()
234 # fine. it worked, resources are allocated.
235 self.next_activation = self.begin
239 self.log(7, "prepare failed")
240 if self.first_try_prepare:
241 self.first_try_prepare = False
242 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
243 if cur_ref and not cur_ref.getPath():
244 if not config.recording.asktozap.value:
245 self.log(8, "asking user to zap away")
246 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
247 else: # zap without asking
248 self.log(9, "zap without asking")
249 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
252 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
254 self.log(8, "currently no service running... so we dont need to stop it")
256 elif next_state == self.StateRunning:
257 # if this timer has been cancelled, just go to "end" state.
262 if Screens.Standby.inStandby:
263 self.log(11, "wakeup and zap")
264 #set service to zap after standby
265 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
267 Screens.Standby.inStandby.Power()
269 self.log(11, "zapping")
270 NavigationInstance.instance.playService(self.service_ref.ref)
273 self.log(11, "start recording")
274 record_res = self.record_service.start()
277 self.log(13, "start record returned %d" % record_res)
280 self.begin = time() + self.backoff
284 elif next_state == self.StateEnded:
286 if self.setAutoincreaseEnd():
287 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
290 self.log(12, "stop recording")
291 if not self.justplay:
292 NavigationInstance.instance.stopRecordService(self.record_service)
293 self.record_service = None
294 if self.afterEvent == AFTEREVENT.STANDBY:
295 if not Screens.Standby.inStandby: # not already in standby
296 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
297 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
298 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
299 if Screens.Standby.inStandby: # in standby
300 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
302 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
305 def setAutoincreaseEnd(self, entry = None):
306 if not self.autoincrease:
309 new_end = int(time()) + self.autoincreasetime
311 new_end = entry.begin -30
313 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)
314 dummyentry.disabled = self.disabled
315 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
316 if not timersanitycheck.check():
317 simulTimerList = timersanitycheck.getSimulTimerList()
318 new_end = simulTimerList[1].begin
320 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
322 if new_end <= time():
328 def sendStandbyNotification(self, answer):
330 Notifications.AddNotification(Screens.Standby.Standby)
332 def sendTryQuitMainloopNotification(self, answer):
334 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
336 def getNextActivation(self):
337 if self.state == self.StateEnded:
340 next_state = self.state + 1
342 return {self.StatePrepared: self.start_prepare,
343 self.StateRunning: self.begin,
344 self.StateEnded: self.end }[next_state]
346 def failureCB(self, answer):
348 self.log(13, "ok, zapped away")
349 #NavigationInstance.instance.stopUserServices()
350 NavigationInstance.instance.playService(self.service_ref.ref)
352 self.log(14, "user didn't want to zap away, record will probably fail")
354 def timeChanged(self):
355 old_prepare = self.start_prepare
356 self.start_prepare = self.begin - self.prepare_time
359 if int(old_prepare) != int(self.start_prepare):
360 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
362 def gotRecordEvent(self, record, event):
363 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
364 if self.__record_service.__deref__() != record.__deref__():
366 self.log(16, "record event %d" % event)
367 if event == iRecordableService.evRecordWriteError:
368 print "WRITE ERROR on recording, disk full?"
369 # show notification. the 'id' will make sure that it will be
370 # displayed only once, even if more timers are failing at the
371 # same time. (which is very likely in case of disk fullness)
372 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
373 # ok, the recording has been stopped. we need to properly note
374 # that in our state, with also keeping the possibility to re-try.
375 # TODO: this has to be done.
376 elif event == iRecordableService.evStart:
377 text = _("A record has been started:\n%s") % self.name
378 if self.dirnameHadToFallback:
379 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
381 if config.usage.show_message_when_recording_starts.value:
382 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
384 # we have record_service as property to automatically subscribe to record service events
385 def setRecordService(self, service):
386 if self.__record_service is not None:
387 print "[remove callback]"
388 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
390 self.__record_service = service
392 if self.__record_service is not None:
393 print "[add callback]"
394 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
396 record_service = property(lambda self: self.__record_service, setRecordService)
398 def createTimer(xml):
399 begin = int(xml.get("begin"))
400 end = int(xml.get("end"))
401 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
402 description = xml.get("description").encode("utf-8")
403 repeated = xml.get("repeated").encode("utf-8")
404 disabled = long(xml.get("disabled") or "0")
405 justplay = long(xml.get("justplay") or "0")
406 afterevent = str(xml.get("afterevent") or "nothing")
408 "nothing": AFTEREVENT.NONE,
409 "standby": AFTEREVENT.STANDBY,
410 "deepstandby": AFTEREVENT.DEEPSTANDBY,
411 "auto": AFTEREVENT.AUTO
414 if eit and eit != "None":
418 location = xml.get("location")
419 if location and location != "None":
420 location = location.encode("utf-8")
423 tags = xml.get("tags")
424 if tags and tags != "None":
425 tags = tags.encode("utf-8").split(' ')
429 name = xml.get("name").encode("utf-8")
430 #filename = xml.get("filename").encode("utf-8")
431 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
432 entry.repeated = int(repeated)
434 for l in xml.findall("log"):
435 time = int(l.get("time"))
436 code = int(l.get("code"))
437 msg = l.text.strip().encode("utf-8")
438 entry.log_entries.append((time, code, msg))
442 class RecordTimer(timer.Timer):
444 timer.Timer.__init__(self)
446 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
451 print "unable to load timers from file!"
453 def doActivate(self, w):
454 # when activating a timer which has already passed,
455 # simply abort the timer. don't run trough all the stages.
457 w.state = RecordTimerEntry.StateEnded
459 # when active returns true, this means "accepted".
460 # otherwise, the current state is kept.
461 # the timer entry itself will fix up the delay then.
465 self.timer_list.remove(w)
467 # did this timer reached the last state?
468 if w.state < RecordTimerEntry.StateEnded:
469 # no, sort it into active list
470 insort(self.timer_list, w)
472 # yes. Process repeated, and re-add.
475 w.state = RecordTimerEntry.StateWaiting
476 self.addTimerEntry(w)
478 insort(self.processed_timers, w)
482 def isRecording(self):
484 for timer in self.timer_list:
485 if timer.isRunning() and not timer.justplay:
492 doc = xml.etree.cElementTree.parse(self.Filename)
494 from Tools.Notifications import AddPopup
495 from Screens.MessageBox import MessageBox
497 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
499 print "timers.xml failed to load!"
502 os.rename(self.Filename, self.Filename + "_old")
503 except (IOError, OSError):
504 print "renaming broken timer failed"
507 print "timers.xml not found!"
512 # put out a message when at least one timer overlaps
514 for timer in root.findall("timer"):
515 newTimer = createTimer(timer)
516 if (self.record(newTimer, True, True) is not None) and (checkit == True):
517 from Tools.Notifications import AddPopup
518 from Screens.MessageBox import MessageBox
519 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
520 checkit = False # at moment it is enough when the message is displayed one time
523 #root_element = xml.etree.cElementTree.Element('timers')
524 #root_element.text = "\n"
526 #for timer in self.timer_list + self.processed_timers:
527 # some timers (instant records) don't want to be saved.
531 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
532 #t.set("begin", str(int(timer.begin)))
533 #t.set("end", str(int(timer.end)))
534 #t.set("serviceref", str(timer.service_ref))
535 #t.set("repeated", str(timer.repeated))
536 #t.set("name", timer.name)
537 #t.set("description", timer.description)
538 #t.set("afterevent", str({
539 # AFTEREVENT.NONE: "nothing",
540 # AFTEREVENT.STANDBY: "standby",
541 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
542 # AFTEREVENT.AUTO: "auto"}))
543 #if timer.eit is not None:
544 # t.set("eit", str(timer.eit))
545 #if timer.dirname is not None:
546 # t.set("location", str(timer.dirname))
547 #t.set("disabled", str(int(timer.disabled)))
548 #t.set("justplay", str(int(timer.justplay)))
552 #for time, code, msg in timer.log_entries:
553 #l = xml.etree.cElementTree.SubElement(t, 'log')
554 #l.set("time", str(time))
555 #l.set("code", str(code))
559 #doc = xml.etree.cElementTree.ElementTree(root_element)
560 #doc.write(self.Filename)
564 list.append('<?xml version="1.0" ?>\n')
565 list.append('<timers>\n')
567 for timer in self.timer_list + self.processed_timers:
571 list.append('<timer')
572 list.append(' begin="' + str(int(timer.begin)) + '"')
573 list.append(' end="' + str(int(timer.end)) + '"')
574 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
575 list.append(' repeated="' + str(int(timer.repeated)) + '"')
576 list.append(' name="' + str(stringToXML(timer.name)) + '"')
577 list.append(' description="' + str(stringToXML(timer.description)) + '"')
578 list.append(' afterevent="' + str(stringToXML({
579 AFTEREVENT.NONE: "nothing",
580 AFTEREVENT.STANDBY: "standby",
581 AFTEREVENT.DEEPSTANDBY: "deepstandby",
582 AFTEREVENT.AUTO: "auto"
583 }[timer.afterEvent])) + '"')
584 if timer.eit is not None:
585 list.append(' eit="' + str(timer.eit) + '"')
586 if timer.dirname is not None:
587 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
588 if timer.tags is not None:
589 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
590 list.append(' disabled="' + str(int(timer.disabled)) + '"')
591 list.append(' justplay="' + str(int(timer.justplay)) + '"')
594 if config.recording.debug.value:
595 for time, code, msg in timer.log_entries:
597 list.append(' code="' + str(code) + '"')
598 list.append(' time="' + str(time) + '"')
600 list.append(str(stringToXML(msg)))
601 list.append('</log>\n')
603 list.append('</timer>\n')
605 list.append('</timers>\n')
607 file = open(self.Filename, "w")
612 def getNextZapTime(self):
614 for timer in self.timer_list:
615 if not timer.justplay or timer.begin < now:
620 def getNextRecordingTime(self):
622 for timer in self.timer_list:
623 next_act = timer.getNextActivation()
624 if timer.justplay or next_act < now:
629 def isNextRecordAfterEventActionAuto(self):
632 for timer in self.timer_list:
633 if timer.justplay or timer.begin < now:
635 if t is None or t.begin == timer.begin:
637 if t.afterEvent == AFTEREVENT.AUTO:
641 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
642 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
643 if not timersanitycheck.check():
644 if ignoreTSC != True:
645 print "timer conflict detected!"
646 print timersanitycheck.getSimulTimerList()
647 return timersanitycheck.getSimulTimerList()
649 print "ignore timer conflict"
650 elif timersanitycheck.doubleCheck():
651 print "ignore double timer"
654 print "[Timer] Record " + str(entry)
656 self.addTimerEntry(entry)
661 def isInTimer(self, eventid, begin, duration, service):
665 chktimecmp_end = None
666 end = begin + duration
667 refstr = str(service)
668 for x in self.timer_list:
669 check = x.service_ref.ref.toString() == refstr
671 sref = x.service_ref.ref
672 parent_sid = sref.getUnsignedData(5)
673 parent_tsid = sref.getUnsignedData(6)
674 if parent_sid and parent_tsid: # check for subservice
675 sid = sref.getUnsignedData(1)
676 tsid = sref.getUnsignedData(2)
677 sref.setUnsignedData(1, parent_sid)
678 sref.setUnsignedData(2, parent_tsid)
679 sref.setUnsignedData(5, 0)
680 sref.setUnsignedData(6, 0)
681 check = sref.toCompareString() == refstr
685 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
686 num = event and event.getNumOfLinkageServices() or 0
687 sref.setUnsignedData(1, sid)
688 sref.setUnsignedData(2, tsid)
689 sref.setUnsignedData(5, parent_sid)
690 sref.setUnsignedData(6, parent_tsid)
691 for cnt in range(num):
692 subservice = event.getLinkageService(sref, cnt)
693 if sref.toCompareString() == subservice.toCompareString():
699 chktime = localtime(begin)
700 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
701 chktimecmp_end = chktimecmp + (duration / 60)
702 time = localtime(x.begin)
703 for y in (0, 1, 2, 3, 4, 5, 6):
704 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
705 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
706 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
707 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
708 elif chktimecmp <= timecmp < chktimecmp_end:
709 time_match = (chktimecmp_end - timecmp) * 60
710 else: #if x.eit is None:
711 if begin <= x.begin <= end:
713 if time_match < diff:
715 elif x.begin <= begin <= x.end:
717 if time_match < diff:
723 def removeEntry(self, entry):
724 print "[Timer] Remove " + str(entry)
727 entry.repeated = False
730 # this sets the end time to current time, so timer will be stopped.
731 entry.autoincrease = False
734 if entry.state != entry.StateEnded:
735 self.timeChanged(entry)
737 print "state: ", entry.state
738 print "in processed: ", entry in self.processed_timers
739 print "in running: ", entry in self.timer_list
740 # autoincrease instanttimer if possible
741 if not entry.dontSave:
742 for x in self.timer_list:
743 if x.setAutoincreaseEnd():
745 # now the timer should be in the processed_timers list. remove it from there.
746 self.processed_timers.remove(entry)