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, descramble = True, record_ecm = False, filename = 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.descramble = descramble
126 self.record_ecm = record_ecm
128 self.log_entries = []
131 self.Filename = filename
132 self.pvrConvert = False
134 def log(self, code, msg):
135 self.log_entries.append((int(time()), code, msg))
138 def calculateFilename(self):
140 self.log(0, "Filename calculated as: '%s'" % self.Filename)
143 service_name = self.service_ref.getServiceName()
144 begin_date = strftime("%Y%m%d %H%M", localtime(self.begin))
145 begin_shortdate = strftime("%Y%m%d", localtime(self.begin))
147 print "begin_date: ", begin_date
148 print "service_name: ", service_name
149 print "name:", self.name
150 print "description: ", self.description
152 filename = begin_date + " - " + service_name
154 if config.usage.setup_level.index >= 2: # expert+
155 if config.recording.filename_composition.value == "short":
156 filename = begin_shortdate + " - " + self.name
157 elif config.recording.filename_composition.value == "long":
158 filename += " - " + self.name + " - " + self.description
160 filename += " - " + self.name # standard
162 filename += " - " + self.name
164 if config.recording.ascii_filenames.value:
165 filename = ASCIItranslit.legacyEncode(filename)
167 if not self.dirname or not Directories.fileExists(self.dirname, 'w'):
169 self.dirnameHadToFallback = True
170 dirname = defaultMoviePath()
172 dirname = self.dirname
173 self.Filename = Directories.getRecordingFilename(filename, dirname)
174 self.log(0, "Filename calculated as: '%s'" % self.Filename)
175 #begin_date + " - " + service_name + description)
177 def tryPrepare(self):
181 self.calculateFilename()
182 rec_ref = self.service_ref and self.service_ref.ref
183 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
184 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
186 self.log(1, "'get best playable service for group... record' failed")
189 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
191 if not self.record_service:
192 self.log(1, "'record service' failed")
196 epgcache = eEPGCache.getInstance()
197 queryTime=self.begin+(self.end-self.begin)/2
198 evt = epgcache.lookupEventTime(rec_ref, queryTime)
200 self.description = evt.getShortDescription()
201 event_id = evt.getEventId()
209 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), bool(self.descramble), bool(self.record_ecm))
212 self.log(4, "failed to write meta information")
214 self.log(2, "'prepare' failed: error %d" % prep_res)
216 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
217 # the next start time in evEnd event handler...
219 self.start_prepare = time() + self.backoff
221 NavigationInstance.instance.stopRecordService(self.record_service)
222 self.record_service = None
226 def do_backoff(self):
227 if self.backoff == 0:
231 if self.backoff > 100:
233 self.log(10, "backoff: retry in %d seconds" % self.backoff)
236 next_state = self.state + 1
237 self.log(5, "activating state %d" % next_state)
239 if next_state == self.StatePrepared:
240 if self.tryPrepare():
241 self.log(6, "prepare ok, waiting for begin")
242 # create file to "reserve" the filename
243 # because another recording at the same time on another service can try to record the same event
244 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
245 # here than calculateFilename is happy
246 if not self.justplay:
247 open(self.Filename + ".ts", "w").close()
248 # fine. it worked, resources are allocated.
249 self.next_activation = self.begin
253 self.log(7, "prepare failed")
254 if self.first_try_prepare:
255 self.first_try_prepare = False
256 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
257 if cur_ref and not cur_ref.getPath():
258 if not config.recording.asktozap.value:
259 self.log(8, "asking user to zap away")
260 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
261 else: # zap without asking
262 self.log(9, "zap without asking")
263 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
266 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
268 self.log(8, "currently no service running... so we dont need to stop it")
270 elif next_state == self.StateRunning:
271 # if this timer has been cancelled, just go to "end" state.
276 if Screens.Standby.inStandby:
277 self.log(11, "wakeup and zap")
278 #set service to zap after standby
279 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
281 Screens.Standby.inStandby.Power()
283 self.log(11, "zapping")
284 NavigationInstance.instance.playService(self.service_ref.ref)
287 self.log(11, "start recording")
288 record_res = self.record_service.start()
291 self.log(13, "start record returned %d" % record_res)
294 self.begin = time() + self.backoff
298 elif next_state == self.StateEnded:
300 if self.setAutoincreaseEnd():
301 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
304 self.log(12, "stop recording")
305 if not self.justplay:
306 NavigationInstance.instance.stopRecordService(self.record_service)
307 self.record_service = None
308 if self.afterEvent == AFTEREVENT.STANDBY:
309 if not Screens.Standby.inStandby: # not already in standby
310 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nSTB to standby. Do that now?"), timeout = 20)
311 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
312 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
313 if Screens.Standby.inStandby: # in standby
314 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
316 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour STB. Shutdown now?"), timeout = 20)
319 def setAutoincreaseEnd(self, entry = None):
320 if not self.autoincrease:
323 new_end = int(time()) + self.autoincreasetime
325 new_end = entry.begin -30
327 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)
328 dummyentry.disabled = self.disabled
329 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
330 if not timersanitycheck.check():
331 simulTimerList = timersanitycheck.getSimulTimerList()
332 if simulTimerList is not None and len(simulTimerList) > 1:
333 new_end = simulTimerList[1].begin
334 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
335 if new_end <= time():
340 def sendStandbyNotification(self, answer):
342 Notifications.AddNotification(Screens.Standby.Standby)
344 def sendTryQuitMainloopNotification(self, answer):
346 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
348 def getNextActivation(self):
349 if self.state == self.StateEnded:
352 next_state = self.state + 1
354 return {self.StatePrepared: self.start_prepare,
355 self.StateRunning: self.begin,
356 self.StateEnded: self.end }[next_state]
358 def failureCB(self, answer):
360 self.log(13, "ok, zapped away")
361 #NavigationInstance.instance.stopUserServices()
362 NavigationInstance.instance.playService(self.service_ref.ref)
364 self.log(14, "user didn't want to zap away, record will probably fail")
366 def timeChanged(self):
367 old_prepare = self.start_prepare
368 self.start_prepare = self.begin - self.prepare_time
371 if int(old_prepare) != int(self.start_prepare):
372 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
374 def gotRecordEvent(self, record, event):
375 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
376 if self.__record_service.__deref__() != record.__deref__():
378 self.log(16, "record event %d" % event)
379 if event == iRecordableService.evRecordWriteError:
380 print "WRITE ERROR on recording, disk full?"
381 # show notification. the 'id' will make sure that it will be
382 # displayed only once, even if more timers are failing at the
383 # same time. (which is very likely in case of disk fullness)
384 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
385 # ok, the recording has been stopped. we need to properly note
386 # that in our state, with also keeping the possibility to re-try.
387 # TODO: this has to be done.
388 elif event == iRecordableService.evStart:
392 text = _("A record has been started:\n%s") % self.name
393 if self.dirnameHadToFallback:
394 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
396 if config.usage.show_message_when_recording_starts.value:
397 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
399 # we have record_service as property to automatically subscribe to record service events
400 def setRecordService(self, service):
401 if self.__record_service is not None:
402 print "[remove callback]"
403 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
405 self.__record_service = service
407 if self.__record_service is not None:
408 print "[add callback]"
409 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
411 record_service = property(lambda self: self.__record_service, setRecordService)
413 def createTimer(xml):
414 begin = int(xml.get("begin"))
415 end = int(xml.get("end"))
416 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
417 description = xml.get("description").encode("utf-8")
418 repeated = xml.get("repeated").encode("utf-8")
419 disabled = long(xml.get("disabled") or "0")
420 justplay = long(xml.get("justplay") or "0")
421 afterevent = str(xml.get("afterevent") or "nothing")
423 "nothing": AFTEREVENT.NONE,
424 "standby": AFTEREVENT.STANDBY,
425 "deepstandby": AFTEREVENT.DEEPSTANDBY,
426 "auto": AFTEREVENT.AUTO
429 if eit and eit != "None":
433 location = xml.get("location")
434 if location and location != "None":
435 location = location.encode("utf-8")
438 tags = xml.get("tags")
439 if tags and tags != "None":
440 tags = tags.encode("utf-8").split(' ')
444 name = xml.get("name").encode("utf-8")
445 #filename = xml.get("filename").encode("utf-8")
446 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
447 entry.repeated = int(repeated)
449 for l in xml.findall("log"):
450 time = int(l.get("time"))
451 code = int(l.get("code"))
452 msg = l.text.strip().encode("utf-8")
453 entry.log_entries.append((time, code, msg))
457 class RecordTimer(timer.Timer):
459 timer.Timer.__init__(self)
461 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
466 print "unable to load timers from file!"
468 def doActivate(self, w):
469 # when activating a timer which has already passed,
470 # simply abort the timer. don't run trough all the stages.
472 w.state = RecordTimerEntry.StateEnded
474 # when active returns true, this means "accepted".
475 # otherwise, the current state is kept.
476 # the timer entry itself will fix up the delay then.
480 self.timer_list.remove(w)
482 # did this timer reached the last state?
483 if w.state < RecordTimerEntry.StateEnded:
484 # no, sort it into active list
485 insort(self.timer_list, w)
487 # yes. Process repeated, and re-add.
490 w.state = RecordTimerEntry.StateWaiting
491 self.addTimerEntry(w)
493 insort(self.processed_timers, w)
497 def isRecording(self):
499 for timer in self.timer_list:
500 if timer.isRunning() and not timer.justplay:
507 doc = xml.etree.cElementTree.parse(self.Filename)
509 from Tools.Notifications import AddPopup
510 from Screens.MessageBox import MessageBox
512 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
514 print "timers.xml failed to load!"
517 os.rename(self.Filename, self.Filename + "_old")
518 except (IOError, OSError):
519 print "renaming broken timer failed"
522 print "timers.xml not found!"
527 # put out a message when at least one timer overlaps
529 for timer in root.findall("timer"):
530 newTimer = createTimer(timer)
531 if (self.record(newTimer, True, dosave=False) is not None) and (checkit == True):
532 from Tools.Notifications import AddPopup
533 from Screens.MessageBox import MessageBox
534 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
535 checkit = False # at moment it is enough when the message is displayed one time
538 #root_element = xml.etree.cElementTree.Element('timers')
539 #root_element.text = "\n"
541 #for timer in self.timer_list + self.processed_timers:
542 # some timers (instant records) don't want to be saved.
546 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
547 #t.set("begin", str(int(timer.begin)))
548 #t.set("end", str(int(timer.end)))
549 #t.set("serviceref", str(timer.service_ref))
550 #t.set("repeated", str(timer.repeated))
551 #t.set("name", timer.name)
552 #t.set("description", timer.description)
553 #t.set("afterevent", str({
554 # AFTEREVENT.NONE: "nothing",
555 # AFTEREVENT.STANDBY: "standby",
556 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
557 # AFTEREVENT.AUTO: "auto"}))
558 #if timer.eit is not None:
559 # t.set("eit", str(timer.eit))
560 #if timer.dirname is not None:
561 # t.set("location", str(timer.dirname))
562 #t.set("disabled", str(int(timer.disabled)))
563 #t.set("justplay", str(int(timer.justplay)))
567 #for time, code, msg in timer.log_entries:
568 #l = xml.etree.cElementTree.SubElement(t, 'log')
569 #l.set("time", str(time))
570 #l.set("code", str(code))
574 #doc = xml.etree.cElementTree.ElementTree(root_element)
575 #doc.write(self.Filename)
579 list.append('<?xml version="1.0" ?>\n')
580 list.append('<timers>\n')
582 for timer in self.timer_list + self.processed_timers:
586 list.append('<timer')
587 list.append(' begin="' + str(int(timer.begin)) + '"')
588 list.append(' end="' + str(int(timer.end)) + '"')
589 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
590 list.append(' repeated="' + str(int(timer.repeated)) + '"')
591 list.append(' name="' + str(stringToXML(timer.name)) + '"')
592 list.append(' description="' + str(stringToXML(timer.description)) + '"')
593 list.append(' afterevent="' + str(stringToXML({
594 AFTEREVENT.NONE: "nothing",
595 AFTEREVENT.STANDBY: "standby",
596 AFTEREVENT.DEEPSTANDBY: "deepstandby",
597 AFTEREVENT.AUTO: "auto"
598 }[timer.afterEvent])) + '"')
599 if timer.eit is not None:
600 list.append(' eit="' + str(timer.eit) + '"')
601 if timer.dirname is not None:
602 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
603 if timer.tags is not None:
604 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
605 list.append(' disabled="' + str(int(timer.disabled)) + '"')
606 list.append(' justplay="' + str(int(timer.justplay)) + '"')
609 if config.recording.debug.value:
610 for time, code, msg in timer.log_entries:
612 list.append(' code="' + str(code) + '"')
613 list.append(' time="' + str(time) + '"')
615 list.append(str(stringToXML(msg)))
616 list.append('</log>\n')
618 list.append('</timer>\n')
620 list.append('</timers>\n')
622 file = open(self.Filename, "w")
627 def getNextZapTime(self):
629 for timer in self.timer_list:
630 if not timer.justplay or timer.begin < now:
635 def getNextRecordingTime(self):
637 for timer in self.timer_list:
638 next_act = timer.getNextActivation()
639 if timer.justplay or next_act < now:
644 def isNextRecordAfterEventActionAuto(self):
647 for timer in self.timer_list:
648 if timer.justplay or timer.begin < now:
650 if t is None or t.begin == timer.begin:
652 if t.afterEvent == AFTEREVENT.AUTO:
656 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
657 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
658 if not timersanitycheck.check():
659 if ignoreTSC != True:
660 print "timer conflict detected!"
661 print timersanitycheck.getSimulTimerList()
662 return timersanitycheck.getSimulTimerList()
664 print "ignore timer conflict"
665 elif timersanitycheck.doubleCheck():
666 print "ignore double timer"
669 print "[Timer] Record " + str(entry)
671 self.addTimerEntry(entry)
676 def isInTimer(self, eventid, begin, duration, service):
680 chktimecmp_end = None
681 end = begin + duration
682 refstr = str(service)
683 for x in self.timer_list:
684 check = x.service_ref.ref.toString() == refstr
686 sref = x.service_ref.ref
687 parent_sid = sref.getUnsignedData(5)
688 parent_tsid = sref.getUnsignedData(6)
689 if parent_sid and parent_tsid: # check for subservice
690 sid = sref.getUnsignedData(1)
691 tsid = sref.getUnsignedData(2)
692 sref.setUnsignedData(1, parent_sid)
693 sref.setUnsignedData(2, parent_tsid)
694 sref.setUnsignedData(5, 0)
695 sref.setUnsignedData(6, 0)
696 check = sref.toCompareString() == refstr
700 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
701 num = event and event.getNumOfLinkageServices() or 0
702 sref.setUnsignedData(1, sid)
703 sref.setUnsignedData(2, tsid)
704 sref.setUnsignedData(5, parent_sid)
705 sref.setUnsignedData(6, parent_tsid)
706 for cnt in range(num):
707 subservice = event.getLinkageService(sref, cnt)
708 if sref.toCompareString() == subservice.toCompareString():
714 chktime = localtime(begin)
715 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
716 chktimecmp_end = chktimecmp + (duration / 60)
717 time = localtime(x.begin)
718 for y in (0, 1, 2, 3, 4, 5, 6):
719 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
720 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
721 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
722 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
723 elif chktimecmp <= timecmp < chktimecmp_end:
724 time_match = (chktimecmp_end - timecmp) * 60
725 else: #if x.eit is None:
726 if begin <= x.begin <= end:
728 if time_match < diff:
730 elif x.begin <= begin <= x.end:
732 if time_match < diff:
738 def removeEntry(self, entry):
739 print "[Timer] Remove " + str(entry)
742 entry.repeated = False
745 # this sets the end time to current time, so timer will be stopped.
746 entry.autoincrease = False
749 if entry.state != entry.StateEnded:
750 self.timeChanged(entry)
752 print "state: ", entry.state
753 print "in processed: ", entry in self.processed_timers
754 print "in running: ", entry in self.timer_list
755 # autoincrease instanttimer if possible
756 if not entry.dontSave:
757 for x in self.timer_list:
758 if x.setAutoincreaseEnd():
760 # now the timer should be in the processed_timers list. remove it from there.
761 self.processed_timers.remove(entry)