2 #from time import datetime
3 from Tools import Directories, Notifications
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 len(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.tags = tags or []
123 self.log_entries = []
126 def log(self, code, msg):
127 self.log_entries.append((int(time.time()), code, msg))
130 def calculateFilename(self):
131 service_name = self.service_ref.getServiceName()
132 begin_date = time.strftime("%Y%m%d %H%M", time.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 filename += " - " + self.name
143 if self.dirname and not Directories.pathExists(self.dirname):
144 self.dirnameHadToFallback = True
145 self.Filename = Directories.getRecordingFilename(filename, None)
147 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
148 self.log(0, "Filename calculated as: '%s'" % self.Filename)
149 #begin_date + " - " + service_name + description)
151 def tryPrepare(self):
155 self.calculateFilename()
156 rec_ref = self.service_ref and self.service_ref.ref
157 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
158 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
160 self.log(1, "'get best playable service for group... record' failed")
163 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
165 if not self.record_service:
166 self.log(1, "'record service' failed")
170 epgcache = eEPGCache.getInstance()
171 queryTime=self.begin+(self.end-self.begin)/2
172 evt = epgcache.lookupEventTime(rec_ref, queryTime)
174 self.description = evt.getShortDescription()
175 event_id = evt.getEventId()
183 prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
185 self.log(2, "'prepare' failed: error %d" % prep_res)
186 NavigationInstance.instance.stopRecordService(self.record_service)
187 self.record_service = None
190 self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
192 f = open(self.Filename + ".ts.meta", "w")
193 f.write(rec_ref.toString() + "\n")
194 f.write(self.name + "\n")
195 f.write(self.description + "\n")
196 f.write(str(self.begin) + "\n")
197 f.write(' '.join(self.tags))
200 self.log(4, "failed to write meta information")
201 NavigationInstance.instance.stopRecordService(self.record_service)
202 self.record_service = None
206 def do_backoff(self):
207 if self.backoff == 0:
211 if self.backoff > 100:
213 self.log(10, "backoff: retry in %d seconds" % self.backoff)
216 next_state = self.state + 1
217 self.log(5, "activating state %d" % next_state)
219 if next_state == self.StatePrepared:
220 if self.tryPrepare():
221 self.log(6, "prepare ok, waiting for begin")
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 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)
240 self.start_prepare = time.time() + self.backoff
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:
271 self.log(12, "stop recording")
272 if not self.justplay:
273 NavigationInstance.instance.stopRecordService(self.record_service)
274 self.record_service = None
275 if self.afterEvent == AFTEREVENT.STANDBY:
276 if not Screens.Standby.inStandby: # not already in standby
277 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
278 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
279 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
280 if Screens.Standby.inStandby: # in standby
281 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
283 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
286 def sendStandbyNotification(self, answer):
288 Notifications.AddNotification(Screens.Standby.Standby)
290 def sendTryQuitMainloopNotification(self, answer):
292 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
294 def getNextActivation(self):
295 if self.state == self.StateEnded:
298 next_state = self.state + 1
300 return {self.StatePrepared: self.start_prepare,
301 self.StateRunning: self.begin,
302 self.StateEnded: self.end }[next_state]
304 def failureCB(self, answer):
306 self.log(13, "ok, zapped away")
307 #NavigationInstance.instance.stopUserServices()
308 NavigationInstance.instance.playService(self.service_ref.ref)
310 self.log(14, "user didn't want to zap away, record will probably fail")
312 def timeChanged(self):
313 old_prepare = self.start_prepare
314 self.start_prepare = self.begin - self.prepare_time
317 if int(old_prepare) != int(self.start_prepare):
318 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
320 def gotRecordEvent(self, record, event):
321 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
322 if self.__record_service.__deref__() != record.__deref__():
324 self.log(16, "record event %d" % event)
325 if event == iRecordableService.evRecordWriteError:
326 print "WRITE ERROR on recording, disk full?"
327 # show notification. the 'id' will make sure that it will be
328 # displayed only once, even if more timers are failing at the
329 # same time. (which is very likely in case of disk fullness)
330 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
331 # ok, the recording has been stopped. we need to properly note
332 # that in our state, with also keeping the possibility to re-try.
333 # TODO: this has to be done.
334 elif event == iRecordableService.evStart:
335 text = _("A record has been started:\n%s") % self.name
336 if self.dirnameHadToFallback:
337 text = '\n'.join([text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")])
339 # maybe this should be configurable?
340 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
342 # we have record_service as property to automatically subscribe to record service events
343 def setRecordService(self, service):
344 if self.__record_service is not None:
345 print "[remove callback]"
346 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
348 self.__record_service = service
350 if self.__record_service is not None:
351 print "[add callback]"
352 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
354 record_service = property(lambda self: self.__record_service, setRecordService)
356 def createTimer(xml):
357 begin = int(xml.get("begin"))
358 end = int(xml.get("end"))
359 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
360 description = xml.get("description").encode("utf-8")
361 repeated = xml.get("repeated").encode("utf-8")
362 disabled = long(xml.get("disabled") or "0")
363 justplay = long(xml.get("justplay") or "0")
364 afterevent = str(xml.get("afterevent") or "nothing")
366 "nothing": AFTEREVENT.NONE,
367 "standby": AFTEREVENT.STANDBY,
368 "deepstandby": AFTEREVENT.DEEPSTANDBY,
369 "auto": AFTEREVENT.AUTO
372 if eit and eit != "None":
376 location = xml.get("location")
377 if location and location != "None":
378 location = location.encode("utf-8")
381 tags = xml.get("tags")
382 if tags and tags != "None":
383 tags = tags.encode("utf-8").split(' ')
387 name = xml.get("name").encode("utf-8")
388 #filename = xml.get("filename").encode("utf-8")
389 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
390 entry.repeated = int(repeated)
392 for l in xml.findall("log"):
393 time = int(l.get("time"))
394 code = int(l.get("code"))
395 msg = l.text.strip().encode("utf-8")
396 entry.log_entries.append((time, code, msg))
400 class RecordTimer(timer.Timer):
402 timer.Timer.__init__(self)
404 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
409 print "unable to load timers from file!"
411 def isRecording(self):
413 for timer in self.timer_list:
414 if timer.isRunning() and not timer.justplay:
421 doc = xml.etree.cElementTree.parse(self.Filename)
423 from Tools.Notifications import AddPopup
424 from Screens.MessageBox import MessageBox
426 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
428 print "timers.xml failed to load!"
431 os.rename(self.Filename, self.Filename + "_old")
432 except (IOError, OSError):
433 print "renaming broken timer failed"
436 print "timers.xml not found!"
441 # put out a message when at least one timer overlaps
443 for timer in root.findall("timer"):
444 newTimer = createTimer(timer)
445 if (self.record(newTimer, True, True) is not None) and (checkit == True):
446 from Tools.Notifications import AddPopup
447 from Screens.MessageBox import MessageBox
448 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
449 checkit = False # at moment it is enough when the message is displayed one time
452 #root_element = xml.etree.cElementTree.Element('timers')
453 #root_element.text = "\n"
455 #for timer in self.timer_list + self.processed_timers:
456 # some timers (instant records) don't want to be saved.
460 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
461 #t.set("begin", str(int(timer.begin)))
462 #t.set("end", str(int(timer.end)))
463 #t.set("serviceref", str(timer.service_ref))
464 #t.set("repeated", str(timer.repeated))
465 #t.set("name", timer.name)
466 #t.set("description", timer.description)
467 #t.set("afterevent", str({
468 # AFTEREVENT.NONE: "nothing",
469 # AFTEREVENT.STANDBY: "standby",
470 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
471 # AFTEREVENT.AUTO: "auto"}))
472 #if timer.eit is not None:
473 # t.set("eit", str(timer.eit))
474 #if timer.dirname is not None:
475 # t.set("location", str(timer.dirname))
476 #t.set("disabled", str(int(timer.disabled)))
477 #t.set("justplay", str(int(timer.justplay)))
481 #for time, code, msg in timer.log_entries:
482 #l = xml.etree.cElementTree.SubElement(t, 'log')
483 #l.set("time", str(time))
484 #l.set("code", str(code))
488 #doc = xml.etree.cElementTree.ElementTree(root_element)
489 #doc.write(self.Filename)
493 list.append('<?xml version="1.0" ?>\n')
494 list.append('<timers>\n')
496 for timer in self.timer_list + self.processed_timers:
500 list.append('<timer')
501 list.append(' begin="' + str(int(timer.begin)) + '"')
502 list.append(' end="' + str(int(timer.end)) + '"')
503 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
504 list.append(' repeated="' + str(int(timer.repeated)) + '"')
505 list.append(' name="' + str(stringToXML(timer.name)) + '"')
506 list.append(' description="' + str(stringToXML(timer.description)) + '"')
507 list.append(' afterevent="' + str(stringToXML({
508 AFTEREVENT.NONE: "nothing",
509 AFTEREVENT.STANDBY: "standby",
510 AFTEREVENT.DEEPSTANDBY: "deepstandby",
511 AFTEREVENT.AUTO: "auto"
512 }[timer.afterEvent])) + '"')
513 if timer.eit is not None:
514 list.append(' eit="' + str(timer.eit) + '"')
515 if timer.dirname is not None:
516 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
517 if timer.tags is not None:
518 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
519 list.append(' disabled="' + str(int(timer.disabled)) + '"')
520 list.append(' justplay="' + str(int(timer.justplay)) + '"')
523 if config.recording.debug.value:
524 for time, code, msg in timer.log_entries:
526 list.append(' code="' + str(code) + '"')
527 list.append(' time="' + str(time) + '"')
529 list.append(str(stringToXML(msg)))
530 list.append('</log>\n')
532 list.append('</timer>\n')
534 list.append('</timers>\n')
536 file = open(self.Filename, "w")
541 def getNextZapTime(self):
543 for timer in self.timer_list:
544 if not timer.justplay or timer.begin < now:
549 def getNextRecordingTime(self):
551 for timer in self.timer_list:
552 if timer.justplay or timer.begin < now:
557 def isNextRecordAfterEventActionAuto(self):
560 for timer in self.timer_list:
561 if timer.justplay or timer.begin < now:
563 if t is None or t.begin == timer.begin:
565 if t.afterEvent == AFTEREVENT.AUTO:
569 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
570 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
571 if not timersanitycheck.check():
572 if ignoreTSC != True:
573 print "timer conflict detected!"
574 print timersanitycheck.getSimulTimerList()
575 return timersanitycheck.getSimulTimerList()
577 print "ignore timer conflict"
578 elif timersanitycheck.doubleCheck():
579 print "ignore double timer"
581 print "[Timer] Record " + str(entry)
583 self.addTimerEntry(entry)
588 def isInTimer(self, eventid, begin, duration, service):
592 chktimecmp_end = None
593 end = begin + duration
594 for x in self.timer_list:
595 check = x.service_ref.ref.toCompareString() == str(service)
597 sref = x.service_ref.ref
598 parent_sid = sref.getUnsignedData(5)
599 parent_tsid = sref.getUnsignedData(6)
600 if parent_sid and parent_tsid: # check for subservice
601 sid = sref.getUnsignedData(1)
602 tsid = sref.getUnsignedData(2)
603 sref.setUnsignedData(1, parent_sid)
604 sref.setUnsignedData(2, parent_tsid)
605 sref.setUnsignedData(5, 0)
606 sref.setUnsignedData(6, 0)
607 check = x.service_ref.ref.toCompareString() == str(service)
611 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
612 num = event and event.getNumOfLinkageServices() or 0
613 sref.setUnsignedData(1, sid)
614 sref.setUnsignedData(2, tsid)
615 sref.setUnsignedData(5, parent_sid)
616 sref.setUnsignedData(6, parent_tsid)
617 for cnt in range(num):
618 subservice = event.getLinkageService(sref, cnt)
619 if sref.toCompareString() == subservice.toCompareString():
623 #if x.eit is not None and x.repeated == 0:
624 # if x.eit == eventid:
628 chktime = localtime(begin)
629 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
630 chktimecmp_end = chktimecmp + (duration / 60)
631 time = localtime(x.begin)
633 if x.repeated & (2 ** y):
634 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
635 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
636 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
637 elif chktimecmp <= timecmp < chktimecmp_end:
638 time_match = (chktimecmp_end - timecmp) * 60
639 else: #if x.eit is None:
640 if begin <= x.begin <= end:
642 if time_match < diff:
644 elif x.begin <= begin <= x.end:
646 if time_match < diff:
650 def removeEntry(self, entry):
651 print "[Timer] Remove " + str(entry)
654 entry.repeated = False
657 # this sets the end time to current time, so timer will be stopped.
660 if entry.state != entry.StateEnded:
661 self.timeChanged(entry)
663 print "state: ", entry.state
664 print "in processed: ", entry in self.processed_timers
665 print "in running: ", entry in self.timer_list
666 # now the timer should be in the processed_timers list. remove it from there.
667 self.processed_timers.remove(entry)