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)
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.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 # fine. it worked, resources are allocated.
221 self.next_activation = self.begin
225 self.log(7, "prepare failed")
226 if self.first_try_prepare:
227 self.first_try_prepare = False
228 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
229 if cur_ref and not cur_ref.getPath():
230 if not config.recording.asktozap.value:
231 self.log(8, "asking user to zap away")
232 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
233 else: # zap without asking
234 self.log(9, "zap without asking")
235 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
238 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
240 self.log(8, "currently no service running... so we dont need to stop it")
242 elif next_state == self.StateRunning:
243 # if this timer has been cancelled, just go to "end" state.
248 if Screens.Standby.inStandby:
249 self.log(11, "wakeup and zap")
250 #set service to zap after standby
251 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
253 Screens.Standby.inStandby.Power()
255 self.log(11, "zapping")
256 NavigationInstance.instance.playService(self.service_ref.ref)
259 self.log(11, "start recording")
260 record_res = self.record_service.start()
263 self.log(13, "start record returned %d" % record_res)
266 self.begin = time.time() + self.backoff
270 elif next_state == self.StateEnded:
272 if self.setAutoincreaseEnd():
273 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
276 self.log(12, "stop recording")
277 if not self.justplay:
278 NavigationInstance.instance.stopRecordService(self.record_service)
279 self.record_service = None
280 if self.afterEvent == AFTEREVENT.STANDBY:
281 if not Screens.Standby.inStandby: # not already in standby
282 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
283 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
284 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
285 if Screens.Standby.inStandby: # in standby
286 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
288 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
291 def setAutoincreaseEnd(self, entry = None):
292 if not self.autoincrease:
295 new_end = int(time.time()) + self.autoincreasetime
297 new_end = entry.begin -30
299 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)
300 dummyentry.disabled = self.disabled
301 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
302 if not timersanitycheck.check():
303 simulTimerList = timersanitycheck.getSimulTimerList()
304 new_end = simulTimerList[1].begin
306 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
308 if new_end <= time.time():
314 def sendStandbyNotification(self, answer):
316 Notifications.AddNotification(Screens.Standby.Standby)
318 def sendTryQuitMainloopNotification(self, answer):
320 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
322 def getNextActivation(self):
323 if self.state == self.StateEnded:
326 next_state = self.state + 1
328 return {self.StatePrepared: self.start_prepare,
329 self.StateRunning: self.begin,
330 self.StateEnded: self.end }[next_state]
332 def failureCB(self, answer):
334 self.log(13, "ok, zapped away")
335 #NavigationInstance.instance.stopUserServices()
336 NavigationInstance.instance.playService(self.service_ref.ref)
338 self.log(14, "user didn't want to zap away, record will probably fail")
340 def timeChanged(self):
341 old_prepare = self.start_prepare
342 self.start_prepare = self.begin - self.prepare_time
345 if int(old_prepare) != int(self.start_prepare):
346 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
348 def gotRecordEvent(self, record, event):
349 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
350 if self.__record_service.__deref__() != record.__deref__():
352 self.log(16, "record event %d" % event)
353 if event == iRecordableService.evRecordWriteError:
354 print "WRITE ERROR on recording, disk full?"
355 # show notification. the 'id' will make sure that it will be
356 # displayed only once, even if more timers are failing at the
357 # same time. (which is very likely in case of disk fullness)
358 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
359 # ok, the recording has been stopped. we need to properly note
360 # that in our state, with also keeping the possibility to re-try.
361 # TODO: this has to be done.
362 elif event == iRecordableService.evStart:
363 text = _("A record has been started:\n%s") % self.name
364 if self.dirnameHadToFallback:
365 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
367 if config.usage.show_message_when_recording_starts.value:
368 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
370 # we have record_service as property to automatically subscribe to record service events
371 def setRecordService(self, service):
372 if self.__record_service is not None:
373 print "[remove callback]"
374 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
376 self.__record_service = service
378 if self.__record_service is not None:
379 print "[add callback]"
380 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
382 record_service = property(lambda self: self.__record_service, setRecordService)
384 def createTimer(xml):
385 begin = int(xml.get("begin"))
386 end = int(xml.get("end"))
387 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
388 description = xml.get("description").encode("utf-8")
389 repeated = xml.get("repeated").encode("utf-8")
390 disabled = long(xml.get("disabled") or "0")
391 justplay = long(xml.get("justplay") or "0")
392 afterevent = str(xml.get("afterevent") or "nothing")
394 "nothing": AFTEREVENT.NONE,
395 "standby": AFTEREVENT.STANDBY,
396 "deepstandby": AFTEREVENT.DEEPSTANDBY,
397 "auto": AFTEREVENT.AUTO
400 if eit and eit != "None":
404 location = xml.get("location")
405 if location and location != "None":
406 location = location.encode("utf-8")
409 tags = xml.get("tags")
410 if tags and tags != "None":
411 tags = tags.encode("utf-8").split(' ')
415 name = xml.get("name").encode("utf-8")
416 #filename = xml.get("filename").encode("utf-8")
417 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
418 entry.repeated = int(repeated)
420 for l in xml.findall("log"):
421 time = int(l.get("time"))
422 code = int(l.get("code"))
423 msg = l.text.strip().encode("utf-8")
424 entry.log_entries.append((time, code, msg))
428 class RecordTimer(timer.Timer):
430 timer.Timer.__init__(self)
432 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
437 print "unable to load timers from file!"
439 def isRecording(self):
441 for timer in self.timer_list:
442 if timer.isRunning() and not timer.justplay:
449 doc = xml.etree.cElementTree.parse(self.Filename)
451 from Tools.Notifications import AddPopup
452 from Screens.MessageBox import MessageBox
454 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
456 print "timers.xml failed to load!"
459 os.rename(self.Filename, self.Filename + "_old")
460 except (IOError, OSError):
461 print "renaming broken timer failed"
464 print "timers.xml not found!"
469 # put out a message when at least one timer overlaps
471 for timer in root.findall("timer"):
472 newTimer = createTimer(timer)
473 if (self.record(newTimer, True, True) is not None) and (checkit == True):
474 from Tools.Notifications import AddPopup
475 from Screens.MessageBox import MessageBox
476 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
477 checkit = False # at moment it is enough when the message is displayed one time
480 #root_element = xml.etree.cElementTree.Element('timers')
481 #root_element.text = "\n"
483 #for timer in self.timer_list + self.processed_timers:
484 # some timers (instant records) don't want to be saved.
488 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
489 #t.set("begin", str(int(timer.begin)))
490 #t.set("end", str(int(timer.end)))
491 #t.set("serviceref", str(timer.service_ref))
492 #t.set("repeated", str(timer.repeated))
493 #t.set("name", timer.name)
494 #t.set("description", timer.description)
495 #t.set("afterevent", str({
496 # AFTEREVENT.NONE: "nothing",
497 # AFTEREVENT.STANDBY: "standby",
498 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
499 # AFTEREVENT.AUTO: "auto"}))
500 #if timer.eit is not None:
501 # t.set("eit", str(timer.eit))
502 #if timer.dirname is not None:
503 # t.set("location", str(timer.dirname))
504 #t.set("disabled", str(int(timer.disabled)))
505 #t.set("justplay", str(int(timer.justplay)))
509 #for time, code, msg in timer.log_entries:
510 #l = xml.etree.cElementTree.SubElement(t, 'log')
511 #l.set("time", str(time))
512 #l.set("code", str(code))
516 #doc = xml.etree.cElementTree.ElementTree(root_element)
517 #doc.write(self.Filename)
521 list.append('<?xml version="1.0" ?>\n')
522 list.append('<timers>\n')
524 for timer in self.timer_list + self.processed_timers:
528 list.append('<timer')
529 list.append(' begin="' + str(int(timer.begin)) + '"')
530 list.append(' end="' + str(int(timer.end)) + '"')
531 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
532 list.append(' repeated="' + str(int(timer.repeated)) + '"')
533 list.append(' name="' + str(stringToXML(timer.name)) + '"')
534 list.append(' description="' + str(stringToXML(timer.description)) + '"')
535 list.append(' afterevent="' + str(stringToXML({
536 AFTEREVENT.NONE: "nothing",
537 AFTEREVENT.STANDBY: "standby",
538 AFTEREVENT.DEEPSTANDBY: "deepstandby",
539 AFTEREVENT.AUTO: "auto"
540 }[timer.afterEvent])) + '"')
541 if timer.eit is not None:
542 list.append(' eit="' + str(timer.eit) + '"')
543 if timer.dirname is not None:
544 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
545 if timer.tags is not None:
546 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
547 list.append(' disabled="' + str(int(timer.disabled)) + '"')
548 list.append(' justplay="' + str(int(timer.justplay)) + '"')
551 if config.recording.debug.value:
552 for time, code, msg in timer.log_entries:
554 list.append(' code="' + str(code) + '"')
555 list.append(' time="' + str(time) + '"')
557 list.append(str(stringToXML(msg)))
558 list.append('</log>\n')
560 list.append('</timer>\n')
562 list.append('</timers>\n')
564 file = open(self.Filename, "w")
569 def getNextZapTime(self):
571 for timer in self.timer_list:
572 if not timer.justplay or timer.begin < now:
577 def getNextRecordingTime(self):
579 for timer in self.timer_list:
581 next_act = timer.getNextActivation()
582 if timer.justplay or next_act < now:
587 def isNextRecordAfterEventActionAuto(self):
590 for timer in self.timer_list:
591 if timer.justplay or timer.begin < now:
593 if t is None or t.begin == timer.begin:
595 if t.afterEvent == AFTEREVENT.AUTO:
599 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
600 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
601 if not timersanitycheck.check():
602 if ignoreTSC != True:
603 print "timer conflict detected!"
604 print timersanitycheck.getSimulTimerList()
605 return timersanitycheck.getSimulTimerList()
607 print "ignore timer conflict"
608 elif timersanitycheck.doubleCheck():
609 print "ignore double timer"
612 print "[Timer] Record " + str(entry)
614 self.addTimerEntry(entry)
619 def isInTimer(self, eventid, begin, duration, service):
623 chktimecmp_end = None
624 end = begin + duration
625 refstr = str(service)
626 for x in self.timer_list:
627 check = x.service_ref.ref.toString() == refstr
629 sref = x.service_ref.ref
630 parent_sid = sref.getUnsignedData(5)
631 parent_tsid = sref.getUnsignedData(6)
632 if parent_sid and parent_tsid: # check for subservice
633 sid = sref.getUnsignedData(1)
634 tsid = sref.getUnsignedData(2)
635 sref.setUnsignedData(1, parent_sid)
636 sref.setUnsignedData(2, parent_tsid)
637 sref.setUnsignedData(5, 0)
638 sref.setUnsignedData(6, 0)
639 check = sref.toCompareString() == refstr
643 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
644 num = event and event.getNumOfLinkageServices() or 0
645 sref.setUnsignedData(1, sid)
646 sref.setUnsignedData(2, tsid)
647 sref.setUnsignedData(5, parent_sid)
648 sref.setUnsignedData(6, parent_tsid)
649 for cnt in range(num):
650 subservice = event.getLinkageService(sref, cnt)
651 if sref.toCompareString() == subservice.toCompareString():
657 chktime = localtime(begin)
658 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
659 chktimecmp_end = chktimecmp + (duration / 60)
660 time = localtime(x.begin)
661 for y in (0, 1, 2, 3, 4, 5, 6):
662 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
663 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
664 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
665 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
666 elif chktimecmp <= timecmp < chktimecmp_end:
667 time_match = (chktimecmp_end - timecmp) * 60
668 else: #if x.eit is None:
669 if begin <= x.begin <= end:
671 if time_match < diff:
673 elif x.begin <= begin <= x.end:
675 if time_match < diff:
681 def removeEntry(self, entry):
682 print "[Timer] Remove " + str(entry)
685 entry.repeated = False
688 # this sets the end time to current time, so timer will be stopped.
689 entry.autoincrease = False
692 if entry.state != entry.StateEnded:
693 self.timeChanged(entry)
695 print "state: ", entry.state
696 print "in processed: ", entry in self.processed_timers
697 print "in running: ", entry in self.timer_list
698 # autoincrease instanttimer if possible
699 if not entry.dontSave:
700 for x in self.timer_list:
701 if x.setAutoincreaseEnd():
703 # now the timer should be in the processed_timers list. remove it from there.
704 self.processed_timers.remove(entry)