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 if config.usage.setup_level.index >= 2: # expert+
142 if config.recording.filename_composition.value == "short":
143 filename = begin_shortdate + " - " + self.name
144 elif config.recording.filename_composition.value == "long":
145 filename += " - " + self.name + " - " + self.description
147 filename += " - " + self.name # standard
149 filename += " - " + self.name
151 if config.recording.ascii_filenames.value:
152 filename = ASCIItranslit.legacyEncode(filename)
154 if not self.dirname or not Directories.fileExists(self.dirname, 'w'):
156 self.dirnameHadToFallback = True
157 dirname = defaultMoviePath()
159 dirname = self.dirname
160 self.Filename = Directories.getRecordingFilename(filename, dirname)
161 self.log(0, "Filename calculated as: '%s'" % self.Filename)
162 #begin_date + " - " + service_name + description)
164 def tryPrepare(self):
168 self.calculateFilename()
169 rec_ref = self.service_ref and self.service_ref.ref
170 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
171 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
173 self.log(1, "'get best playable service for group... record' failed")
176 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
178 if not self.record_service:
179 self.log(1, "'record service' failed")
183 epgcache = eEPGCache.getInstance()
184 queryTime=self.begin+(self.end-self.begin)/2
185 evt = epgcache.lookupEventTime(rec_ref, queryTime)
187 self.description = evt.getShortDescription()
188 event_id = evt.getEventId()
196 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))
199 self.log(4, "failed to write meta information")
201 self.log(2, "'prepare' failed: error %d" % prep_res)
203 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
204 # the next start time in evEnd event handler...
206 self.start_prepare = time() + self.backoff
208 NavigationInstance.instance.stopRecordService(self.record_service)
209 self.record_service = None
213 def do_backoff(self):
214 if self.backoff == 0:
218 if self.backoff > 100:
220 self.log(10, "backoff: retry in %d seconds" % self.backoff)
223 next_state = self.state + 1
224 self.log(5, "activating state %d" % next_state)
226 if next_state == self.StatePrepared:
227 if self.tryPrepare():
228 self.log(6, "prepare ok, waiting for begin")
229 # create file to "reserve" the filename
230 # because another recording at the same time on another service can try to record the same event
231 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
232 # here than calculateFilename is happy
233 if not self.justplay:
234 open(self.Filename + ".ts", "w").close()
235 # fine. it worked, resources are allocated.
236 self.next_activation = self.begin
240 self.log(7, "prepare failed")
241 if self.first_try_prepare:
242 self.first_try_prepare = False
243 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
244 if cur_ref and not cur_ref.getPath():
245 if not config.recording.asktozap.value:
246 self.log(8, "asking user to zap away")
247 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
248 else: # zap without asking
249 self.log(9, "zap without asking")
250 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
253 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
255 self.log(8, "currently no service running... so we dont need to stop it")
257 elif next_state == self.StateRunning:
258 # if this timer has been cancelled, just go to "end" state.
263 if Screens.Standby.inStandby:
264 self.log(11, "wakeup and zap")
265 #set service to zap after standby
266 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
268 Screens.Standby.inStandby.Power()
270 self.log(11, "zapping")
271 NavigationInstance.instance.playService(self.service_ref.ref)
274 self.log(11, "start recording")
275 record_res = self.record_service.start()
278 self.log(13, "start record returned %d" % record_res)
281 self.begin = time() + self.backoff
285 elif next_state == self.StateEnded:
287 if self.setAutoincreaseEnd():
288 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
291 self.log(12, "stop recording")
292 if not self.justplay:
293 NavigationInstance.instance.stopRecordService(self.record_service)
294 self.record_service = None
295 if self.afterEvent == AFTEREVENT.STANDBY:
296 if not Screens.Standby.inStandby: # not already in standby
297 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
298 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
299 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
300 if Screens.Standby.inStandby: # in standby
301 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
303 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
306 def setAutoincreaseEnd(self, entry = None):
307 if not self.autoincrease:
310 new_end = int(time()) + self.autoincreasetime
312 new_end = entry.begin -30
314 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)
315 dummyentry.disabled = self.disabled
316 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
317 if not timersanitycheck.check():
318 simulTimerList = timersanitycheck.getSimulTimerList()
319 new_end = simulTimerList[1].begin
321 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
323 if new_end <= time():
329 def sendStandbyNotification(self, answer):
331 Notifications.AddNotification(Screens.Standby.Standby)
333 def sendTryQuitMainloopNotification(self, answer):
335 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
337 def getNextActivation(self):
338 if self.state == self.StateEnded:
341 next_state = self.state + 1
343 return {self.StatePrepared: self.start_prepare,
344 self.StateRunning: self.begin,
345 self.StateEnded: self.end }[next_state]
347 def failureCB(self, answer):
349 self.log(13, "ok, zapped away")
350 #NavigationInstance.instance.stopUserServices()
351 NavigationInstance.instance.playService(self.service_ref.ref)
353 self.log(14, "user didn't want to zap away, record will probably fail")
355 def timeChanged(self):
356 old_prepare = self.start_prepare
357 self.start_prepare = self.begin - self.prepare_time
360 if int(old_prepare) != int(self.start_prepare):
361 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
363 def gotRecordEvent(self, record, event):
364 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
365 if self.__record_service.__deref__() != record.__deref__():
367 self.log(16, "record event %d" % event)
368 if event == iRecordableService.evRecordWriteError:
369 print "WRITE ERROR on recording, disk full?"
370 # show notification. the 'id' will make sure that it will be
371 # displayed only once, even if more timers are failing at the
372 # same time. (which is very likely in case of disk fullness)
373 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
374 # ok, the recording has been stopped. we need to properly note
375 # that in our state, with also keeping the possibility to re-try.
376 # TODO: this has to be done.
377 elif event == iRecordableService.evStart:
378 text = _("A record has been started:\n%s") % self.name
379 if self.dirnameHadToFallback:
380 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
382 if config.usage.show_message_when_recording_starts.value:
383 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
385 # we have record_service as property to automatically subscribe to record service events
386 def setRecordService(self, service):
387 if self.__record_service is not None:
388 print "[remove callback]"
389 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
391 self.__record_service = service
393 if self.__record_service is not None:
394 print "[add callback]"
395 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
397 record_service = property(lambda self: self.__record_service, setRecordService)
399 def createTimer(xml):
400 begin = int(xml.get("begin"))
401 end = int(xml.get("end"))
402 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
403 description = xml.get("description").encode("utf-8")
404 repeated = xml.get("repeated").encode("utf-8")
405 disabled = long(xml.get("disabled") or "0")
406 justplay = long(xml.get("justplay") or "0")
407 afterevent = str(xml.get("afterevent") or "nothing")
409 "nothing": AFTEREVENT.NONE,
410 "standby": AFTEREVENT.STANDBY,
411 "deepstandby": AFTEREVENT.DEEPSTANDBY,
412 "auto": AFTEREVENT.AUTO
415 if eit and eit != "None":
419 location = xml.get("location")
420 if location and location != "None":
421 location = location.encode("utf-8")
424 tags = xml.get("tags")
425 if tags and tags != "None":
426 tags = tags.encode("utf-8").split(' ')
430 name = xml.get("name").encode("utf-8")
431 #filename = xml.get("filename").encode("utf-8")
432 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
433 entry.repeated = int(repeated)
435 for l in xml.findall("log"):
436 time = int(l.get("time"))
437 code = int(l.get("code"))
438 msg = l.text.strip().encode("utf-8")
439 entry.log_entries.append((time, code, msg))
443 class RecordTimer(timer.Timer):
445 timer.Timer.__init__(self)
447 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
452 print "unable to load timers from file!"
454 def doActivate(self, w):
455 # when activating a timer which has already passed,
456 # simply abort the timer. don't run trough all the stages.
458 w.state = RecordTimerEntry.StateEnded
460 # when active returns true, this means "accepted".
461 # otherwise, the current state is kept.
462 # the timer entry itself will fix up the delay then.
466 self.timer_list.remove(w)
468 # did this timer reached the last state?
469 if w.state < RecordTimerEntry.StateEnded:
470 # no, sort it into active list
471 insort(self.timer_list, w)
473 # yes. Process repeated, and re-add.
476 w.state = RecordTimerEntry.StateWaiting
477 self.addTimerEntry(w)
479 insort(self.processed_timers, w)
483 def isRecording(self):
485 for timer in self.timer_list:
486 if timer.isRunning() and not timer.justplay:
493 doc = xml.etree.cElementTree.parse(self.Filename)
495 from Tools.Notifications import AddPopup
496 from Screens.MessageBox import MessageBox
498 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
500 print "timers.xml failed to load!"
503 os.rename(self.Filename, self.Filename + "_old")
504 except (IOError, OSError):
505 print "renaming broken timer failed"
508 print "timers.xml not found!"
513 # put out a message when at least one timer overlaps
515 for timer in root.findall("timer"):
516 newTimer = createTimer(timer)
517 if (self.record(newTimer, True, True) is not None) and (checkit == True):
518 from Tools.Notifications import AddPopup
519 from Screens.MessageBox import MessageBox
520 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
521 checkit = False # at moment it is enough when the message is displayed one time
524 #root_element = xml.etree.cElementTree.Element('timers')
525 #root_element.text = "\n"
527 #for timer in self.timer_list + self.processed_timers:
528 # some timers (instant records) don't want to be saved.
532 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
533 #t.set("begin", str(int(timer.begin)))
534 #t.set("end", str(int(timer.end)))
535 #t.set("serviceref", str(timer.service_ref))
536 #t.set("repeated", str(timer.repeated))
537 #t.set("name", timer.name)
538 #t.set("description", timer.description)
539 #t.set("afterevent", str({
540 # AFTEREVENT.NONE: "nothing",
541 # AFTEREVENT.STANDBY: "standby",
542 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
543 # AFTEREVENT.AUTO: "auto"}))
544 #if timer.eit is not None:
545 # t.set("eit", str(timer.eit))
546 #if timer.dirname is not None:
547 # t.set("location", str(timer.dirname))
548 #t.set("disabled", str(int(timer.disabled)))
549 #t.set("justplay", str(int(timer.justplay)))
553 #for time, code, msg in timer.log_entries:
554 #l = xml.etree.cElementTree.SubElement(t, 'log')
555 #l.set("time", str(time))
556 #l.set("code", str(code))
560 #doc = xml.etree.cElementTree.ElementTree(root_element)
561 #doc.write(self.Filename)
565 list.append('<?xml version="1.0" ?>\n')
566 list.append('<timers>\n')
568 for timer in self.timer_list + self.processed_timers:
572 list.append('<timer')
573 list.append(' begin="' + str(int(timer.begin)) + '"')
574 list.append(' end="' + str(int(timer.end)) + '"')
575 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
576 list.append(' repeated="' + str(int(timer.repeated)) + '"')
577 list.append(' name="' + str(stringToXML(timer.name)) + '"')
578 list.append(' description="' + str(stringToXML(timer.description)) + '"')
579 list.append(' afterevent="' + str(stringToXML({
580 AFTEREVENT.NONE: "nothing",
581 AFTEREVENT.STANDBY: "standby",
582 AFTEREVENT.DEEPSTANDBY: "deepstandby",
583 AFTEREVENT.AUTO: "auto"
584 }[timer.afterEvent])) + '"')
585 if timer.eit is not None:
586 list.append(' eit="' + str(timer.eit) + '"')
587 if timer.dirname is not None:
588 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
589 if timer.tags is not None:
590 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
591 list.append(' disabled="' + str(int(timer.disabled)) + '"')
592 list.append(' justplay="' + str(int(timer.justplay)) + '"')
595 if config.recording.debug.value:
596 for time, code, msg in timer.log_entries:
598 list.append(' code="' + str(code) + '"')
599 list.append(' time="' + str(time) + '"')
601 list.append(str(stringToXML(msg)))
602 list.append('</log>\n')
604 list.append('</timer>\n')
606 list.append('</timers>\n')
608 file = open(self.Filename, "w")
613 def getNextZapTime(self):
615 for timer in self.timer_list:
616 if not timer.justplay or timer.begin < now:
621 def getNextRecordingTime(self):
623 for timer in self.timer_list:
624 next_act = timer.getNextActivation()
625 if timer.justplay or next_act < now:
630 def isNextRecordAfterEventActionAuto(self):
633 for timer in self.timer_list:
634 if timer.justplay or timer.begin < now:
636 if t is None or t.begin == timer.begin:
638 if t.afterEvent == AFTEREVENT.AUTO:
642 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
643 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
644 if not timersanitycheck.check():
645 if ignoreTSC != True:
646 print "timer conflict detected!"
647 print timersanitycheck.getSimulTimerList()
648 return timersanitycheck.getSimulTimerList()
650 print "ignore timer conflict"
651 elif timersanitycheck.doubleCheck():
652 print "ignore double timer"
655 print "[Timer] Record " + str(entry)
657 self.addTimerEntry(entry)
662 def isInTimer(self, eventid, begin, duration, service):
666 chktimecmp_end = None
667 end = begin + duration
668 refstr = str(service)
669 for x in self.timer_list:
670 check = x.service_ref.ref.toString() == refstr
672 sref = x.service_ref.ref
673 parent_sid = sref.getUnsignedData(5)
674 parent_tsid = sref.getUnsignedData(6)
675 if parent_sid and parent_tsid: # check for subservice
676 sid = sref.getUnsignedData(1)
677 tsid = sref.getUnsignedData(2)
678 sref.setUnsignedData(1, parent_sid)
679 sref.setUnsignedData(2, parent_tsid)
680 sref.setUnsignedData(5, 0)
681 sref.setUnsignedData(6, 0)
682 check = sref.toCompareString() == refstr
686 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
687 num = event and event.getNumOfLinkageServices() or 0
688 sref.setUnsignedData(1, sid)
689 sref.setUnsignedData(2, tsid)
690 sref.setUnsignedData(5, parent_sid)
691 sref.setUnsignedData(6, parent_tsid)
692 for cnt in range(num):
693 subservice = event.getLinkageService(sref, cnt)
694 if sref.toCompareString() == subservice.toCompareString():
700 chktime = localtime(begin)
701 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
702 chktimecmp_end = chktimecmp + (duration / 60)
703 time = localtime(x.begin)
704 for y in (0, 1, 2, 3, 4, 5, 6):
705 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
706 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
707 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
708 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
709 elif chktimecmp <= timecmp < chktimecmp_end:
710 time_match = (chktimecmp_end - timecmp) * 60
711 else: #if x.eit is None:
712 if begin <= x.begin <= end:
714 if time_match < diff:
716 elif x.begin <= begin <= x.end:
718 if time_match < diff:
724 def removeEntry(self, entry):
725 print "[Timer] Remove " + str(entry)
728 entry.repeated = False
731 # this sets the end time to current time, so timer will be stopped.
732 entry.autoincrease = False
735 if entry.state != entry.StateEnded:
736 self.timeChanged(entry)
738 print "state: ", entry.state
739 print "in processed: ", entry in self.processed_timers
740 print "in running: ", entry in self.timer_list
741 # autoincrease instanttimer if possible
742 if not entry.dontSave:
743 for x in self.timer_list:
744 if x.setAutoincreaseEnd():
746 # now the timer should be in the processed_timers list. remove it from there.
747 self.processed_timers.remove(entry)