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):
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
121 self.log_entries = []
124 def log(self, code, msg):
125 self.log_entries.append((int(time.time()), code, msg))
128 def calculateFilename(self):
129 service_name = self.service_ref.getServiceName()
130 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
132 print "begin_date: ", begin_date
133 print "service_name: ", service_name
134 print "name:", self.name
135 print "description: ", self.description
137 filename = begin_date + " - " + service_name
139 filename += " - " + self.name
141 if self.dirname and not Directories.pathExists(self.dirname):
142 self.dirnameHadToFallback = True
143 self.Filename = Directories.getRecordingFilename(filename, None)
145 self.Filename = Directories.getRecordingFilename(filename, self.dirname)
146 self.log(0, "Filename calculated as: '%s'" % self.Filename)
147 #begin_date + " - " + service_name + description)
149 def tryPrepare(self):
153 self.calculateFilename()
154 rec_ref = self.service_ref and self.service_ref.ref
155 if rec_ref and rec_ref.flags & eServiceReference.isGroup:
156 rec_ref = getBestPlayableServiceReference(rec_ref, eServiceReference())
158 self.log(1, "'get best playable service for group... record' failed")
161 self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
163 if not self.record_service:
164 self.log(1, "'record service' failed")
168 epgcache = eEPGCache.getInstance()
169 queryTime=self.begin+(self.end-self.begin)/2
170 evt = epgcache.lookupEventTime(rec_ref, queryTime)
172 self.description = evt.getShortDescription()
173 event_id = evt.getEventId()
181 prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
183 self.log(2, "'prepare' failed: error %d" % prep_res)
184 NavigationInstance.instance.stopRecordService(self.record_service)
185 self.record_service = None
188 self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
190 f = open(self.Filename + ".ts.meta", "w")
191 f.write(rec_ref.toString() + "\n")
192 f.write(self.name + "\n")
193 f.write(self.description + "\n")
194 f.write(str(self.begin) + "\n")
197 self.log(4, "failed to write meta information")
198 NavigationInstance.instance.stopRecordService(self.record_service)
199 self.record_service = None
203 def do_backoff(self):
204 if self.backoff == 0:
208 if self.backoff > 100:
210 self.log(10, "backoff: retry in %d seconds" % self.backoff)
213 next_state = self.state + 1
214 self.log(5, "activating state %d" % next_state)
216 if next_state == self.StatePrepared:
217 if self.tryPrepare():
218 self.log(6, "prepare ok, waiting for begin")
219 # fine. it worked, resources are allocated.
220 self.next_activation = self.begin
224 self.log(7, "prepare failed")
225 if self.first_try_prepare:
226 self.first_try_prepare = False
227 if not config.recording.asktozap.value:
228 self.log(8, "asking user to zap away")
229 Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
230 else: # zap without asking
231 self.log(9, "zap without asking")
232 Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
237 self.start_prepare = time.time() + self.backoff
239 elif next_state == self.StateRunning:
240 # if this timer has been cancelled, just go to "end" state.
245 if Screens.Standby.inStandby:
246 self.log(11, "wakeup and zap")
247 #set service to zap after standby
248 Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
250 Screens.Standby.inStandby.Power()
252 self.log(11, "zapping")
253 NavigationInstance.instance.playService(self.service_ref.ref)
256 self.log(11, "start recording")
257 record_res = self.record_service.start()
260 self.log(13, "start record returned %d" % record_res)
263 self.begin = time.time() + self.backoff
267 elif next_state == self.StateEnded:
268 self.log(12, "stop recording")
269 if not self.justplay:
270 NavigationInstance.instance.stopRecordService(self.record_service)
271 self.record_service = None
272 if self.afterEvent == AFTEREVENT.STANDBY:
273 if not Screens.Standby.inStandby: # not already in standby
274 Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
275 elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
276 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
277 if Screens.Standby.inStandby: # in standby
278 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
280 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
283 def sendStandbyNotification(self, answer):
285 Notifications.AddNotification(Screens.Standby.Standby)
287 def sendTryQuitMainloopNotification(self, answer):
289 Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
291 def getNextActivation(self):
292 if self.state == self.StateEnded:
295 next_state = self.state + 1
297 return {self.StatePrepared: self.start_prepare,
298 self.StateRunning: self.begin,
299 self.StateEnded: self.end }[next_state]
301 def failureCB(self, answer):
303 self.log(13, "ok, zapped away")
304 #NavigationInstance.instance.stopUserServices()
305 NavigationInstance.instance.playService(self.service_ref.ref)
307 self.log(14, "user didn't want to zap away, record will probably fail")
309 def timeChanged(self):
310 old_prepare = self.start_prepare
311 self.start_prepare = self.begin - self.prepare_time
314 if int(old_prepare) != int(self.start_prepare):
315 self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
317 def gotRecordEvent(self, record, event):
318 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
319 if self.__record_service.__deref__() != record.__deref__():
321 self.log(16, "record event %d" % event)
322 if event == iRecordableService.evRecordWriteError:
323 print "WRITE ERROR on recording, disk full?"
324 # show notification. the 'id' will make sure that it will be
325 # displayed only once, even if more timers are failing at the
326 # same time. (which is very likely in case of disk fullness)
327 Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
328 # ok, the recording has been stopped. we need to properly note
329 # that in our state, with also keeping the possibility to re-try.
330 # TODO: this has to be done.
331 elif event == iRecordableService.evStart:
332 text = _("A record has been started:\n%s") % self.name
333 if self.dirnameHadToFallback:
334 text = '\n'.join([text, _("Please note that the previously selected media could not be accessed and therefore the default directory is being used instead.")])
336 # maybe this should be configurable?
337 Notifications.AddPopup(text = text, type = MessageBox.TYPE_INFO, timeout = 3)
339 # we have record_service as property to automatically subscribe to record service events
340 def setRecordService(self, service):
341 if self.__record_service is not None:
342 print "[remove callback]"
343 NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
345 self.__record_service = service
347 if self.__record_service is not None:
348 print "[add callback]"
349 NavigationInstance.instance.record_event.append(self.gotRecordEvent)
351 record_service = property(lambda self: self.__record_service, setRecordService)
353 def createTimer(xml):
354 begin = int(xml.getAttribute("begin"))
355 end = int(xml.getAttribute("end"))
356 serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
357 description = xml.getAttribute("description").encode("utf-8")
358 repeated = xml.getAttribute("repeated").encode("utf-8")
359 disabled = long(xml.getAttribute("disabled") or "0")
360 justplay = long(xml.getAttribute("justplay") or "0")
361 afterevent = str(xml.getAttribute("afterevent") or "nothing")
362 afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
363 if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
364 eit = long(xml.getAttribute("eit"))
367 if xml.hasAttribute("location") and xml.getAttribute("location") != "None":
368 location = xml.getAttribute("location").encode("utf-8")
372 name = xml.getAttribute("name").encode("utf-8")
373 #filename = xml.getAttribute("filename").encode("utf-8")
374 entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent, dirname = location)
375 entry.repeated = int(repeated)
377 for l in elementsWithTag(xml.childNodes, "log"):
378 time = int(l.getAttribute("time"))
379 code = int(l.getAttribute("code"))
380 msg = mergeText(l.childNodes).strip().encode("utf-8")
381 entry.log_entries.append((time, code, msg))
385 class RecordTimer(timer.Timer):
387 timer.Timer.__init__(self)
389 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
394 print "unable to load timers from file!"
396 def isRecording(self):
398 for timer in self.timer_list:
399 if timer.isRunning() and not timer.justplay:
406 doc = xml.dom.minidom.parse(self.Filename)
407 except xml.parsers.expat.ExpatError:
408 from Tools.Notifications import AddPopup
409 from Screens.MessageBox import MessageBox
411 AddPopup(_("The timer file (timers.xml) is corrupt and could not be loaded."), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
413 print "timers.xml failed to load!"
416 os.rename(self.Filename, self.Filename + "_old")
418 print "renaming broken timer failed"
421 root = doc.childNodes[0]
423 # put out a message when at least one timer overlaps
425 for timer in elementsWithTag(root.childNodes, "timer"):
426 newTimer = createTimer(timer)
427 if (self.record(newTimer, True, True) is not None) and (checkit == True):
428 from Tools.Notifications import AddPopup
429 from Screens.MessageBox import MessageBox
430 AddPopup(_("Timer overlap in timers.xml detected!\nPlease recheck it!"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "TimerLoadFailed")
431 checkit = False # at moment it is enough when the message is displayed one time
434 #doc = xml.dom.minidom.Document()
435 #root_element = doc.createElement('timers')
436 #doc.appendChild(root_element)
437 #root_element.appendChild(doc.createTextNode("\n"))
439 #for timer in self.timer_list + self.processed_timers:
440 # some timers (instant records) don't want to be saved.
444 #t = doc.createTextNode("\t")
445 #root_element.appendChild(t)
446 #t = doc.createElement('timer')
447 #t.setAttribute("begin", str(int(timer.begin)))
448 #t.setAttribute("end", str(int(timer.end)))
449 #t.setAttribute("serviceref", str(timer.service_ref))
450 #t.setAttribute("repeated", str(timer.repeated))
451 #t.setAttribute("name", timer.name)
452 #t.setAttribute("description", timer.description)
453 #t.setAttribute("eit", str(timer.eit))
455 #for time, code, msg in timer.log_entries:
456 #t.appendChild(doc.createTextNode("\t\t"))
457 #l = doc.createElement('log')
458 #l.setAttribute("time", str(time))
459 #l.setAttribute("code", str(code))
460 #l.appendChild(doc.createTextNode(msg))
462 #t.appendChild(doc.createTextNode("\n"))
464 #root_element.appendChild(t)
465 #t = doc.createTextNode("\n")
466 #root_element.appendChild(t)
469 #file = open(self.Filename, "w")
476 list.append('<?xml version="1.0" ?>\n')
477 list.append('<timers>\n')
479 for timer in self.timer_list + self.processed_timers:
483 list.append('<timer')
484 list.append(' begin="' + str(int(timer.begin)) + '"')
485 list.append(' end="' + str(int(timer.end)) + '"')
486 list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
487 list.append(' repeated="' + str(int(timer.repeated)) + '"')
488 list.append(' name="' + str(stringToXML(timer.name)) + '"')
489 list.append(' description="' + str(stringToXML(timer.description)) + '"')
490 list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
491 if timer.eit is not None:
492 list.append(' eit="' + str(timer.eit) + '"')
493 if timer.dirname is not None:
494 list.append(' location="' + str(stringToXML(timer.dirname)) + '"')
495 list.append(' disabled="' + str(int(timer.disabled)) + '"')
496 list.append(' justplay="' + str(int(timer.justplay)) + '"')
499 if config.recording.debug.value:
500 for time, code, msg in timer.log_entries:
502 list.append(' code="' + str(code) + '"')
503 list.append(' time="' + str(time) + '"')
505 list.append(str(stringToXML(msg)))
506 list.append('</log>\n')
508 list.append('</timer>\n')
510 list.append('</timers>\n')
512 file = open(self.Filename, "w")
517 def getNextZapTime(self):
519 for timer in self.timer_list:
520 if not timer.justplay or timer.begin < now:
525 def getNextRecordingTime(self):
527 for timer in self.timer_list:
528 if timer.justplay or timer.begin < now:
533 def record(self, entry, ignoreTSC=False, dosave=True): #wird von loadTimer mit dosave=False aufgerufen
534 timersanitycheck = TimerSanityCheck(self.timer_list,entry)
535 if not timersanitycheck.check():
536 if ignoreTSC != True:
537 print "timer conflict detected!"
538 print timersanitycheck.getSimulTimerList()
539 return timersanitycheck.getSimulTimerList()
541 print "ignore timer conflict"
542 elif timersanitycheck.doubleCheck():
543 print "ignore double timer"
545 print "[Timer] Record " + str(entry)
547 self.addTimerEntry(entry)
552 def isInTimer(self, eventid, begin, duration, service):
556 chktimecmp_end = None
557 end = begin + duration
558 for x in self.timer_list:
559 check = x.service_ref.ref.toCompareString() == str(service)
561 sref = x.service_ref.ref
562 parent_sid = sref.getUnsignedData(5)
563 parent_tsid = sref.getUnsignedData(6)
564 if parent_sid and parent_tsid: # check for subservice
565 sid = sref.getUnsignedData(1)
566 tsid = sref.getUnsignedData(2)
567 sref.setUnsignedData(1, parent_sid)
568 sref.setUnsignedData(2, parent_tsid)
569 sref.setUnsignedData(5, 0)
570 sref.setUnsignedData(6, 0)
571 check = x.service_ref.ref.toCompareString() == str(service)
575 event = eEPGCache.getInstance().lookupEventId(sref, eventid)
576 num = event and event.getNumOfLinkageServices() or 0
577 sref.setUnsignedData(1, sid)
578 sref.setUnsignedData(2, tsid)
579 sref.setUnsignedData(5, parent_sid)
580 sref.setUnsignedData(6, parent_tsid)
581 for cnt in range(num):
582 subservice = event.getLinkageService(sref, cnt)
583 if sref.toCompareString() == subservice.toCompareString():
587 #if x.eit is not None and x.repeated == 0:
588 # if x.eit == eventid:
592 chktime = localtime(begin)
593 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
594 chktimecmp_end = chktimecmp + (duration / 60)
595 time = localtime(x.begin)
597 if x.repeated & (2 ** y):
598 timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
599 if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
600 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
601 elif chktimecmp <= timecmp < chktimecmp_end:
602 time_match = (chktimecmp_end - timecmp) * 60
603 else: #if x.eit is None:
604 if begin <= x.begin <= end:
606 if time_match < diff:
608 elif x.begin <= begin <= x.end:
610 if time_match < diff:
614 def removeEntry(self, entry):
615 print "[Timer] Remove " + str(entry)
618 entry.repeated = False
621 # this sets the end time to current time, so timer will be stopped.
624 if entry.state != entry.StateEnded:
625 self.timeChanged(entry)
627 print "state: ", entry.state
628 print "in processed: ", entry in self.processed_timers
629 print "in running: ", entry in self.timer_list
630 # now the timer should be in the processed_timers list. remove it from there.
631 self.processed_timers.remove(entry)