2 #from time import datetime
3 from Tools import Directories, Notifications
5 from Components.config import config
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 elementsWithTag, mergeText, 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)
51 # please do not translate log messages
52 class RecordTimerEntry(timer.TimerEntry, object):
53 ######### the following static methods and members are only in use when the box is in (soft) standby
54 receiveRecordEvents = False
61 def staticGotRecordEvent(recservice, event):
62 if event == iRecordableService.evEnd:
63 print "RecordTimer.staticGotRecordEvent(iRecordableService.evEnd)"
64 recordings = NavigationInstance.instance.getRecordings()
65 if not len(recordings): # no more recordings exist
66 rec_time = NavigationInstance.instance.RecordTimer.getNextRecordingTime()
67 if rec_time > 0 and (rec_time - time.time()) < 360:
68 print "another recording starts in", rec_time - time.time(), "seconds... do not shutdown yet"
70 print "no starting records in the next 360 seconds... immediate shutdown"
71 RecordTimerEntry.shutdown() # immediate shutdown
72 elif event == iRecordableService.evStart:
73 print "RecordTimer.staticGotRecordEvent(iRecordableService.evStart)"
76 def stopTryQuitMainloop():
77 print "RecordTimer.stopTryQuitMainloop"
78 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
79 RecordTimerEntry.receiveRecordEvents = False
82 def TryQuitMainloop(default_yes = True):
83 if not RecordTimerEntry.receiveRecordEvents:
84 print "RecordTimer.TryQuitMainloop"
85 NavigationInstance.instance.record_event.append(RecordTimerEntry.staticGotRecordEvent)
86 RecordTimerEntry.receiveRecordEvents = True
87 # send fake event.. to check if another recordings are running or
88 # other timers start in a few seconds
89 RecordTimerEntry.staticGotRecordEvent(None, iRecordableService.evEnd)
90 # send normal notification for the case the user leave the standby now..
91 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1, onSessionOpenCallback=RecordTimerEntry.stopTryQuitMainloop, default_yes = default_yes)
92 #################################################################
94 def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.NONE, checkOldTimers = False, dirname = None, tags = None):
95 timer.TimerEntry.__init__(self, int(begin), int(end))
97 if checkOldTimers == True:
98 if self.begin < time.time() - 1209600:
99 self.begin = int(time.time())
101 if self.end < self.begin:
102 self.end = self.begin
104 assert isinstance(serviceref, ServiceReference)
106 self.service_ref = serviceref
108 self.dontSave = False
110 self.description = description
111 self.disabled = disabled
113 self.__record_service = None
114 self.start_prepare = 0
115 self.justplay = justplay
116 self.afterEvent = afterEvent
117 self.dirname = dirname
118 self.dirnameHadToFallback = False
119 self.autoincrease = False
120 self.tags = tags or []
122 self.log_entries = []
125 def log(self, code, msg):
126 self.log_entries.append((int(time.time()), code, msg))
129 def calculateFilename(self):
130 service_name = self.service_ref.getServiceName()
131 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
133 print "begin_date: ", begin_date
134 print "service_name: ", service_name
135 print "name:", self.name
136 print "description: ", self.description
138 filename = begin_date + " - " + service_name
140 filename += " - " + self.name
142 if self.dirname and not Directories.pathExists(self.dirname):
143 self.dirnameHadToFallback = True
144 self.Filename = Directories.getRecordingFilename(filename, None)
146 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
147 self.log(0, "Filename calculated as: '%s'" % self.Filename)
148 #begin_date + " - " + service_name + description)
150 def tryPrepare(self):
154 self.calculateFilename()
155 rec_ref = self.service_ref and self.service_ref.ref
156 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
157 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
159 self.log(1, "'get best playable service for group... record' failed")
162 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
164 if not self.record_service:
165 self.log(1, "'record service' failed")
169 epgcache = eEPGCache.getInstance()
170 queryTime=self.begin+(self.end-self.begin)/2
171 evt = epgcache.lookupEventTime(rec_ref, queryTime)
173 self.description = evt.getShortDescription()
174 event_id = evt.getEventId()
182 prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
184 self.log(2, "'prepare' failed: error %d" % prep_res)
185 NavigationInstance.instance.stopRecordService(self.record_service)
186 self.record_service = None
189 self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
191 f = open(self.Filename + ".ts.meta", "w")
192 f.write(rec_ref.toString() + "\n")
193 f.write(self.name + "\n")
194 f.write(self.description + "\n")
195 f.write(str(self.begin) + "\n")
196 f.write(' '.join(self.tags))
199 self.log(4, "failed to write meta information")
200 NavigationInstance.instance.stopRecordService(self.record_service)
201 self.record_service = None
205 def do_backoff(self):
206 if self.backoff == 0:
210 if self.backoff > 100:
212 self.log(10, "backoff: retry in %d seconds" % self.backoff)
215 next_state = self.state + 1
216 self.log(5, "activating state %d" % next_state)
218 if next_state == self.StatePrepared:
219 if self.tryPrepare():
220 self.log(6, "prepare ok, waiting for begin")
221 # fine. it worked, resources are allocated.
222 self.next_activation = self.begin
226 self.log(7, "prepare failed")
227 if self.first_try_prepare:
228 self.first_try_prepare = False
229 if not config.recording.asktozap.value:
230 self.log(8, "asking user to zap away")
231 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
232 else: # zap without asking
233 self.log(9, "zap without asking")
234 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
239 self.start_prepare = time.time() + self.backoff
241 elif next_state == self.StateRunning:
242 # if this timer has been cancelled, just go to "end" state.
247 if Screens.Standby.inStandby:
248 self.log(11, "wakeup and zap")
249 #set service to zap after standby
250 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
252 Screens.Standby.inStandby.Power()
254 self.log(11, "zapping")
255 NavigationInstance.instance.playService(self.service_ref.ref)
258 self.log(11, "start recording")
259 record_res = self.record_service.start()
262 self.log(13, "start record returned %d" % record_res)
265 self.begin = time.time() + self.backoff
269 elif next_state == self.StateEnded:
270 self.log(12, "stop recording")
271 if not self.justplay:
272 NavigationInstance.instance.stopRecordService(self.record_service)
273 self.record_service = None
274 if self.afterEvent == AFTEREVENT.STANDBY:
275 if not Screens.Standby.inStandby: # not already in standby
276 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
277 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
278 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
279 if Screens.Standby.inStandby: # in standby
280 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
282 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
285 def sendStandbyNotification(self, answer):
287 Notifications.AddNotification(Screens.Standby.Standby)
289 def sendTryQuitMainloopNotification(self, answer):
291 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
293 def getNextActivation(self):
294 if self.state == self.StateEnded:
297 next_state = self.state + 1
299 return {self.StatePrepared: self.start_prepare,
300 self.StateRunning: self.begin,
301 self.StateEnded: self.end }[next_state]
303 def failureCB(self, answer):
305 self.log(13, "ok, zapped away")
306 #NavigationInstance.instance.stopUserServices()
307 NavigationInstance.instance.playService(self.service_ref.ref)
309 self.log(14, "user didn't want to zap away, record will probably fail")
311 def timeChanged(self):
312 old_prepare = self.start_prepare
313 self.start_prepare = self.begin - self.prepare_time
316 if int(old_prepare) != int(self.start_prepare):
317 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
319 def gotRecordEvent(self, record, event):
320 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
321 if self.__record_service.__deref__() != record.__deref__():
323 self.log(16, "record event %d" % event)
324 if event == iRecordableService.evRecordWriteError:
325 print "WRITE ERROR on recording, disk full?"
326 # show notification. the 'id' will make sure that it will be
327 # displayed only once, even if more timers are failing at the
328 # same time. (which is very likely in case of disk fullness)
329 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
330 # ok, the recording has been stopped. we need to properly note
331 # that in our state, with also keeping the possibility to re-try.
332 # TODO: this has to be done.
333 elif event == iRecordableService.evStart:
334 text = _("A record has been started:\n%s") % self.name
335 if self.dirnameHadToFallback:
336 text = '\n'.join([text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")])
338 # maybe this should be configurable?
339 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
341 # we have record_service as property to automatically subscribe to record service events
342 def setRecordService(self, service):
343 if self.__record_service is not None:
344 print "[remove callback]"
345 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
347 self.__record_service = service
349 if self.__record_service is not None:
350 print "[add callback]"
351 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
353 record_service = property(lambda self: self.__record_service, setRecordService)
355 def createTimer(xml):
356 begin = int(xml.getAttribute("begin"))
357 end = int(xml.getAttribute("end"))
358 serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
359 description = xml.getAttribute("description").encode("utf-8")
360 repeated = xml.getAttribute("repeated").encode("utf-8")
361 disabled = long(xml.getAttribute("disabled") or "0")
362 justplay = long(xml.getAttribute("justplay") or "0")
363 afterevent = str(xml.getAttribute("afterevent") or "nothing")
364 afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
365 if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
366 eit = long(xml.getAttribute("eit"))
369 if xml.hasAttribute("location") and xml.getAttribute("location") != "None":
370 location = xml.getAttribute("location").encode("utf-8")
373 if xml.hasAttribute("tags") and xml.getAttribute("tags"):
374 tags = xml.getAttribute("tags").encode("utf-8").split(' ')
378 name = xml.getAttribute("name").encode("utf-8")
379 #filename = xml.getAttribute("filename").encode("utf-8")
380 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location, tags = tags)
381 entry.repeated = int(repeated)
383 for l in elementsWithTag(xml.childNodes, "log"):
384 time = int(l.getAttribute("time"))
385 code = int(l.getAttribute("code"))
386 msg = mergeText(l.childNodes).strip().encode("utf-8")
387 entry.log_entries.append((time, code, msg))
391 class RecordTimer(timer.Timer):
393 timer.Timer.__init__(self)
395 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
400 print "unable to load timers from file!"
402 def isRecording(self):
404 for timer in self.timer_list:
405 if timer.isRunning() and not timer.justplay:
412 doc = xml.dom.minidom.parse(self.Filename)
413 except xml.parsers.expat.ExpatError:
414 from Tools.Notifications import AddPopup
415 from Screens.MessageBox import MessageBox
417 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
419 print "timers.xml failed to load!"
422 os.rename(self.Filename, self.Filename + "_old")
424 print "renaming broken timer failed"
427 root = doc.childNodes[0]
429 # put out a message when at least one timer overlaps
431 for timer in elementsWithTag(root.childNodes, "timer"):
432 newTimer = createTimer(timer)
433 if (self.record(newTimer, True, True) is not None) and (checkit == True):
434 from Tools.Notifications import AddPopup
435 from Screens.MessageBox import MessageBox
436 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
437 checkit = False # at moment it is enough when the message is displayed one time
440 #doc = xml.dom.minidom.Document()
441 #root_element = doc.createElement('timers')
442 #doc.appendChild(root_element)
443 #root_element.appendChild(doc.createTextNode("\n"))
445 #for timer in self.timer_list + self.processed_timers:
446 # some timers (instant records) don't want to be saved.
450 #t = doc.createTextNode("\t")
451 #root_element.appendChild(t)
452 #t = doc.createElement('timer')
453 #t.setAttribute("begin", str(int(timer.begin)))
454 #t.setAttribute("end", str(int(timer.end)))
455 #t.setAttribute("serviceref", str(timer.service_ref))
456 #t.setAttribute("repeated", str(timer.repeated))
457 #t.setAttribute("name", timer.name)
458 #t.setAttribute("description", timer.description)
459 #t.setAttribute("eit", str(timer.eit))
461 #for time, code, msg in timer.log_entries:
462 #t.appendChild(doc.createTextNode("\t\t"))
463 #l = doc.createElement('log')
464 #l.setAttribute("time", str(time))
465 #l.setAttribute("code", str(code))
466 #l.appendChild(doc.createTextNode(msg))
468 #t.appendChild(doc.createTextNode("\n"))
470 #root_element.appendChild(t)
471 #t = doc.createTextNode("\n")
472 #root_element.appendChild(t)
475 #file = open(self.Filename, "w")
482 list.append('<?xml version="1.0" ?>\n')
483 list.append('<timers>\n')
485 for timer in self.timer_list + self.processed_timers:
489 list.append('<timer')
490 list.append(' begin="' + str(int(timer.begin)) + '"')
491 list.append(' end="' + str(int(timer.end)) + '"')
492 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
493 list.append(' repeated="' + str(int(timer.repeated)) + '"')
494 list.append(' name="' + str(stringToXML(timer.name)) + '"')
495 list.append(' description="' + str(stringToXML(timer.description)) + '"')
496 list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
497 if timer.eit is not None:
498 list.append(' eit="' + str(timer.eit) + '"')
499 if timer.dirname is not None:
500 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
501 if timer.tags is not None:
502 list.append(' tags="' + str(stringToXML(' '.join(timer.tags))) + '"')
503 list.append(' disabled="' + str(int(timer.disabled)) + '"')
504 list.append(' justplay="' + str(int(timer.justplay)) + '"')
507 if config.recording.debug.value:
508 for time, code, msg in timer.log_entries:
510 list.append(' code="' + str(code) + '"')
511 list.append(' time="' + str(time) + '"')
513 list.append(str(stringToXML(msg)))
514 list.append('</log>\n')
516 list.append('</timer>\n')
518 list.append('</timers>\n')
520 file = open(self.Filename, "w")
525 def getNextZapTime(self):
527 for timer in self.timer_list:
528 if not timer.justplay or timer.begin < now:
533 def getNextRecordingTime(self):
535 for timer in self.timer_list:
536 if timer.justplay or timer.begin < now:
541 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
542 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
543 if not timersanitycheck.check():
544 if ignoreTSC != True:
545 print "timer conflict detected!"
546 print timersanitycheck.getSimulTimerList()
547 return timersanitycheck.getSimulTimerList()
549 print "ignore timer conflict"
550 elif timersanitycheck.doubleCheck():
551 print "ignore double timer"
553 print "[Timer] Record " + str(entry)
555 self.addTimerEntry(entry)
560 def isInTimer(self, eventid, begin, duration, service):
564 chktimecmp_end = None
565 end = begin + duration
566 for x in self.timer_list:
567 check = x.service_ref.ref.toCompareString() == str(service)
569 sref = x.service_ref.ref
570 parent_sid = sref.getUnsignedData(5)
571 parent_tsid = sref.getUnsignedData(6)
572 if parent_sid and parent_tsid: # check for subservice
573 sid = sref.getUnsignedData(1)
574 tsid = sref.getUnsignedData(2)
575 sref.setUnsignedData(1, parent_sid)
576 sref.setUnsignedData(2, parent_tsid)
577 sref.setUnsignedData(5, 0)
578 sref.setUnsignedData(6, 0)
579 check = x.service_ref.ref.toCompareString() == str(service)
583 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
584 num = event and event.getNumOfLinkageServices() or 0
585 sref.setUnsignedData(1, sid)
586 sref.setUnsignedData(2, tsid)
587 sref.setUnsignedData(5, parent_sid)
588 sref.setUnsignedData(6, parent_tsid)
589 for cnt in range(num):
590 subservice = event.getLinkageService(sref, cnt)
591 if sref.toCompareString() == subservice.toCompareString():
595 #if x.eit is not None and x.repeated == 0:
596 # if x.eit == eventid:
600 chktime = localtime(begin)
601 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
602 chktimecmp_end = chktimecmp + (duration / 60)
603 time = localtime(x.begin)
605 if x.repeated & (2 ** y):
606 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
607 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
608 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
609 elif chktimecmp <= timecmp < chktimecmp_end:
610 time_match = (chktimecmp_end - timecmp) * 60
611 else: #if x.eit is None:
612 if begin <= x.begin <= end:
614 if time_match < diff:
616 elif x.begin <= begin <= x.end:
618 if time_match < diff:
622 def removeEntry(self, entry):
623 print "[Timer] Remove " + str(entry)
626 entry.repeated = False
629 # this sets the end time to current time, so timer will be stopped.
632 if entry.state != entry.StateEnded:
633 self.timeChanged(entry)
635 print "state: ", entry.state
636 print "in processed: ", entry in self.processed_timers
637 print "in running: ", entry in self.timer_list
638 # now the timer should be in the processed_timers list. remove it from there.
639 self.processed_timers.remove(entry)