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 new_end = simulTimerList[1].begin
324 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
326 if new_end <= time():
332 def sendStandbyNotification(self, answer):
334 Notifications.AddNotification(Screens.Standby.Standby)
336 def sendTryQuitMainloopNotification(self, answer):
338 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
340 def getNextActivation(self):
341 if self.state == self.StateEnded:
344 next_state = self.state + 1
346 return {self.StatePrepared: self.start_prepare,
347 self.StateRunning: self.begin,
348 self.StateEnded: self.end }[next_state]
350 def failureCB(self, answer):
352 self.log(13, "ok, zapped away")
353 #NavigationInstance.instance.stopUserServices()
354 NavigationInstance.instance.playService(self.service_ref.ref)
356 self.log(14, "user didn't want to zap away, record will probably fail")
358 def timeChanged(self):
359 old_prepare = self.start_prepare
360 self.start_prepare = self.begin - self.prepare_time
363 if int(old_prepare) != int(self.start_prepare):
364 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
366 def gotRecordEvent(self, record, event):
367 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
368 if self.__record_service.__deref__() != record.__deref__():
370 self.log(16, "record event %d" % event)
371 if event == iRecordableService.evRecordWriteError:
372 print "WRITE ERROR on recording, disk full?"
373 # show notification. the 'id' will make sure that it will be
374 # displayed only once, even if more timers are failing at the
375 # same time. (which is very likely in case of disk fullness)
376 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
377 # ok, the recording has been stopped. we need to properly note
378 # that in our state, with also keeping the possibility to re-try.
379 # TODO: this has to be done.
380 elif event == iRecordableService.evStart:
381 text = _("A record has been started:\n%s") % self.name
382 if self.dirnameHadToFallback:
383 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
385 if config.usage.show_message_when_recording_starts.value:
386 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
388 # we have record_service as property to automatically subscribe to record service events
389 def setRecordService(self, service):
390 if self.__record_service is not None:
391 print "[remove callback]"
392 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
394 self.__record_service = service
396 if self.__record_service is not None:
397 print "[add callback]"
398 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
400 record_service = property(lambda self: self.__record_service, setRecordService)
402 def createTimer(xml):
403 begin = int(xml.get("begin"))
404 end = int(xml.get("end"))
405 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
406 description = xml.get("description").encode("utf-8")
407 repeated = xml.get("repeated").encode("utf-8")
408 disabled = long(xml.get("disabled") or "0")
409 justplay = long(xml.get("justplay") or "0")
410 afterevent = str(xml.get("afterevent") or "nothing")
412 "nothing": AFTEREVENT.NONE,
413 "standby": AFTEREVENT.STANDBY,
414 "deepstandby": AFTEREVENT.DEEPSTANDBY,
415 "auto": AFTEREVENT.AUTO
418 if eit and eit != "None":
422 location = xml.get("location")
423 if location and location != "None":
424 location = location.encode("utf-8")
427 tags = xml.get("tags")
428 if tags and tags != "None":
429 tags = tags.encode("utf-8").split(' ')
433 name = xml.get("name").encode("utf-8")
434 #filename = xml.get("filename").encode("utf-8")
435 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
436 entry.repeated = int(repeated)
438 for l in xml.findall("log"):
439 time = int(l.get("time"))
440 code = int(l.get("code"))
441 msg = l.text.strip().encode("utf-8")
442 entry.log_entries.append((time, code, msg))
446 class RecordTimer(timer.Timer):
448 timer.Timer.__init__(self)
450 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
455 print "unable to load timers from file!"
457 def doActivate(self, w):
458 # when activating a timer which has already passed,
459 # simply abort the timer. don't run trough all the stages.
461 w.state = RecordTimerEntry.StateEnded
463 # when active returns true, this means "accepted".
464 # otherwise, the current state is kept.
465 # the timer entry itself will fix up the delay then.
469 self.timer_list.remove(w)
471 # did this timer reached the last state?
472 if w.state < RecordTimerEntry.StateEnded:
473 # no, sort it into active list
474 insort(self.timer_list, w)
476 # yes. Process repeated, and re-add.
479 w.state = RecordTimerEntry.StateWaiting
480 self.addTimerEntry(w)
482 insort(self.processed_timers, w)
486 def isRecording(self):
488 for timer in self.timer_list:
489 if timer.isRunning() and not timer.justplay:
496 doc = xml.etree.cElementTree.parse(self.Filename)
498 from Tools.Notifications import AddPopup
499 from Screens.MessageBox import MessageBox
501 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
503 print "timers.xml failed to load!"
506 os.rename(self.Filename, self.Filename + "_old")
507 except (IOError, OSError):
508 print "renaming broken timer failed"
511 print "timers.xml not found!"
516 # put out a message when at least one timer overlaps
518 for timer in root.findall("timer"):
519 newTimer = createTimer(timer)
520 if (self.record(newTimer, True, True) is not None) and (checkit == True):
521 from Tools.Notifications import AddPopup
522 from Screens.MessageBox import MessageBox
523 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
524 checkit = False # at moment it is enough when the message is displayed one time
527 #root_element = xml.etree.cElementTree.Element('timers')
528 #root_element.text = "\n"
530 #for timer in self.timer_list + self.processed_timers:
531 # some timers (instant records) don't want to be saved.
535 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
536 #t.set("begin", str(int(timer.begin)))
537 #t.set("end", str(int(timer.end)))
538 #t.set("serviceref", str(timer.service_ref))
539 #t.set("repeated", str(timer.repeated))
540 #t.set("name", timer.name)
541 #t.set("description", timer.description)
542 #t.set("afterevent", str({
543 # AFTEREVENT.NONE: "nothing",
544 # AFTEREVENT.STANDBY: "standby",
545 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
546 # AFTEREVENT.AUTO: "auto"}))
547 #if timer.eit is not None:
548 # t.set("eit", str(timer.eit))
549 #if timer.dirname is not None:
550 # t.set("location", str(timer.dirname))
551 #t.set("disabled", str(int(timer.disabled)))
552 #t.set("justplay", str(int(timer.justplay)))
556 #for time, code, msg in timer.log_entries:
557 #l = xml.etree.cElementTree.SubElement(t, 'log')
558 #l.set("time", str(time))
559 #l.set("code", str(code))
563 #doc = xml.etree.cElementTree.ElementTree(root_element)
564 #doc.write(self.Filename)
568 list.append('<?xml version="1.0" ?>\n')
569 list.append('<timers>\n')
571 for timer in self.timer_list + self.processed_timers:
575 list.append('<timer')
576 list.append(' begin="' + str(int(timer.begin)) + '"')
577 list.append(' end="' + str(int(timer.end)) + '"')
578 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
579 list.append(' repeated="' + str(int(timer.repeated)) + '"')
580 list.append(' name="' + str(stringToXML(timer.name)) + '"')
581 list.append(' description="' + str(stringToXML(timer.description)) + '"')
582 list.append(' afterevent="' + str(stringToXML({
583 AFTEREVENT.NONE: "nothing",
584 AFTEREVENT.STANDBY: "standby",
585 AFTEREVENT.DEEPSTANDBY: "deepstandby",
586 AFTEREVENT.AUTO: "auto"
587 }[timer.afterEvent])) + '"')
588 if timer.eit is not None:
589 list.append(' eit="' + str(timer.eit) + '"')
590 if timer.dirname is not None:
591 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
592 if timer.tags is not None:
593 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
594 list.append(' disabled="' + str(int(timer.disabled)) + '"')
595 list.append(' justplay="' + str(int(timer.justplay)) + '"')
598 if config.recording.debug.value:
599 for time, code, msg in timer.log_entries:
601 list.append(' code="' + str(code) + '"')
602 list.append(' time="' + str(time) + '"')
604 list.append(str(stringToXML(msg)))
605 list.append('</log>\n')
607 list.append('</timer>\n')
609 list.append('</timers>\n')
611 file = open(self.Filename, "w")
616 def getNextZapTime(self):
618 for timer in self.timer_list:
619 if not timer.justplay or timer.begin < now:
624 def getNextRecordingTime(self):
626 for timer in self.timer_list:
627 next_act = timer.getNextActivation()
628 if timer.justplay or next_act < now:
633 def isNextRecordAfterEventActionAuto(self):
636 for timer in self.timer_list:
637 if timer.justplay or timer.begin < now:
639 if t is None or t.begin == timer.begin:
641 if t.afterEvent == AFTEREVENT.AUTO:
645 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
646 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
647 if not timersanitycheck.check():
648 if ignoreTSC != True:
649 print "timer conflict detected!"
650 print timersanitycheck.getSimulTimerList()
651 return timersanitycheck.getSimulTimerList()
653 print "ignore timer conflict"
654 elif timersanitycheck.doubleCheck():
655 print "ignore double timer"
658 print "[Timer] Record " + str(entry)
660 self.addTimerEntry(entry)
665 def isInTimer(self, eventid, begin, duration, service):
669 chktimecmp_end = None
670 end = begin + duration
671 refstr = str(service)
672 for x in self.timer_list:
673 check = x.service_ref.ref.toString() == refstr
675 sref = x.service_ref.ref
676 parent_sid = sref.getUnsignedData(5)
677 parent_tsid = sref.getUnsignedData(6)
678 if parent_sid and parent_tsid: # check for subservice
679 sid = sref.getUnsignedData(1)
680 tsid = sref.getUnsignedData(2)
681 sref.setUnsignedData(1, parent_sid)
682 sref.setUnsignedData(2, parent_tsid)
683 sref.setUnsignedData(5, 0)
684 sref.setUnsignedData(6, 0)
685 check = sref.toCompareString() == refstr
689 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
690 num = event and event.getNumOfLinkageServices() or 0
691 sref.setUnsignedData(1, sid)
692 sref.setUnsignedData(2, tsid)
693 sref.setUnsignedData(5, parent_sid)
694 sref.setUnsignedData(6, parent_tsid)
695 for cnt in range(num):
696 subservice = event.getLinkageService(sref, cnt)
697 if sref.toCompareString() == subservice.toCompareString():
703 chktime = localtime(begin)
704 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
705 chktimecmp_end = chktimecmp + (duration / 60)
706 time = localtime(x.begin)
707 for y in (0, 1, 2, 3, 4, 5, 6):
708 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
709 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
710 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
711 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
712 elif chktimecmp <= timecmp < chktimecmp_end:
713 time_match = (chktimecmp_end - timecmp) * 60
714 else: #if x.eit is None:
715 if begin <= x.begin <= end:
717 if time_match < diff:
719 elif x.begin <= begin <= x.end:
721 if time_match < diff:
727 def removeEntry(self, entry):
728 print "[Timer] Remove " + str(entry)
731 entry.repeated = False
734 # this sets the end time to current time, so timer will be stopped.
735 entry.autoincrease = False
738 if entry.state != entry.StateEnded:
739 self.timeChanged(entry)
741 print "state: ", entry.state
742 print "in processed: ", entry in self.processed_timers
743 print "in running: ", entry in self.timer_list
744 # autoincrease instanttimer if possible
745 if not entry.dontSave:
746 for x in self.timer_list:
747 if x.setAutoincreaseEnd():
749 # now the timer should be in the processed_timers list. remove it from there.
750 self.processed_timers.remove(entry)