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))
133 print "begin_date: ", begin_date
134 print "service_name: ", service_name
135 print "name:", self.name
136 print "description: ", self.description
138 filename = begin_date + " - " + service_name
140 filename += " - " + self.name
142 if config.recording.ascii_filenames.value:
143 filename = ASCIItranslit.legacyEncode(filename)
145 if not self.dirname or not Directories.fileExists(self.dirname, 'w'):
147 self.dirnameHadToFallback = True
148 dirname = defaultMoviePath()
150 dirname = self.dirname
151 self.Filename = Directories.getRecordingFilename(filename, dirname)
152 self.log(0, "Filename calculated as: '%s'" % self.Filename)
153 #begin_date + " - " + service_name + description)
155 def tryPrepare(self):
159 self.calculateFilename()
160 rec_ref = self.service_ref and self.service_ref.ref
161 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
162 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
164 self.log(1, "'get best playable service for group... record' failed")
167 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
169 if not self.record_service:
170 self.log(1, "'record service' failed")
174 epgcache = eEPGCache.getInstance()
175 queryTime=self.begin+(self.end-self.begin)/2
176 evt = epgcache.lookupEventTime(rec_ref, queryTime)
178 self.description = evt.getShortDescription()
179 event_id = evt.getEventId()
187 prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id, self.name.replace("\n", ""), self.description.replace("\n", ""), ' '.join(self.tags))
190 self.log(4, "failed to write meta information")
192 self.log(2, "'prepare' failed: error %d" % prep_res)
194 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
195 # the next start time in evEnd event handler...
197 self.start_prepare = time() + self.backoff
199 NavigationInstance.instance.stopRecordService(self.record_service)
200 self.record_service = None
204 def do_backoff(self):
205 if self.backoff == 0:
209 if self.backoff > 100:
211 self.log(10, "backoff: retry in %d seconds" % self.backoff)
214 next_state = self.state + 1
215 self.log(5, "activating state %d" % next_state)
217 if next_state == self.StatePrepared:
218 if self.tryPrepare():
219 self.log(6, "prepare ok, waiting for begin")
220 # create file to "reserve" the filename
221 # because another recording at the same time on another service can try to record the same event
222 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
223 # here than calculateFilename is happy
224 if not self.justplay:
225 open(self.Filename + ".ts", "w").close()
226 # fine. it worked, resources are allocated.
227 self.next_activation = self.begin
231 self.log(7, "prepare failed")
232 if self.first_try_prepare:
233 self.first_try_prepare = False
234 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
235 if cur_ref and not cur_ref.getPath():
236 if not config.recording.asktozap.value:
237 self.log(8, "asking user to zap away")
238 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
239 else: # zap without asking
240 self.log(9, "zap without asking")
241 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
244 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
246 self.log(8, "currently no service running... so we dont need to stop it")
248 elif next_state == self.StateRunning:
249 # if this timer has been cancelled, just go to "end" state.
254 if Screens.Standby.inStandby:
255 self.log(11, "wakeup and zap")
256 #set service to zap after standby
257 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
259 Screens.Standby.inStandby.Power()
261 self.log(11, "zapping")
262 NavigationInstance.instance.playService(self.service_ref.ref)
265 self.log(11, "start recording")
266 record_res = self.record_service.start()
269 self.log(13, "start record returned %d" % record_res)
272 self.begin = time() + self.backoff
276 elif next_state == self.StateEnded:
278 if self.setAutoincreaseEnd():
279 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
282 self.log(12, "stop recording")
283 if not self.justplay:
284 NavigationInstance.instance.stopRecordService(self.record_service)
285 self.record_service = None
286 if self.afterEvent == AFTEREVENT.STANDBY:
287 if not Screens.Standby.inStandby: # not already in standby
288 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
289 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
290 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
291 if Screens.Standby.inStandby: # in standby
292 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
294 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
297 def setAutoincreaseEnd(self, entry = None):
298 if not self.autoincrease:
301 new_end = int(time()) + self.autoincreasetime
303 new_end = entry.begin -30
305 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)
306 dummyentry.disabled = self.disabled
307 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
308 if not timersanitycheck.check():
309 simulTimerList = timersanitycheck.getSimulTimerList()
310 new_end = simulTimerList[1].begin
312 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
314 if new_end <= time():
320 def sendStandbyNotification(self, answer):
322 Notifications.AddNotification(Screens.Standby.Standby)
324 def sendTryQuitMainloopNotification(self, answer):
326 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
328 def getNextActivation(self):
329 if self.state == self.StateEnded:
332 next_state = self.state + 1
334 return {self.StatePrepared: self.start_prepare,
335 self.StateRunning: self.begin,
336 self.StateEnded: self.end }[next_state]
338 def failureCB(self, answer):
340 self.log(13, "ok, zapped away")
341 #NavigationInstance.instance.stopUserServices()
342 NavigationInstance.instance.playService(self.service_ref.ref)
344 self.log(14, "user didn't want to zap away, record will probably fail")
346 def timeChanged(self):
347 old_prepare = self.start_prepare
348 self.start_prepare = self.begin - self.prepare_time
351 if int(old_prepare) != int(self.start_prepare):
352 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
354 def gotRecordEvent(self, record, event):
355 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
356 if self.__record_service.__deref__() != record.__deref__():
358 self.log(16, "record event %d" % event)
359 if event == iRecordableService.evRecordWriteError:
360 print "WRITE ERROR on recording, disk full?"
361 # show notification. the 'id' will make sure that it will be
362 # displayed only once, even if more timers are failing at the
363 # same time. (which is very likely in case of disk fullness)
364 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
365 # ok, the recording has been stopped. we need to properly note
366 # that in our state, with also keeping the possibility to re-try.
367 # TODO: this has to be done.
368 elif event == iRecordableService.evStart:
369 text = _("A record has been started:\n%s") % self.name
370 if self.dirnameHadToFallback:
371 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
373 if config.usage.show_message_when_recording_starts.value:
374 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
376 # we have record_service as property to automatically subscribe to record service events
377 def setRecordService(self, service):
378 if self.__record_service is not None:
379 print "[remove callback]"
380 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
382 self.__record_service = service
384 if self.__record_service is not None:
385 print "[add callback]"
386 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
388 record_service = property(lambda self: self.__record_service, setRecordService)
390 def createTimer(xml):
391 begin = int(xml.get("begin"))
392 end = int(xml.get("end"))
393 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
394 description = xml.get("description").encode("utf-8")
395 repeated = xml.get("repeated").encode("utf-8")
396 disabled = long(xml.get("disabled") or "0")
397 justplay = long(xml.get("justplay") or "0")
398 afterevent = str(xml.get("afterevent") or "nothing")
400 "nothing": AFTEREVENT.NONE,
401 "standby": AFTEREVENT.STANDBY,
402 "deepstandby": AFTEREVENT.DEEPSTANDBY,
403 "auto": AFTEREVENT.AUTO
406 if eit and eit != "None":
410 location = xml.get("location")
411 if location and location != "None":
412 location = location.encode("utf-8")
415 tags = xml.get("tags")
416 if tags and tags != "None":
417 tags = tags.encode("utf-8").split(' ')
421 name = xml.get("name").encode("utf-8")
422 #filename = xml.get("filename").encode("utf-8")
423 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
424 entry.repeated = int(repeated)
426 for l in xml.findall("log"):
427 time = int(l.get("time"))
428 code = int(l.get("code"))
429 msg = l.text.strip().encode("utf-8")
430 entry.log_entries.append((time, code, msg))
434 class RecordTimer(timer.Timer):
436 timer.Timer.__init__(self)
438 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
443 print "unable to load timers from file!"
445 def doActivate(self, w):
446 # when activating a timer which has already passed,
447 # simply abort the timer. don't run trough all the stages.
449 w.state = RecordTimerEntry.StateEnded
451 # when active returns true, this means "accepted".
452 # otherwise, the current state is kept.
453 # the timer entry itself will fix up the delay then.
457 self.timer_list.remove(w)
459 # did this timer reached the last state?
460 if w.state < RecordTimerEntry.StateEnded:
461 # no, sort it into active list
462 insort(self.timer_list, w)
464 # yes. Process repeated, and re-add.
467 w.state = RecordTimerEntry.StateWaiting
468 self.addTimerEntry(w)
470 insort(self.processed_timers, w)
474 def isRecording(self):
476 for timer in self.timer_list:
477 if timer.isRunning() and not timer.justplay:
484 doc = xml.etree.cElementTree.parse(self.Filename)
486 from Tools.Notifications import AddPopup
487 from Screens.MessageBox import MessageBox
489 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
491 print "timers.xml failed to load!"
494 os.rename(self.Filename, self.Filename + "_old")
495 except (IOError, OSError):
496 print "renaming broken timer failed"
499 print "timers.xml not found!"
504 # put out a message when at least one timer overlaps
506 for timer in root.findall("timer"):
507 newTimer = createTimer(timer)
508 if (self.record(newTimer, True, True) is not None) and (checkit == True):
509 from Tools.Notifications import AddPopup
510 from Screens.MessageBox import MessageBox
511 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
512 checkit = False # at moment it is enough when the message is displayed one time
515 #root_element = xml.etree.cElementTree.Element('timers')
516 #root_element.text = "\n"
518 #for timer in self.timer_list + self.processed_timers:
519 # some timers (instant records) don't want to be saved.
523 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
524 #t.set("begin", str(int(timer.begin)))
525 #t.set("end", str(int(timer.end)))
526 #t.set("serviceref", str(timer.service_ref))
527 #t.set("repeated", str(timer.repeated))
528 #t.set("name", timer.name)
529 #t.set("description", timer.description)
530 #t.set("afterevent", str({
531 # AFTEREVENT.NONE: "nothing",
532 # AFTEREVENT.STANDBY: "standby",
533 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
534 # AFTEREVENT.AUTO: "auto"}))
535 #if timer.eit is not None:
536 # t.set("eit", str(timer.eit))
537 #if timer.dirname is not None:
538 # t.set("location", str(timer.dirname))
539 #t.set("disabled", str(int(timer.disabled)))
540 #t.set("justplay", str(int(timer.justplay)))
544 #for time, code, msg in timer.log_entries:
545 #l = xml.etree.cElementTree.SubElement(t, 'log')
546 #l.set("time", str(time))
547 #l.set("code", str(code))
551 #doc = xml.etree.cElementTree.ElementTree(root_element)
552 #doc.write(self.Filename)
556 list.append('<?xml version="1.0" ?>\n')
557 list.append('<timers>\n')
559 for timer in self.timer_list + self.processed_timers:
563 list.append('<timer')
564 list.append(' begin="' + str(int(timer.begin)) + '"')
565 list.append(' end="' + str(int(timer.end)) + '"')
566 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
567 list.append(' repeated="' + str(int(timer.repeated)) + '"')
568 list.append(' name="' + str(stringToXML(timer.name)) + '"')
569 list.append(' description="' + str(stringToXML(timer.description)) + '"')
570 list.append(' afterevent="' + str(stringToXML({
571 AFTEREVENT.NONE: "nothing",
572 AFTEREVENT.STANDBY: "standby",
573 AFTEREVENT.DEEPSTANDBY: "deepstandby",
574 AFTEREVENT.AUTO: "auto"
575 }[timer.afterEvent])) + '"')
576 if timer.eit is not None:
577 list.append(' eit="' + str(timer.eit) + '"')
578 if timer.dirname is not None:
579 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
580 if timer.tags is not None:
581 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
582 list.append(' disabled="' + str(int(timer.disabled)) + '"')
583 list.append(' justplay="' + str(int(timer.justplay)) + '"')
586 if config.recording.debug.value:
587 for time, code, msg in timer.log_entries:
589 list.append(' code="' + str(code) + '"')
590 list.append(' time="' + str(time) + '"')
592 list.append(str(stringToXML(msg)))
593 list.append('</log>\n')
595 list.append('</timer>\n')
597 list.append('</timers>\n')
599 file = open(self.Filename, "w")
604 def getNextZapTime(self):
606 for timer in self.timer_list:
607 if not timer.justplay or timer.begin < now:
612 def getNextRecordingTime(self):
614 for timer in self.timer_list:
615 next_act = timer.getNextActivation()
616 if timer.justplay or next_act < now:
621 def isNextRecordAfterEventActionAuto(self):
624 for timer in self.timer_list:
625 if timer.justplay or timer.begin < now:
627 if t is None or t.begin == timer.begin:
629 if t.afterEvent == AFTEREVENT.AUTO:
633 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
634 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
635 if not timersanitycheck.check():
636 if ignoreTSC != True:
637 print "timer conflict detected!"
638 print timersanitycheck.getSimulTimerList()
639 return timersanitycheck.getSimulTimerList()
641 print "ignore timer conflict"
642 elif timersanitycheck.doubleCheck():
643 print "ignore double timer"
646 print "[Timer] Record " + str(entry)
648 self.addTimerEntry(entry)
653 def isInTimer(self, eventid, begin, duration, service):
657 chktimecmp_end = None
658 end = begin + duration
659 refstr = str(service)
660 for x in self.timer_list:
661 check = x.service_ref.ref.toString() == refstr
663 sref = x.service_ref.ref
664 parent_sid = sref.getUnsignedData(5)
665 parent_tsid = sref.getUnsignedData(6)
666 if parent_sid and parent_tsid: # check for subservice
667 sid = sref.getUnsignedData(1)
668 tsid = sref.getUnsignedData(2)
669 sref.setUnsignedData(1, parent_sid)
670 sref.setUnsignedData(2, parent_tsid)
671 sref.setUnsignedData(5, 0)
672 sref.setUnsignedData(6, 0)
673 check = sref.toCompareString() == refstr
677 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
678 num = event and event.getNumOfLinkageServices() or 0
679 sref.setUnsignedData(1, sid)
680 sref.setUnsignedData(2, tsid)
681 sref.setUnsignedData(5, parent_sid)
682 sref.setUnsignedData(6, parent_tsid)
683 for cnt in range(num):
684 subservice = event.getLinkageService(sref, cnt)
685 if sref.toCompareString() == subservice.toCompareString():
691 chktime = localtime(begin)
692 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
693 chktimecmp_end = chktimecmp + (duration / 60)
694 time = localtime(x.begin)
695 for y in (0, 1, 2, 3, 4, 5, 6):
696 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
697 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
698 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
699 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
700 elif chktimecmp <= timecmp < chktimecmp_end:
701 time_match = (chktimecmp_end - timecmp) * 60
702 else: #if x.eit is None:
703 if begin <= x.begin <= end:
705 if time_match < diff:
707 elif x.begin <= begin <= x.end:
709 if time_match < diff:
715 def removeEntry(self, entry):
716 print "[Timer] Remove " + str(entry)
719 entry.repeated = False
722 # this sets the end time to current time, so timer will be stopped.
723 entry.autoincrease = False
726 if entry.state != entry.StateEnded:
727 self.timeChanged(entry)
729 print "state: ", entry.state
730 print "in processed: ", entry in self.processed_timers
731 print "in running: ", entry in self.timer_list
732 # autoincrease instanttimer if possible
733 if not entry.dontSave:
734 for x in self.timer_list:
735 if x.setAutoincreaseEnd():
737 # now the timer should be in the processed_timers list. remove it from there.
738 self.processed_timers.remove(entry)