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 from bisect import insort
25 # ok, for descriptions etc we have:
26 # service reference (to get the service name)
28 # description (description)
29 # event data (ONLY for time adjustments etc.)
32 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
33 # begin and end will be corrected
34 def parseEvent(ev, description = True):
36 name = ev.getEventName()
37 description = ev.getShortDescription()
41 begin = ev.getBeginTime()
42 end = begin + ev.getDuration()
44 begin -= config.recording.margin_before.value * 60
45 end += config.recording.margin_after.value * 60
46 return (begin, end, name, description, eit)
54 # please do not translate log messages
55 class RecordTimerEntry(timer.TimerEntry, object):
56 ######### the following static methods and members are only in use when the box is in (soft) standby
57 receiveRecordEvents = False
64 def staticGotRecordEvent(recservice, event):
65 if event == iRecordableService.evEnd:
66 print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
67 recordings = NavigationInstance.instance.getRecordings()
68 if not recordings: # no more recordings exist
69 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
70 if rec_time > 0 and (rec_time - time.time()) < 360:
71 print "another recording starts in", rec_time - time.time(), "seconds... do not shutdown yet"
73 print "no starting records in the next 360 seconds... immediate shutdown"
74 RecordTimerEntry.shutdown() # immediate shutdown
75 elif event == iRecordableService.evStart:
76 print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
79 def stopTryQuitMainloop():
80 print "RecordTimer.stopTryQuitMainloop"
81 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
82 RecordTimerEntry.receiveRecordEvents = False
85 def TryQuitMainloop(default_yes = True):
86 if not RecordTimerEntry.receiveRecordEvents:
87 print "RecordTimer.TryQuitMainloop"
88 NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
89 RecordTimerEntry.receiveRecordEvents = True
90 # send fake event.. to check if another recordings are running or
91 # other timers start in a few seconds
92 RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
93 # send normal notification for the case the user leave the standby now..
94 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop, default_yes = default_yes)
95 #################################################################
97 def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.AUTO, checkOldTimers = False, dirname = None, tags = None):
98 timer.TimerEntry.__init__(self, int(begin), int(end))
100 if checkOldTimers == True:
101 if self.begin < time.time() - 1209600:
102 self.begin = int(time.time())
104 if self.end < self.begin:
105 self.end = self.begin
107 assert isinstance(serviceref, ServiceReference)
109 self.service_ref = serviceref
111 self.dontSave = False
113 self.description = description
114 self.disabled = disabled
116 self.__record_service = None
117 self.start_prepare = 0
118 self.justplay = justplay
119 self.afterEvent = afterEvent
120 self.dirname = dirname
121 self.dirnameHadToFallback = False
122 self.autoincrease = False
123 self.autoincreasetime = 3600 * 24 # 1 day
124 self.tags = tags or []
126 self.log_entries = []
129 def log(self, code, msg):
130 self.log_entries.append((int(time.time()), code, msg))
133 def calculateFilename(self):
134 service_name = self.service_ref.getServiceName()
135 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
137 print "begin_date: ", begin_date
138 print "service_name: ", service_name
139 print "name:", self.name
140 print "description: ", self.description
142 filename = begin_date + " - " + service_name
144 filename += " - " + self.name
146 if config.recording.ascii_filenames.value:
147 filename = ASCIItranslit.legacyEncode(filename)
149 if self.dirname and not Directories.fileExists(self.dirname, 'w'):
150 self.dirnameHadToFallback = True
151 self.Filename = Directories.getRecordingFilename(filename, None)
153 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
154 self.log(0, "Filename calculated as: '%s'" % self.Filename)
155 #begin_date + " - " + service_name + description)
157 def tryPrepare(self):
161 self.calculateFilename()
162 rec_ref = self.service_ref and self.service_ref.ref
163 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
164 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
166 self.log(1, "'get best playable service for group... record' failed")
169 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
171 if not self.record_service:
172 self.log(1, "'record service' failed")
176 epgcache = eEPGCache.getInstance()
177 queryTime=self.begin+(self.end-self.begin)/2
178 evt = epgcache.lookupEventTime(rec_ref, queryTime)
180 self.description = evt.getShortDescription()
181 event_id = evt.getEventId()
189 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))
192 self.log(4, "failed to write meta information")
194 self.log(2, "'prepare' failed: error %d" % prep_res)
196 # we must calc nur start time before stopRecordService call because in Screens/Standby.py TryQuitMainloop tries to get
197 # the next start time in evEnd event handler...
199 self.start_prepare = time.time() + self.backoff
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 # create file to "reserve" the filename
223 # because another recording at the same time on another service can try to record the same event
224 # i.e. cable / sat.. then the second recording needs an own extension... when we create the file
225 # here than calculateFilename is happy
226 open(self.Filename + ".ts", "w").close()
227 # fine. it worked, resources are allocated.
228 self.next_activation = self.begin
232 self.log(7, "prepare failed")
233 if self.first_try_prepare:
234 self.first_try_prepare = False
235 cur_ref = NavigationInstance.instance.getCurrentlyPlayingServiceReference()
236 if cur_ref and not cur_ref.getPath():
237 if not config.recording.asktozap.value:
238 self.log(8, "asking user to zap away")
239 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
240 else: # zap without asking
241 self.log(9, "zap without asking")
242 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
245 self.log(8, "currently running service is not a live service.. so stop it makes no sense")
247 self.log(8, "currently no service running... so we dont need to stop it")
249 elif next_state == self.StateRunning:
250 # if this timer has been cancelled, just go to "end" state.
255 if Screens.Standby.inStandby:
256 self.log(11, "wakeup and zap")
257 #set service to zap after standby
258 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
260 Screens.Standby.inStandby.Power()
262 self.log(11, "zapping")
263 NavigationInstance.instance.playService(self.service_ref.ref)
266 self.log(11, "start recording")
267 record_res = self.record_service.start()
270 self.log(13, "start record returned %d" % record_res)
273 self.begin = time.time() + self.backoff
277 elif next_state == self.StateEnded:
279 if self.setAutoincreaseEnd():
280 self.log(12, "autoincrase recording %d minute(s)" % int((self.end - old_end)/60))
283 self.log(12, "stop recording")
284 if not self.justplay:
285 NavigationInstance.instance.stopRecordService(self.record_service)
286 self.record_service = None
287 if self.afterEvent == AFTEREVENT.STANDBY:
288 if not Screens.Standby.inStandby: # not already in standby
289 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
290 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
291 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
292 if Screens.Standby.inStandby: # in standby
293 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
295 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
298 def setAutoincreaseEnd(self, entry = None):
299 if not self.autoincrease:
302 new_end = int(time.time()) + self.autoincreasetime
304 new_end = entry.begin -30
306 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)
307 dummyentry.disabled = self.disabled
308 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, dummyentry)
309 if not timersanitycheck.check():
310 simulTimerList = timersanitycheck.getSimulTimerList()
311 new_end = simulTimerList[1].begin
313 new_end -= 30 # 30 Sekunden Prepare-Zeit lassen
315 if new_end <= time.time():
321 def sendStandbyNotification(self, answer):
323 Notifications.AddNotification(Screens.Standby.Standby)
325 def sendTryQuitMainloopNotification(self, answer):
327 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
329 def getNextActivation(self):
330 if self.state == self.StateEnded:
333 next_state = self.state + 1
335 return {self.StatePrepared: self.start_prepare,
336 self.StateRunning: self.begin,
337 self.StateEnded: self.end }[next_state]
339 def failureCB(self, answer):
341 self.log(13, "ok, zapped away")
342 #NavigationInstance.instance.stopUserServices()
343 NavigationInstance.instance.playService(self.service_ref.ref)
345 self.log(14, "user didn't want to zap away, record will probably fail")
347 def timeChanged(self):
348 old_prepare = self.start_prepare
349 self.start_prepare = self.begin - self.prepare_time
352 if int(old_prepare) != int(self.start_prepare):
353 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
355 def gotRecordEvent(self, record, event):
356 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
357 if self.__record_service.__deref__() != record.__deref__():
359 self.log(16, "record event %d" % event)
360 if event == iRecordableService.evRecordWriteError:
361 print "WRITE ERROR on recording, disk full?"
362 # show notification. the 'id' will make sure that it will be
363 # displayed only once, even if more timers are failing at the
364 # same time. (which is very likely in case of disk fullness)
365 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
366 # ok, the recording has been stopped. we need to properly note
367 # that in our state, with also keeping the possibility to re-try.
368 # TODO: this has to be done.
369 elif event == iRecordableService.evStart:
370 text = _("A record has been started:\n%s") % self.name
371 if self.dirnameHadToFallback:
372 text = '\n'.join((text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")))
374 if config.usage.show_message_when_recording_starts.value:
375 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
377 # we have record_service as property to automatically subscribe to record service events
378 def setRecordService(self, service):
379 if self.__record_service is not None:
380 print "[remove callback]"
381 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
383 self.__record_service = service
385 if self.__record_service is not None:
386 print "[add callback]"
387 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
389 record_service = property(lambda self: self.__record_service, setRecordService)
391 def createTimer(xml):
392 begin = int(xml.get("begin"))
393 end = int(xml.get("end"))
394 serviceref = ServiceReference(xml.get("serviceref").encode("utf-8"))
395 description = xml.get("description").encode("utf-8")
396 repeated = xml.get("repeated").encode("utf-8")
397 disabled = long(xml.get("disabled") or "0")
398 justplay = long(xml.get("justplay") or "0")
399 afterevent = str(xml.get("afterevent") or "nothing")
401 "nothing": AFTEREVENT.NONE,
402 "standby": AFTEREVENT.STANDBY,
403 "deepstandby": AFTEREVENT.DEEPSTANDBY,
404 "auto": AFTEREVENT.AUTO
407 if eit and eit != "None":
411 location = xml.get("location")
412 if location and location != "None":
413 location = location.encode("utf-8")
416 tags = xml.get("tags")
417 if tags and tags != "None":
418 tags = tags.encode("utf-8").split(' ')
422 name = xml.get("name").encode("utf-8")
423 #filename = xml.get("filename").encode("utf-8")
424 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
425 entry.repeated = int(repeated)
427 for l in xml.findall("log"):
428 time = int(l.get("time"))
429 code = int(l.get("code"))
430 msg = l.text.strip().encode("utf-8")
431 entry.log_entries.append((time, code, msg))
435 class RecordTimer(timer.Timer):
437 timer.Timer.__init__(self)
439 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
444 print "unable to load timers from file!"
446 def doActivate(self, w):
447 # when activating a timer which has already passed,
448 # simply abort the timer. don't run trough all the stages.
450 w.state = RecordTimerEntry.StateEnded
452 # when active returns true, this means "accepted".
453 # otherwise, the current state is kept.
454 # the timer entry itself will fix up the delay then.
458 self.timer_list.remove(w)
460 # did this timer reached the last state?
461 if w.state < RecordTimerEntry.StateEnded:
462 # no, sort it into active list
463 insort(self.timer_list, w)
465 # yes. Process repeated, and re-add.
468 w.state = RecordTimerEntry.StateWaiting
469 self.addTimerEntry(w)
471 insort(self.processed_timers, w)
475 def isRecording(self):
477 for timer in self.timer_list:
478 if timer.isRunning() and not timer.justplay:
485 doc = xml.etree.cElementTree.parse(self.Filename)
487 from Tools.Notifications import AddPopup
488 from Screens.MessageBox import MessageBox
490 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
492 print "timers.xml failed to load!"
495 os.rename(self.Filename, self.Filename + "_old")
496 except (IOError, OSError):
497 print "renaming broken timer failed"
500 print "timers.xml not found!"
505 # put out a message when at least one timer overlaps
507 for timer in root.findall("timer"):
508 newTimer = createTimer(timer)
509 if (self.record(newTimer, True, True) is not None) and (checkit == True):
510 from Tools.Notifications import AddPopup
511 from Screens.MessageBox import MessageBox
512 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
513 checkit = False # at moment it is enough when the message is displayed one time
516 #root_element = xml.etree.cElementTree.Element('timers')
517 #root_element.text = "\n"
519 #for timer in self.timer_list + self.processed_timers:
520 # some timers (instant records) don't want to be saved.
524 #t = xml.etree.cElementTree.SubElement(root_element, 'timers')
525 #t.set("begin", str(int(timer.begin)))
526 #t.set("end", str(int(timer.end)))
527 #t.set("serviceref", str(timer.service_ref))
528 #t.set("repeated", str(timer.repeated))
529 #t.set("name", timer.name)
530 #t.set("description", timer.description)
531 #t.set("afterevent", str({
532 # AFTEREVENT.NONE: "nothing",
533 # AFTEREVENT.STANDBY: "standby",
534 # AFTEREVENT.DEEPSTANDBY: "deepstandby",
535 # AFTEREVENT.AUTO: "auto"}))
536 #if timer.eit is not None:
537 # t.set("eit", str(timer.eit))
538 #if timer.dirname is not None:
539 # t.set("location", str(timer.dirname))
540 #t.set("disabled", str(int(timer.disabled)))
541 #t.set("justplay", str(int(timer.justplay)))
545 #for time, code, msg in timer.log_entries:
546 #l = xml.etree.cElementTree.SubElement(t, 'log')
547 #l.set("time", str(time))
548 #l.set("code", str(code))
552 #doc = xml.etree.cElementTree.ElementTree(root_element)
553 #doc.write(self.Filename)
557 list.append('<?xml version="1.0" ?>\n')
558 list.append('<timers>\n')
560 for timer in self.timer_list + self.processed_timers:
564 list.append('<timer')
565 list.append(' begin="' + str(int(timer.begin)) + '"')
566 list.append(' end="' + str(int(timer.end)) + '"')
567 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
568 list.append(' repeated="' + str(int(timer.repeated)) + '"')
569 list.append(' name="' + str(stringToXML(timer.name)) + '"')
570 list.append(' description="' + str(stringToXML(timer.description)) + '"')
571 list.append(' afterevent="' + str(stringToXML({
572 AFTEREVENT.NONE: "nothing",
573 AFTEREVENT.STANDBY: "standby",
574 AFTEREVENT.DEEPSTANDBY: "deepstandby",
575 AFTEREVENT.AUTO: "auto"
576 }[timer.afterEvent])) + '"')
577 if timer.eit is not None:
578 list.append(' eit="' + str(timer.eit) + '"')
579 if timer.dirname is not None:
580 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
581 if timer.tags is not None:
582 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
583 list.append(' disabled="' + str(int(timer.disabled)) + '"')
584 list.append(' justplay="' + str(int(timer.justplay)) + '"')
587 if config.recording.debug.value:
588 for time, code, msg in timer.log_entries:
590 list.append(' code="' + str(code) + '"')
591 list.append(' time="' + str(time) + '"')
593 list.append(str(stringToXML(msg)))
594 list.append('</log>\n')
596 list.append('</timer>\n')
598 list.append('</timers>\n')
600 file = open(self.Filename, "w")
605 def getNextZapTime(self):
607 for timer in self.timer_list:
608 if not timer.justplay or timer.begin < now:
613 def getNextRecordingTime(self):
615 for timer in self.timer_list:
616 next_act = timer.getNextActivation()
617 if timer.justplay or next_act < now:
622 def isNextRecordAfterEventActionAuto(self):
625 for timer in self.timer_list:
626 if timer.justplay or timer.begin < now:
628 if t is None or t.begin == timer.begin:
630 if t.afterEvent == AFTEREVENT.AUTO:
634 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
635 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
636 if not timersanitycheck.check():
637 if ignoreTSC != True:
638 print "timer conflict detected!"
639 print timersanitycheck.getSimulTimerList()
640 return timersanitycheck.getSimulTimerList()
642 print "ignore timer conflict"
643 elif timersanitycheck.doubleCheck():
644 print "ignore double timer"
647 print "[Timer] Record " + str(entry)
649 self.addTimerEntry(entry)
654 def isInTimer(self, eventid, begin, duration, service):
658 chktimecmp_end = None
659 end = begin + duration
660 refstr = str(service)
661 for x in self.timer_list:
662 check = x.service_ref.ref.toString() == refstr
664 sref = x.service_ref.ref
665 parent_sid = sref.getUnsignedData(5)
666 parent_tsid = sref.getUnsignedData(6)
667 if parent_sid and parent_tsid: # check for subservice
668 sid = sref.getUnsignedData(1)
669 tsid = sref.getUnsignedData(2)
670 sref.setUnsignedData(1, parent_sid)
671 sref.setUnsignedData(2, parent_tsid)
672 sref.setUnsignedData(5, 0)
673 sref.setUnsignedData(6, 0)
674 check = sref.toCompareString() == refstr
678 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
679 num = event and event.getNumOfLinkageServices() or 0
680 sref.setUnsignedData(1, sid)
681 sref.setUnsignedData(2, tsid)
682 sref.setUnsignedData(5, parent_sid)
683 sref.setUnsignedData(6, parent_tsid)
684 for cnt in range(num):
685 subservice = event.getLinkageService(sref, cnt)
686 if sref.toCompareString() == subservice.toCompareString():
692 chktime = localtime(begin)
693 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
694 chktimecmp_end = chktimecmp + (duration / 60)
695 time = localtime(x.begin)
696 for y in (0, 1, 2, 3, 4, 5, 6):
697 if x.repeated & (2 ** y) and (x.begin <= begin or begin <= x.begin <= end):
698 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
699 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
700 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
701 elif chktimecmp <= timecmp < chktimecmp_end:
702 time_match = (chktimecmp_end - timecmp) * 60
703 else: #if x.eit is None:
704 if begin <= x.begin <= end:
706 if time_match < diff:
708 elif x.begin <= begin <= x.end:
710 if time_match < diff:
716 def removeEntry(self, entry):
717 print "[Timer] Remove " + str(entry)
720 entry.repeated = False
723 # this sets the end time to current time, so timer will be stopped.
724 entry.autoincrease = False
727 if entry.state != entry.StateEnded:
728 self.timeChanged(entry)
730 print "state: ", entry.state
731 print "in processed: ", entry in self.processed_timers
732 print "in running: ", entry in self.timer_list
733 # autoincrease instanttimer if possible
734 if not entry.dontSave:
735 for x in self.timer_list:
736 if x.setAutoincreaseEnd():
738 # now the timer should be in the processed_timers list. remove it from there.
739 self.processed_timers.remove(entry)