1 from enigma import eEPGCache, getBestPlayableServiceReference, \
2 eServiceReference, iRecordableService, quitMainloop
4 from Components.config import config
5 from Components.TimerSanityCheck import TimerSanityCheck
7 from Screens.MessageBox import MessageBox
9 from Tools import Directories, Notifications, ASCIItranslit
10 from Tools.XMLTools import stringToXML
13 import xml.etree.cElementTree
14 import NavigationInstance
15 from ServiceReference import ServiceReference
17 from time import localtime, strftime, ctime, time
18 from bisect import insort
20 # ok, for descriptions etc we have:
21 # service reference (to get the service name)
23 # description (description)
24 # event data (ONLY for time adjustments etc.)
27 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
28 # begin and end will be corrected
29 def parseEvent(ev, description = True):
31 name = ev.getEventName()
32 description = ev.getShortDescription()
36 begin = ev.getBeginTime()
37 end = begin + ev.getDuration()
39 begin -= config.recording.margin_before.value * 60
40 end += config.recording.margin_after.value * 60
41 return (begin, end, name, description, eit)
49 # please do not translate log messages
50 class RecordTimerEntry(timer.TimerEntry, object):
51 ######### the following static methods and members are only in use when the box is in (soft) standby
52 receiveRecordEvents = False
59 def staticGotRecordEvent(recservice, event):
60 if event == iRecordableService.evEnd:
61 print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
62 recordings = NavigationInstance.instance.getRecordings()
63 if not recordings: # no more recordings exist
64 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
65 if rec_time > 0 and (rec_time - time()) < 360:
66 print "another recording starts in", rec_time - time(), "seconds... do not shutdown yet"
68 print "no starting records in the next 360 seconds... immediate shutdown"
69 RecordTimerEntry.shutdown() # immediate shutdown
70 elif event == iRecordableService.evStart:
71 print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
74 def stopTryQuitMainloop():
75 print "RecordTimer.stopTryQuitMainloop"
76 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
77 RecordTimerEntry.receiveRecordEvents = False
80 def TryQuitMainloop(default_yes = True):
81 if not RecordTimerEntry.receiveRecordEvents:
82 print "RecordTimer.TryQuitMainloop"
83 NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
84 RecordTimerEntry.receiveRecordEvents = True
85 # send fake event.. to check if another recordings are running or
86 # other timers start in a few seconds
87 RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
88 # send normal notification for the case the user leave the standby now..
89 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop, default_yes = default_yes)
90 #################################################################
92 def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.AUTO, checkOldTimers = False, dirname = None, tags = None):
93 timer.TimerEntry.__init__(self, int(begin), int(end))
95 if checkOldTimers == True:
96 if self.begin < time() - 1209600:
97 self.begin = int(time())
99 if self.end < self.begin:
100 self.end = self.begin
102 assert isinstance(serviceref, ServiceReference)
104 self.service_ref = serviceref
106 self.dontSave = False
108 self.description = description
109 self.disabled = disabled
111 self.__record_service = None
112 self.start_prepare = 0
113 self.justplay = justplay
114 self.afterEvent = afterEvent
115 self.dirname = dirname
116 self.dirnameHadToFallback = False
117 self.autoincrease = False
118 self.autoincreasetime = 3600 * 24 # 1 day
119 self.tags = tags or []
121 self.log_entries = []
124 def log(self, code, msg):
125 self.log_entries.append((int(time()), code, msg))
128 def calculateFilename(self):
129 service_name = self.service_ref.getServiceName()
130 begin_date = strftime("%Y%m%d %H%M", localtime(self.begin))
132 print "begin_date: ", begin_date
133 print "service_name: ", service_name
134 print "name:", self.name
135 print "description: ", self.description
137 filename = begin_date + " - " + service_name
139 filename += " - " + self.name
141 if config.recording.ascii_filenames.value:
142 filename = ASCIItranslit.legacyEncode(filename)
144 if self.dirname and not Directories.fileExists(self.dirname, 'w'):
145 self.dirnameHadToFallback = True
146 self.Filename = Directories.getRecordingFilename(filename, None)
148 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
149 self.log(0, "Filename calculated as: '%s'" % self.Filename)
150 #begin_date + " - " + service_name + description)
152 def tryPrepare(self):
156 self.calculateFilename()
157 rec_ref = self.service_ref and self.service_ref.ref
158 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
159 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
161 self.log(1, "'get best playable service for group... record' failed")
164 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
166 if not self.record_service:
167 self.log(1, "'record service' failed")
171 epgcache = eEPGCache.getInstance()
172 queryTime=self.begin+(self.end-self.begin)/2
173 evt = epgcache.lookupEventTime(rec_ref, queryTime)
175 self.description = evt.getShortDescription()
176 event_id = evt.getEventId()
184 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))
187 self.log(4, "failed to write meta information")
189 self.log(2, "'prepare' failed: error %d" % prep_res)
191 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
192 # the next start time in evEnd event handler...
194 self.start_prepare = time() + self.backoff
196 NavigationInstance.instance.stopRecordService(self.record_service)
197 self.record_service = None
201 def do_backoff(self):
202 if self.backoff == 0:
206 if self.backoff > 100:
208 self.log(10, "backoff: retry in %d seconds" % self.backoff)
211 next_state = self.state + 1
212 self.log(5, "activating state %d" % next_state)
214 if next_state == self.StatePrepared:
215 if self.tryPrepare():
216 self.log(6, "prepare ok, waiting for begin")
217 # create file to "reserve" the filename
218 # because another recording at the same time on another service can try to record the same event
219 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
220 # here than calculateFilename is happy
221 if not self.justplay:
222 open(self.Filename + ".ts", "w").close()
223 # fine. it worked, resources are allocated.
224 self.next_activation = self.begin
228 self.log(7, "prepare failed")
229 if self.first_try_prepare:
230 self.first_try_prepare = False
231 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
232 if cur_ref and not cur_ref.getPath():
233 if not config.recording.asktozap.value:
234 self.log(8, "asking user to zap away")
235 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
236 else: # zap without asking
237 self.log(9, "zap without asking")
238 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
241 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
243 self.log(8, "currently no service running... so we dont need to stop it")
245 elif next_state == self.StateRunning:
246 # if this timer has been cancelled, just go to "end" state.
251 if Screens.Standby.inStandby:
252 self.log(11, "wakeup and zap")
253 #set service to zap after standby
254 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
256 Screens.Standby.inStandby.Power()
258 self.log(11, "zapping")
259 NavigationInstance.instance.playService(self.service_ref.ref)
262 self.log(11, "start recording")
263 record_res = self.record_service.start()
266 self.log(13, "start record returned %d" % record_res)
269 self.begin = time() + self.backoff
273 elif next_state == self.StateEnded:
275 if self.setAutoincreaseEnd():
276 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
279 self.log(12, "stop recording")
280 if not self.justplay:
281 NavigationInstance.instance.stopRecordService(self.record_service)
282 self.record_service = None
283 if self.afterEvent == AFTEREVENT.STANDBY:
284 if not Screens.Standby.inStandby: # not already in standby
285 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
286 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
287 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
288 if Screens.Standby.inStandby: # in standby
289 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
291 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
294 def setAutoincreaseEnd(self, entry = None):
295 if not self.autoincrease:
298 new_end = int(time()) + self.autoincreasetime
300 new_end = entry.begin -30
302 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)
303 dummyentry.disabled = self.disabled
304 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
305 if not timersanitycheck.check():
306 simulTimerList = timersanitycheck.getSimulTimerList()
307 new_end = simulTimerList[1].begin
309 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
311 if new_end <= time():
317 def sendStandbyNotification(self, answer):
319 Notifications.AddNotification(Screens.Standby.Standby)
321 def sendTryQuitMainloopNotification(self, answer):
323 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
325 def getNextActivation(self):
326 if self.state == self.StateEnded:
329 next_state = self.state + 1
331 return {self.StatePrepared: self.start_prepare,
332 self.StateRunning: self.begin,
333 self.StateEnded: self.end }[next_state]
335 def failureCB(self, answer):
337 self.log(13, "ok, zapped away")
338 #NavigationInstance.instance.stopUserServices()
339 NavigationInstance.instance.playService(self.service_ref.ref)
341 self.log(14, "user didn't want to zap away, record will probably fail")
343 def timeChanged(self):
344 old_prepare = self.start_prepare
345 self.start_prepare = self.begin - self.prepare_time
348 if int(old_prepare) != int(self.start_prepare):
349 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
351 def gotRecordEvent(self, record, event):
352 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
353 if self.__record_service.__deref__() != record.__deref__():
355 self.log(16, "record event %d" % event)
356 if event == iRecordableService.evRecordWriteError:
357 print "WRITE ERROR on recording, disk full?"
358 # show notification. the 'id' will make sure that it will be
359 # displayed only once, even if more timers are failing at the
360 # same time. (which is very likely in case of disk fullness)
361 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
362 # ok, the recording has been stopped. we need to properly note
363 # that in our state, with also keeping the possibility to re-try.
364 # TODO: this has to be done.
365 elif event == iRecordableService.evStart:
366 text = _("A record has been started:\n%s") % self.name
367 if self.dirnameHadToFallback:
368 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
370 if config.usage.show_message_when_recording_starts.value:
371 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
373 # we have record_service as property to automatically subscribe to record service events
374 def setRecordService(self, service):
375 if self.__record_service is not None:
376 print "[remove callback]"
377 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
379 self.__record_service = service
381 if self.__record_service is not None:
382 print "[add callback]"
383 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
385 record_service = property(lambda self: self.__record_service, setRecordService)
387 def createTimer(xml):
388 begin = int(xml.get("begin"))
389 end = int(xml.get("end"))
390 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
391 description = xml.get("description").encode("utf-8")
392 repeated = xml.get("repeated").encode("utf-8")
393 disabled = long(xml.get("disabled") or "0")
394 justplay = long(xml.get("justplay") or "0")
395 afterevent = str(xml.get("afterevent") or "nothing")
397 "nothing": AFTEREVENT.NONE,
398 "standby": AFTEREVENT.STANDBY,
399 "deepstandby": AFTEREVENT.DEEPSTANDBY,
400 "auto": AFTEREVENT.AUTO
403 if eit and eit != "None":
407 location = xml.get("location")
408 if location and location != "None":
409 location = location.encode("utf-8")
412 tags = xml.get("tags")
413 if tags and tags != "None":
414 tags = tags.encode("utf-8").split(' ')
418 name = xml.get("name").encode("utf-8")
419 #filename = xml.get("filename").encode("utf-8")
420 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
421 entry.repeated = int(repeated)
423 for l in xml.findall("log"):
424 time = int(l.get("time"))
425 code = int(l.get("code"))
426 msg = l.text.strip().encode("utf-8")
427 entry.log_entries.append((time, code, msg))
431 class RecordTimer(timer.Timer):
433 timer.Timer.__init__(self)
435 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
440 print "unable to load timers from file!"
442 def doActivate(self, w):
443 # when activating a timer which has already passed,
444 # simply abort the timer. don't run trough all the stages.
446 w.state = RecordTimerEntry.StateEnded
448 # when active returns true, this means "accepted".
449 # otherwise, the current state is kept.
450 # the timer entry itself will fix up the delay then.
454 self.timer_list.remove(w)
456 # did this timer reached the last state?
457 if w.state < RecordTimerEntry.StateEnded:
458 # no, sort it into active list
459 insort(self.timer_list, w)
461 # yes. Process repeated, and re-add.
464 w.state = RecordTimerEntry.StateWaiting
465 self.addTimerEntry(w)
467 insort(self.processed_timers, w)
471 def isRecording(self):
473 for timer in self.timer_list:
474 if timer.isRunning() and not timer.justplay:
481 doc = xml.etree.cElementTree.parse(self.Filename)
483 from Tools.Notifications import AddPopup
484 from Screens.MessageBox import MessageBox
486 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
488 print "timers.xml failed to load!"
491 os.rename(self.Filename, self.Filename + "_old")
492 except (IOError, OSError):
493 print "renaming broken timer failed"
496 print "timers.xml not found!"
501 # put out a message when at least one timer overlaps
503 for timer in root.findall("timer"):
504 newTimer = createTimer(timer)
505 if (self.record(newTimer, True, True) is not None) and (checkit == True):
506 from Tools.Notifications import AddPopup
507 from Screens.MessageBox import MessageBox
508 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
509 checkit = False # at moment it is enough when the message is displayed one time
512 #root_element = xml.etree.cElementTree.Element('timers')
513 #root_element.text = "\n"
515 #for timer in self.timer_list + self.processed_timers:
516 # some timers (instant records) don't want to be saved.
520 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
521 #t.set("begin", str(int(timer.begin)))
522 #t.set("end", str(int(timer.end)))
523 #t.set("serviceref", str(timer.service_ref))
524 #t.set("repeated", str(timer.repeated))
525 #t.set("name", timer.name)
526 #t.set("description", timer.description)
527 #t.set("afterevent", str({
528 # AFTEREVENT.NONE: "nothing",
529 # AFTEREVENT.STANDBY: "standby",
530 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
531 # AFTEREVENT.AUTO: "auto"}))
532 #if timer.eit is not None:
533 # t.set("eit", str(timer.eit))
534 #if timer.dirname is not None:
535 # t.set("location", str(timer.dirname))
536 #t.set("disabled", str(int(timer.disabled)))
537 #t.set("justplay", str(int(timer.justplay)))
541 #for time, code, msg in timer.log_entries:
542 #l = xml.etree.cElementTree.SubElement(t, 'log')
543 #l.set("time", str(time))
544 #l.set("code", str(code))
548 #doc = xml.etree.cElementTree.ElementTree(root_element)
549 #doc.write(self.Filename)
553 list.append('<?xml version="1.0" ?>\n')
554 list.append('<timers>\n')
556 for timer in self.timer_list + self.processed_timers:
560 list.append('<timer')
561 list.append(' begin="' + str(int(timer.begin)) + '"')
562 list.append(' end="' + str(int(timer.end)) + '"')
563 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
564 list.append(' repeated="' + str(int(timer.repeated)) + '"')
565 list.append(' name="' + str(stringToXML(timer.name)) + '"')
566 list.append(' description="' + str(stringToXML(timer.description)) + '"')
567 list.append(' afterevent="' + str(stringToXML({
568 AFTEREVENT.NONE: "nothing",
569 AFTEREVENT.STANDBY: "standby",
570 AFTEREVENT.DEEPSTANDBY: "deepstandby",
571 AFTEREVENT.AUTO: "auto"
572 }[timer.afterEvent])) + '"')
573 if timer.eit is not None:
574 list.append(' eit="' + str(timer.eit) + '"')
575 if timer.dirname is not None:
576 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
577 if timer.tags is not None:
578 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
579 list.append(' disabled="' + str(int(timer.disabled)) + '"')
580 list.append(' justplay="' + str(int(timer.justplay)) + '"')
583 if config.recording.debug.value:
584 for time, code, msg in timer.log_entries:
586 list.append(' code="' + str(code) + '"')
587 list.append(' time="' + str(time) + '"')
589 list.append(str(stringToXML(msg)))
590 list.append('</log>\n')
592 list.append('</timer>\n')
594 list.append('</timers>\n')
596 file = open(self.Filename, "w")
601 def getNextZapTime(self):
603 for timer in self.timer_list:
604 if not timer.justplay or timer.begin < now:
609 def getNextRecordingTime(self):
611 for timer in self.timer_list:
612 next_act = timer.getNextActivation()
613 if timer.justplay or next_act < now:
618 def isNextRecordAfterEventActionAuto(self):
621 for timer in self.timer_list:
622 if timer.justplay or timer.begin < now:
624 if t is None or t.begin == timer.begin:
626 if t.afterEvent == AFTEREVENT.AUTO:
630 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
631 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
632 if not timersanitycheck.check():
633 if ignoreTSC != True:
634 print "timer conflict detected!"
635 print timersanitycheck.getSimulTimerList()
636 return timersanitycheck.getSimulTimerList()
638 print "ignore timer conflict"
639 elif timersanitycheck.doubleCheck():
640 print "ignore double timer"
643 print "[Timer] Record " + str(entry)
645 self.addTimerEntry(entry)
650 def isInTimer(self, eventid, begin, duration, service):
654 chktimecmp_end = None
655 end = begin + duration
656 refstr = str(service)
657 for x in self.timer_list:
658 check = x.service_ref.ref.toString() == refstr
660 sref = x.service_ref.ref
661 parent_sid = sref.getUnsignedData(5)
662 parent_tsid = sref.getUnsignedData(6)
663 if parent_sid and parent_tsid: # check for subservice
664 sid = sref.getUnsignedData(1)
665 tsid = sref.getUnsignedData(2)
666 sref.setUnsignedData(1, parent_sid)
667 sref.setUnsignedData(2, parent_tsid)
668 sref.setUnsignedData(5, 0)
669 sref.setUnsignedData(6, 0)
670 check = sref.toCompareString() == refstr
674 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
675 num = event and event.getNumOfLinkageServices() or 0
676 sref.setUnsignedData(1, sid)
677 sref.setUnsignedData(2, tsid)
678 sref.setUnsignedData(5, parent_sid)
679 sref.setUnsignedData(6, parent_tsid)
680 for cnt in range(num):
681 subservice = event.getLinkageService(sref, cnt)
682 if sref.toCompareString() == subservice.toCompareString():
688 chktime = localtime(begin)
689 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
690 chktimecmp_end = chktimecmp + (duration / 60)
691 time = localtime(x.begin)
692 for y in (0, 1, 2, 3, 4, 5, 6):
693 if x.repeated & (1 << y) and (x.begin <= begin or begin <= x.begin <= end):
694 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
695 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
696 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
697 elif chktimecmp <= timecmp < chktimecmp_end:
698 time_match = (chktimecmp_end - timecmp) * 60
699 else: #if x.eit is None:
700 if begin <= x.begin <= end:
702 if time_match < diff:
704 elif x.begin <= begin <= x.end:
706 if time_match < diff:
712 def removeEntry(self, entry):
713 print "[Timer] Remove " + str(entry)
716 entry.repeated = False
719 # this sets the end time to current time, so timer will be stopped.
720 entry.autoincrease = False
723 if entry.state != entry.StateEnded:
724 self.timeChanged(entry)
726 print "state: ", entry.state
727 print "in processed: ", entry in self.processed_timers
728 print "in running: ", entry in self.timer_list
729 # autoincrease instanttimer if possible
730 if not entry.dontSave:
731 for x in self.timer_list:
732 if x.setAutoincreaseEnd():
734 # now the timer should be in the processed_timers list. remove it from there.
735 self.processed_timers.remove(entry)