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, localtime, 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 open(self.Filename + ".ts", "w").close()
222 # fine. it worked, resources are allocated.
223 self.next_activation = self.begin
227 self.log(7, "prepare failed")
228 if self.first_try_prepare:
229 self.first_try_prepare = False
230 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
231 if cur_ref and not cur_ref.getPath():
232 if not config.recording.asktozap.value:
233 self.log(8, "asking user to zap away")
234 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
235 else: # zap without asking
236 self.log(9, "zap without asking")
237 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
240 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
242 self.log(8, "currently no service running... so we dont need to stop it")
244 elif next_state == self.StateRunning:
245 # if this timer has been cancelled, just go to "end" state.
250 if Screens.Standby.inStandby:
251 self.log(11, "wakeup and zap")
252 #set service to zap after standby
253 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
255 Screens.Standby.inStandby.Power()
257 self.log(11, "zapping")
258 NavigationInstance.instance.playService(self.service_ref.ref)
261 self.log(11, "start recording")
262 record_res = self.record_service.start()
265 self.log(13, "start record returned %d" % record_res)
268 self.begin = time() + self.backoff
272 elif next_state == self.StateEnded:
274 if self.setAutoincreaseEnd():
275 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
278 self.log(12, "stop recording")
279 if not self.justplay:
280 NavigationInstance.instance.stopRecordService(self.record_service)
281 self.record_service = None
282 if self.afterEvent == AFTEREVENT.STANDBY:
283 if not Screens.Standby.inStandby: # not already in standby
284 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
285 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
286 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
287 if Screens.Standby.inStandby: # in standby
288 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
290 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
293 def setAutoincreaseEnd(self, entry = None):
294 if not self.autoincrease:
297 new_end = int(time()) + self.autoincreasetime
299 new_end = entry.begin -30
301 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)
302 dummyentry.disabled = self.disabled
303 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
304 if not timersanitycheck.check():
305 simulTimerList = timersanitycheck.getSimulTimerList()
306 new_end = simulTimerList[1].begin
308 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
310 if new_end <= time():
316 def sendStandbyNotification(self, answer):
318 Notifications.AddNotification(Screens.Standby.Standby)
320 def sendTryQuitMainloopNotification(self, answer):
322 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
324 def getNextActivation(self):
325 if self.state == self.StateEnded:
328 next_state = self.state + 1
330 return {self.StatePrepared: self.start_prepare,
331 self.StateRunning: self.begin,
332 self.StateEnded: self.end }[next_state]
334 def failureCB(self, answer):
336 self.log(13, "ok, zapped away")
337 #NavigationInstance.instance.stopUserServices()
338 NavigationInstance.instance.playService(self.service_ref.ref)
340 self.log(14, "user didn't want to zap away, record will probably fail")
342 def timeChanged(self):
343 old_prepare = self.start_prepare
344 self.start_prepare = self.begin - self.prepare_time
347 if int(old_prepare) != int(self.start_prepare):
348 self.log(15, "record time changed, start prepare is now: %s" % ctime(self.start_prepare))
350 def gotRecordEvent(self, record, event):
351 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
352 if self.__record_service.__deref__() != record.__deref__():
354 self.log(16, "record event %d" % event)
355 if event == iRecordableService.evRecordWriteError:
356 print "WRITE ERROR on recording, disk full?"
357 # show notification. the 'id' will make sure that it will be
358 # displayed only once, even if more timers are failing at the
359 # same time. (which is very likely in case of disk fullness)
360 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
361 # ok, the recording has been stopped. we need to properly note
362 # that in our state, with also keeping the possibility to re-try.
363 # TODO: this has to be done.
364 elif event == iRecordableService.evStart:
365 text = _("A record has been started:\n%s") % self.name
366 if self.dirnameHadToFallback:
367 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
369 if config.usage.show_message_when_recording_starts.value:
370 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
372 # we have record_service as property to automatically subscribe to record service events
373 def setRecordService(self, service):
374 if self.__record_service is not None:
375 print "[remove callback]"
376 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
378 self.__record_service = service
380 if self.__record_service is not None:
381 print "[add callback]"
382 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
384 record_service = property(lambda self: self.__record_service, setRecordService)
386 def createTimer(xml):
387 begin = int(xml.get("begin"))
388 end = int(xml.get("end"))
389 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
390 description = xml.get("description").encode("utf-8")
391 repeated = xml.get("repeated").encode("utf-8")
392 disabled = long(xml.get("disabled") or "0")
393 justplay = long(xml.get("justplay") or "0")
394 afterevent = str(xml.get("afterevent") or "nothing")
396 "nothing": AFTEREVENT.NONE,
397 "standby": AFTEREVENT.STANDBY,
398 "deepstandby": AFTEREVENT.DEEPSTANDBY,
399 "auto": AFTEREVENT.AUTO
402 if eit and eit != "None":
406 location = xml.get("location")
407 if location and location != "None":
408 location = location.encode("utf-8")
411 tags = xml.get("tags")
412 if tags and tags != "None":
413 tags = tags.encode("utf-8").split(' ')
417 name = xml.get("name").encode("utf-8")
418 #filename = xml.get("filename").encode("utf-8")
419 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
420 entry.repeated = int(repeated)
422 for l in xml.findall("log"):
423 time = int(l.get("time"))
424 code = int(l.get("code"))
425 msg = l.text.strip().encode("utf-8")
426 entry.log_entries.append((time, code, msg))
430 class RecordTimer(timer.Timer):
432 timer.Timer.__init__(self)
434 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
439 print "unable to load timers from file!"
441 def doActivate(self, w):
442 # when activating a timer which has already passed,
443 # simply abort the timer. don't run trough all the stages.
445 w.state = RecordTimerEntry.StateEnded
447 # when active returns true, this means "accepted".
448 # otherwise, the current state is kept.
449 # the timer entry itself will fix up the delay then.
453 self.timer_list.remove(w)
455 # did this timer reached the last state?
456 if w.state < RecordTimerEntry.StateEnded:
457 # no, sort it into active list
458 insort(self.timer_list, w)
460 # yes. Process repeated, and re-add.
463 w.state = RecordTimerEntry.StateWaiting
464 self.addTimerEntry(w)
466 insort(self.processed_timers, w)
470 def isRecording(self):
472 for timer in self.timer_list:
473 if timer.isRunning() and not timer.justplay:
480 doc = xml.etree.cElementTree.parse(self.Filename)
482 from Tools.Notifications import AddPopup
483 from Screens.MessageBox import MessageBox
485 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
487 print "timers.xml failed to load!"
490 os.rename(self.Filename, self.Filename + "_old")
491 except (IOError, OSError):
492 print "renaming broken timer failed"
495 print "timers.xml not found!"
500 # put out a message when at least one timer overlaps
502 for timer in root.findall("timer"):
503 newTimer = createTimer(timer)
504 if (self.record(newTimer, True, True) is not None) and (checkit == True):
505 from Tools.Notifications import AddPopup
506 from Screens.MessageBox import MessageBox
507 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
508 checkit = False # at moment it is enough when the message is displayed one time
511 #root_element = xml.etree.cElementTree.Element('timers')
512 #root_element.text = "\n"
514 #for timer in self.timer_list + self.processed_timers:
515 # some timers (instant records) don't want to be saved.
519 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
520 #t.set("begin", str(int(timer.begin)))
521 #t.set("end", str(int(timer.end)))
522 #t.set("serviceref", str(timer.service_ref))
523 #t.set("repeated", str(timer.repeated))
524 #t.set("name", timer.name)
525 #t.set("description", timer.description)
526 #t.set("afterevent", str({
527 # AFTEREVENT.NONE: "nothing",
528 # AFTEREVENT.STANDBY: "standby",
529 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
530 # AFTEREVENT.AUTO: "auto"}))
531 #if timer.eit is not None:
532 # t.set("eit", str(timer.eit))
533 #if timer.dirname is not None:
534 # t.set("location", str(timer.dirname))
535 #t.set("disabled", str(int(timer.disabled)))
536 #t.set("justplay", str(int(timer.justplay)))
540 #for time, code, msg in timer.log_entries:
541 #l = xml.etree.cElementTree.SubElement(t, 'log')
542 #l.set("time", str(time))
543 #l.set("code", str(code))
547 #doc = xml.etree.cElementTree.ElementTree(root_element)
548 #doc.write(self.Filename)
552 list.append('<?xml version="1.0" ?>\n')
553 list.append('<timers>\n')
555 for timer in self.timer_list + self.processed_timers:
559 list.append('<timer')
560 list.append(' begin="' + str(int(timer.begin)) + '"')
561 list.append(' end="' + str(int(timer.end)) + '"')
562 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
563 list.append(' repeated="' + str(int(timer.repeated)) + '"')
564 list.append(' name="' + str(stringToXML(timer.name)) + '"')
565 list.append(' description="' + str(stringToXML(timer.description)) + '"')
566 list.append(' afterevent="' + str(stringToXML({
567 AFTEREVENT.NONE: "nothing",
568 AFTEREVENT.STANDBY: "standby",
569 AFTEREVENT.DEEPSTANDBY: "deepstandby",
570 AFTEREVENT.AUTO: "auto"
571 }[timer.afterEvent])) + '"')
572 if timer.eit is not None:
573 list.append(' eit="' + str(timer.eit) + '"')
574 if timer.dirname is not None:
575 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
576 if timer.tags is not None:
577 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
578 list.append(' disabled="' + str(int(timer.disabled)) + '"')
579 list.append(' justplay="' + str(int(timer.justplay)) + '"')
582 if config.recording.debug.value:
583 for time, code, msg in timer.log_entries:
585 list.append(' code="' + str(code) + '"')
586 list.append(' time="' + str(time) + '"')
588 list.append(str(stringToXML(msg)))
589 list.append('</log>\n')
591 list.append('</timer>\n')
593 list.append('</timers>\n')
595 file = open(self.Filename, "w")
600 def getNextZapTime(self):
602 for timer in self.timer_list:
603 if not timer.justplay or timer.begin < now:
608 def getNextRecordingTime(self):
610 for timer in self.timer_list:
611 next_act = timer.getNextActivation()
612 if timer.justplay or next_act < now:
617 def isNextRecordAfterEventActionAuto(self):
620 for timer in self.timer_list:
621 if timer.justplay or timer.begin < now:
623 if t is None or t.begin == timer.begin:
625 if t.afterEvent == AFTEREVENT.AUTO:
629 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
630 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
631 if not timersanitycheck.check():
632 if ignoreTSC != True:
633 print "timer conflict detected!"
634 print timersanitycheck.getSimulTimerList()
635 return timersanitycheck.getSimulTimerList()
637 print "ignore timer conflict"
638 elif timersanitycheck.doubleCheck():
639 print "ignore double timer"
642 print "[Timer] Record " + str(entry)
644 self.addTimerEntry(entry)
649 def isInTimer(self, eventid, begin, duration, service):
653 chktimecmp_end = None
654 end = begin + duration
655 refstr = str(service)
656 for x in self.timer_list:
657 check = x.service_ref.ref.toString() == refstr
659 sref = x.service_ref.ref
660 parent_sid = sref.getUnsignedData(5)
661 parent_tsid = sref.getUnsignedData(6)
662 if parent_sid and parent_tsid: # check for subservice
663 sid = sref.getUnsignedData(1)
664 tsid = sref.getUnsignedData(2)
665 sref.setUnsignedData(1, parent_sid)
666 sref.setUnsignedData(2, parent_tsid)
667 sref.setUnsignedData(5, 0)
668 sref.setUnsignedData(6, 0)
669 check = sref.toCompareString() == refstr
673 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
674 num = event and event.getNumOfLinkageServices() or 0
675 sref.setUnsignedData(1, sid)
676 sref.setUnsignedData(2, tsid)
677 sref.setUnsignedData(5, parent_sid)
678 sref.setUnsignedData(6, parent_tsid)
679 for cnt in range(num):
680 subservice = event.getLinkageService(sref, cnt)
681 if sref.toCompareString() == subservice.toCompareString():
687 chktime = localtime(begin)
688 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
689 chktimecmp_end = chktimecmp + (duration / 60)
690 time = localtime(x.begin)
691 for y in (0, 1, 2, 3, 4, 5, 6):
692 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
693 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
694 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
695 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
696 elif chktimecmp <= timecmp < chktimecmp_end:
697 time_match = (chktimecmp_end - timecmp) * 60
698 else: #if x.eit is None:
699 if begin <= x.begin <= end:
701 if time_match < diff:
703 elif x.begin <= begin <= x.end:
705 if time_match < diff:
711 def removeEntry(self, entry):
712 print "[Timer] Remove " + str(entry)
715 entry.repeated = False
718 # this sets the end time to current time, so timer will be stopped.
719 entry.autoincrease = False
722 if entry.state != entry.StateEnded:
723 self.timeChanged(entry)
725 print "state: ", entry.state
726 print "in processed: ", entry in self.processed_timers
727 print "in running: ", entry in self.timer_list
728 # autoincrease instanttimer if possible
729 if not entry.dontSave:
730 for x in self.timer_list:
731 if x.setAutoincreaseEnd():
733 # now the timer should be in the processed_timers list. remove it from there.
734 self.processed_timers.remove(entry)