2 #from time import datetime
3 from Tools import Directories, Notifications, ASCIItranslit
5 from Components.config import config
7 import xml.etree.cElementTree
9 from enigma import eEPGCache, getBestPlayableServiceReference, \
10 eServiceReference, iRecordableService, quitMainloop
12 from Screens.MessageBox import MessageBox
13 from Components.TimerSanityCheck import TimerSanityCheck
14 import NavigationInstance
16 import Screens.Standby
18 from time import localtime
20 from Tools.XMLTools import stringToXML
21 from ServiceReference import ServiceReference
23 # ok, for descriptions etc we have:
24 # service reference (to get the service name)
26 # description (description)
27 # event data (ONLY for time adjustments etc.)
30 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
31 # begin and end will be corrected
32 def parseEvent(ev, description = True):
34 name = ev.getEventName()
35 description = ev.getShortDescription()
39 begin = ev.getBeginTime()
40 end = begin + ev.getDuration()
42 begin -= config.recording.margin_before.value * 60
43 end += config.recording.margin_after.value * 60
44 return (begin, end, name, description, eit)
52 # please do not translate log messages
53 class RecordTimerEntry(timer.TimerEntry, object):
54 ######### the following static methods and members are only in use when the box is in (soft) standby
55 receiveRecordEvents = False
62 def staticGotRecordEvent(recservice, event):
63 if event == iRecordableService.evEnd:
64 print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
65 recordings = NavigationInstance.instance.getRecordings()
66 if not recordings: # no more recordings exist
67 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
68 if rec_time > 0 and (rec_time - time.time()) < 360:
69 print "another recording starts in", rec_time - time.time(), "seconds... do not shutdown yet"
71 print "no starting records in the next 360 seconds... immediate shutdown"
72 RecordTimerEntry.shutdown() # immediate shutdown
73 elif event == iRecordableService.evStart:
74 print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
77 def stopTryQuitMainloop():
78 print "RecordTimer.stopTryQuitMainloop"
79 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
80 RecordTimerEntry.receiveRecordEvents = False
83 def TryQuitMainloop(default_yes = True):
84 if not RecordTimerEntry.receiveRecordEvents:
85 print "RecordTimer.TryQuitMainloop"
86 NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
87 RecordTimerEntry.receiveRecordEvents = True
88 # send fake event.. to check if another recordings are running or
89 # other timers start in a few seconds
90 RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
91 # send normal notification for the case the user leave the standby now..
92 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop, default_yes = default_yes)
93 #################################################################
95 def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.AUTO, checkOldTimers = False, dirname = None, tags = None):
96 timer.TimerEntry.__init__(self, int(begin), int(end))
98 if checkOldTimers == True:
99 if self.begin < time.time() - 1209600:
100 self.begin = int(time.time())
102 if self.end < self.begin:
103 self.end = self.begin
105 assert isinstance(serviceref, ServiceReference)
107 self.service_ref = serviceref
109 self.dontSave = False
111 self.description = description
112 self.disabled = disabled
114 self.__record_service = None
115 self.start_prepare = 0
116 self.justplay = justplay
117 self.afterEvent = afterEvent
118 self.dirname = dirname
119 self.dirnameHadToFallback = False
120 self.autoincrease = False
121 self.autoincreasetime = 3600 * 24 # 1 day
122 self.tags = tags or []
124 self.log_entries = []
127 def log(self, code, msg):
128 self.log_entries.append((int(time.time()), code, msg))
131 def calculateFilename(self):
132 service_name = self.service_ref.getServiceName()
133 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
135 print "begin_date: ", begin_date
136 print "service_name: ", service_name
137 print "name:", self.name
138 print "description: ", self.description
140 filename = begin_date + " - " + service_name
142 filename += " - " + self.name
144 if config.recording.ascii_filenames.value:
145 filename = ASCIItranslit.legacyEncode(filename)
147 if self.dirname and not Directories.fileExists(self.dirname, 'w'):
148 self.dirnameHadToFallback = True
149 self.Filename = Directories.getRecordingFilename(filename, None)
151 self.Filename = Directories.getRecordingFilename(filename, self.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)
193 NavigationInstance.instance.stopRecordService(self.record_service)
194 self.record_service = None
198 def do_backoff(self):
199 if self.backoff == 0:
203 if self.backoff > 100:
205 self.log(10, "backoff: retry in %d seconds" % self.backoff)
208 next_state = self.state + 1
209 self.log(5, "activating state %d" % next_state)
211 if next_state == self.StatePrepared:
212 if self.tryPrepare():
213 self.log(6, "prepare ok, waiting for begin")
214 # fine. it worked, resources are allocated.
215 self.next_activation = self.begin
219 self.log(7, "prepare failed")
220 if self.first_try_prepare:
221 self.first_try_prepare = False
222 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
223 if cur_ref and not cur_ref.getPath():
224 if not config.recording.asktozap.value:
225 self.log(8, "asking user to zap away")
226 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
227 else: # zap without asking
228 self.log(9, "zap without asking")
229 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
232 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
234 self.log(8, "currently no service running... so we dont need to stop it")
238 self.start_prepare = time.time() + self.backoff
240 elif next_state == self.StateRunning:
241 # if this timer has been cancelled, just go to "end" state.
246 if Screens.Standby.inStandby:
247 self.log(11, "wakeup and zap")
248 #set service to zap after standby
249 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
251 Screens.Standby.inStandby.Power()
253 self.log(11, "zapping")
254 NavigationInstance.instance.playService(self.service_ref.ref)
257 self.log(11, "start recording")
258 record_res = self.record_service.start()
261 self.log(13, "start record returned %d" % record_res)
264 self.begin = time.time() + self.backoff
268 elif next_state == self.StateEnded:
270 if self.setAutoincreaseEnd():
271 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
274 self.log(12, "stop recording")
275 if not self.justplay:
276 NavigationInstance.instance.stopRecordService(self.record_service)
277 self.record_service = None
278 if self.afterEvent == AFTEREVENT.STANDBY:
279 if not Screens.Standby.inStandby: # not already in standby
280 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
281 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
282 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
283 if Screens.Standby.inStandby: # in standby
284 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
286 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
289 def setAutoincreaseEnd(self, entry = None):
290 if not self.autoincrease:
293 new_end = int(time.time()) + self.autoincreasetime
295 new_end = entry.begin -30
297 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)
298 dummyentry.disabled = self.disabled
299 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
300 if not timersanitycheck.check():
301 simulTimerList = timersanitycheck.getSimulTimerList()
302 new_end = simulTimerList[1].begin
304 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
306 if new_end <= time.time():
312 def sendStandbyNotification(self, answer):
314 Notifications.AddNotification(Screens.Standby.Standby)
316 def sendTryQuitMainloopNotification(self, answer):
318 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
320 def getNextActivation(self):
321 if self.state == self.StateEnded:
324 next_state = self.state + 1
326 return {self.StatePrepared: self.start_prepare,
327 self.StateRunning: self.begin,
328 self.StateEnded: self.end }[next_state]
330 def failureCB(self, answer):
332 self.log(13, "ok, zapped away")
333 #NavigationInstance.instance.stopUserServices()
334 NavigationInstance.instance.playService(self.service_ref.ref)
336 self.log(14, "user didn't want to zap away, record will probably fail")
338 def timeChanged(self):
339 old_prepare = self.start_prepare
340 self.start_prepare = self.begin - self.prepare_time
343 if int(old_prepare) != int(self.start_prepare):
344 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
346 def gotRecordEvent(self, record, event):
347 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
348 if self.__record_service.__deref__() != record.__deref__():
350 self.log(16, "record event %d" % event)
351 if event == iRecordableService.evRecordWriteError:
352 print "WRITE ERROR on recording, disk full?"
353 # show notification. the 'id' will make sure that it will be
354 # displayed only once, even if more timers are failing at the
355 # same time. (which is very likely in case of disk fullness)
356 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
357 # ok, the recording has been stopped. we need to properly note
358 # that in our state, with also keeping the possibility to re-try.
359 # TODO: this has to be done.
360 elif event == iRecordableService.evStart:
361 text = _("A record has been started:\n%s") % self.name
362 if self.dirnameHadToFallback:
363 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
365 if config.usage.show_message_when_recording_starts.value:
366 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
368 # we have record_service as property to automatically subscribe to record service events
369 def setRecordService(self, service):
370 if self.__record_service is not None:
371 print "[remove callback]"
372 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
374 self.__record_service = service
376 if self.__record_service is not None:
377 print "[add callback]"
378 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
380 record_service = property(lambda self: self.__record_service, setRecordService)
382 def createTimer(xml):
383 begin = int(xml.get("begin"))
384 end = int(xml.get("end"))
385 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
386 description = xml.get("description").encode("utf-8")
387 repeated = xml.get("repeated").encode("utf-8")
388 disabled = long(xml.get("disabled") or "0")
389 justplay = long(xml.get("justplay") or "0")
390 afterevent = str(xml.get("afterevent") or "nothing")
392 "nothing": AFTEREVENT.NONE,
393 "standby": AFTEREVENT.STANDBY,
394 "deepstandby": AFTEREVENT.DEEPSTANDBY,
395 "auto": AFTEREVENT.AUTO
398 if eit and eit != "None":
402 location = xml.get("location")
403 if location and location != "None":
404 location = location.encode("utf-8")
407 tags = xml.get("tags")
408 if tags and tags != "None":
409 tags = tags.encode("utf-8").split(' ')
413 name = xml.get("name").encode("utf-8")
414 #filename = xml.get("filename").encode("utf-8")
415 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
416 entry.repeated = int(repeated)
418 for l in xml.findall("log"):
419 time = int(l.get("time"))
420 code = int(l.get("code"))
421 msg = l.text.strip().encode("utf-8")
422 entry.log_entries.append((time, code, msg))
426 class RecordTimer(timer.Timer):
428 timer.Timer.__init__(self)
430 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
435 print "unable to load timers from file!"
437 def isRecording(self):
439 for timer in self.timer_list:
440 if timer.isRunning() and not timer.justplay:
447 doc = xml.etree.cElementTree.parse(self.Filename)
449 from Tools.Notifications import AddPopup
450 from Screens.MessageBox import MessageBox
452 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
454 print "timers.xml failed to load!"
457 os.rename(self.Filename, self.Filename + "_old")
458 except (IOError, OSError):
459 print "renaming broken timer failed"
462 print "timers.xml not found!"
467 # put out a message when at least one timer overlaps
469 for timer in root.findall("timer"):
470 newTimer = createTimer(timer)
471 if (self.record(newTimer, True, True) is not None) and (checkit == True):
472 from Tools.Notifications import AddPopup
473 from Screens.MessageBox import MessageBox
474 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
475 checkit = False # at moment it is enough when the message is displayed one time
478 #root_element = xml.etree.cElementTree.Element('timers')
479 #root_element.text = "\n"
481 #for timer in self.timer_list + self.processed_timers:
482 # some timers (instant records) don't want to be saved.
486 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
487 #t.set("begin", str(int(timer.begin)))
488 #t.set("end", str(int(timer.end)))
489 #t.set("serviceref", str(timer.service_ref))
490 #t.set("repeated", str(timer.repeated))
491 #t.set("name", timer.name)
492 #t.set("description", timer.description)
493 #t.set("afterevent", str({
494 # AFTEREVENT.NONE: "nothing",
495 # AFTEREVENT.STANDBY: "standby",
496 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
497 # AFTEREVENT.AUTO: "auto"}))
498 #if timer.eit is not None:
499 # t.set("eit", str(timer.eit))
500 #if timer.dirname is not None:
501 # t.set("location", str(timer.dirname))
502 #t.set("disabled", str(int(timer.disabled)))
503 #t.set("justplay", str(int(timer.justplay)))
507 #for time, code, msg in timer.log_entries:
508 #l = xml.etree.cElementTree.SubElement(t, 'log')
509 #l.set("time", str(time))
510 #l.set("code", str(code))
514 #doc = xml.etree.cElementTree.ElementTree(root_element)
515 #doc.write(self.Filename)
519 list.append('<?xml version="1.0" ?>\n')
520 list.append('<timers>\n')
522 for timer in self.timer_list + self.processed_timers:
526 list.append('<timer')
527 list.append(' begin="' + str(int(timer.begin)) + '"')
528 list.append(' end="' + str(int(timer.end)) + '"')
529 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
530 list.append(' repeated="' + str(int(timer.repeated)) + '"')
531 list.append(' name="' + str(stringToXML(timer.name)) + '"')
532 list.append(' description="' + str(stringToXML(timer.description)) + '"')
533 list.append(' afterevent="' + str(stringToXML({
534 AFTEREVENT.NONE: "nothing",
535 AFTEREVENT.STANDBY: "standby",
536 AFTEREVENT.DEEPSTANDBY: "deepstandby",
537 AFTEREVENT.AUTO: "auto"
538 }[timer.afterEvent])) + '"')
539 if timer.eit is not None:
540 list.append(' eit="' + str(timer.eit) + '"')
541 if timer.dirname is not None:
542 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
543 if timer.tags is not None:
544 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
545 list.append(' disabled="' + str(int(timer.disabled)) + '"')
546 list.append(' justplay="' + str(int(timer.justplay)) + '"')
549 if config.recording.debug.value:
550 for time, code, msg in timer.log_entries:
552 list.append(' code="' + str(code) + '"')
553 list.append(' time="' + str(time) + '"')
555 list.append(str(stringToXML(msg)))
556 list.append('</log>\n')
558 list.append('</timer>\n')
560 list.append('</timers>\n')
562 file = open(self.Filename, "w")
567 def getNextZapTime(self):
569 for timer in self.timer_list:
570 if not timer.justplay or timer.begin < now:
575 def getNextRecordingTime(self):
577 for timer in self.timer_list:
578 if timer.justplay or timer.begin < now:
583 def isNextRecordAfterEventActionAuto(self):
586 for timer in self.timer_list:
587 if timer.justplay or timer.begin < now:
589 if t is None or t.begin == timer.begin:
591 if t.afterEvent == AFTEREVENT.AUTO:
595 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
596 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
597 if not timersanitycheck.check():
598 if ignoreTSC != True:
599 print "timer conflict detected!"
600 print timersanitycheck.getSimulTimerList()
601 return timersanitycheck.getSimulTimerList()
603 print "ignore timer conflict"
604 elif timersanitycheck.doubleCheck():
605 print "ignore double timer"
608 print "[Timer] Record " + str(entry)
610 self.addTimerEntry(entry)
615 def isInTimer(self, eventid, begin, duration, service):
619 chktimecmp_end = None
620 end = begin + duration
621 refstr = str(service)
622 for x in self.timer_list:
623 check = x.service_ref.ref.toString() == refstr
625 sref = x.service_ref.ref
626 parent_sid = sref.getUnsignedData(5)
627 parent_tsid = sref.getUnsignedData(6)
628 if parent_sid and parent_tsid: # check for subservice
629 sid = sref.getUnsignedData(1)
630 tsid = sref.getUnsignedData(2)
631 sref.setUnsignedData(1, parent_sid)
632 sref.setUnsignedData(2, parent_tsid)
633 sref.setUnsignedData(5, 0)
634 sref.setUnsignedData(6, 0)
635 check = sref.toCompareString() == refstr
639 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
640 num = event and event.getNumOfLinkageServices() or 0
641 sref.setUnsignedData(1, sid)
642 sref.setUnsignedData(2, tsid)
643 sref.setUnsignedData(5, parent_sid)
644 sref.setUnsignedData(6, parent_tsid)
645 for cnt in range(num):
646 subservice = event.getLinkageService(sref, cnt)
647 if sref.toCompareString() == subservice.toCompareString():
653 chktime = localtime(begin)
654 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
655 chktimecmp_end = chktimecmp + (duration / 60)
656 time = localtime(x.begin)
657 for y in (0, 1, 2, 3, 4, 5, 6):
658 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
659 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
660 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
661 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
662 elif chktimecmp <= timecmp < chktimecmp_end:
663 time_match = (chktimecmp_end - timecmp) * 60
664 else: #if x.eit is None:
665 if begin <= x.begin <= end:
667 if time_match < diff:
669 elif x.begin <= begin <= x.end:
671 if time_match < diff:
677 def removeEntry(self, entry):
678 print "[Timer] Remove " + str(entry)
681 entry.repeated = False
684 # this sets the end time to current time, so timer will be stopped.
685 entry.autoincrease = False
688 if entry.state != entry.StateEnded:
689 self.timeChanged(entry)
691 print "state: ", entry.state
692 print "in processed: ", entry in self.processed_timers
693 print "in running: ", entry in self.timer_list
694 # autoincrease instanttimer if possible
695 if not entry.dontSave:
696 for x in self.timer_list:
697 if x.setAutoincreaseEnd():
699 # now the timer should be in the processed_timers list. remove it from there.
700 self.processed_timers.remove(entry)