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:
580 next_act = timer.getNextActivation()
581 if timer.justplay or next_act < now:
586 def isNextRecordAfterEventActionAuto(self):
589 for timer in self.timer_list:
590 if timer.justplay or timer.begin < now:
592 if t is None or t.begin == timer.begin:
594 if t.afterEvent == AFTEREVENT.AUTO:
598 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
599 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
600 if not timersanitycheck.check():
601 if ignoreTSC != True:
602 print "timer conflict detected!"
603 print timersanitycheck.getSimulTimerList()
604 return timersanitycheck.getSimulTimerList()
606 print "ignore timer conflict"
607 elif timersanitycheck.doubleCheck():
608 print "ignore double timer"
611 print "[Timer] Record " + str(entry)
613 self.addTimerEntry(entry)
618 def isInTimer(self, eventid, begin, duration, service):
622 chktimecmp_end = None
623 end = begin + duration
624 refstr = str(service)
625 for x in self.timer_list:
626 check = x.service_ref.ref.toString() == refstr
628 sref = x.service_ref.ref
629 parent_sid = sref.getUnsignedData(5)
630 parent_tsid = sref.getUnsignedData(6)
631 if parent_sid and parent_tsid: # check for subservice
632 sid = sref.getUnsignedData(1)
633 tsid = sref.getUnsignedData(2)
634 sref.setUnsignedData(1, parent_sid)
635 sref.setUnsignedData(2, parent_tsid)
636 sref.setUnsignedData(5, 0)
637 sref.setUnsignedData(6, 0)
638 check = sref.toCompareString() == refstr
642 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
643 num = event and event.getNumOfLinkageServices() or 0
644 sref.setUnsignedData(1, sid)
645 sref.setUnsignedData(2, tsid)
646 sref.setUnsignedData(5, parent_sid)
647 sref.setUnsignedData(6, parent_tsid)
648 for cnt in range(num):
649 subservice = event.getLinkageService(sref, cnt)
650 if sref.toCompareString() == subservice.toCompareString():
656 chktime = localtime(begin)
657 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
658 chktimecmp_end = chktimecmp + (duration / 60)
659 time = localtime(x.begin)
660 for y in (0, 1, 2, 3, 4, 5, 6):
661 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
662 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
663 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
664 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
665 elif chktimecmp <= timecmp < chktimecmp_end:
666 time_match = (chktimecmp_end - timecmp) * 60
667 else: #if x.eit is None:
668 if begin <= x.begin <= end:
670 if time_match < diff:
672 elif x.begin <= begin <= x.end:
674 if time_match < diff:
680 def removeEntry(self, entry):
681 print "[Timer] Remove " + str(entry)
684 entry.repeated = False
687 # this sets the end time to current time, so timer will be stopped.
688 entry.autoincrease = False
691 if entry.state != entry.StateEnded:
692 self.timeChanged(entry)
694 print "state: ", entry.state
695 print "in processed: ", entry in self.processed_timers
696 print "in running: ", entry in self.timer_list
697 # autoincrease instanttimer if possible
698 if not entry.dontSave:
699 for x in self.timer_list:
700 if x.setAutoincreaseEnd():
702 # now the timer should be in the processed_timers list. remove it from there.
703 self.processed_timers.remove(entry)