add tests
[vuplus_dvbapp] / RecordTimer.py
1 import time
2 import codecs
3 #from time import datetime
4 from Tools import Directories, Notifications
5
6 from Components.config import config, ConfigYesNo
7 import timer
8 import xml.dom.minidom
9
10 from enigma import eEPGCache, getBestPlayableServiceReference, \
11         eServiceReference, iRecordableService, quitMainloop
12
13 from Screens.MessageBox import MessageBox
14
15 import NavigationInstance
16
17 import Screens.Standby
18
19 from time import localtime
20
21 from Tools.XMLTools import elementsWithTag, mergeText, stringToXML
22 from ServiceReference import ServiceReference
23
24 # ok, for descriptions etc we have:
25 # service reference  (to get the service name)
26 # name               (title)
27 # description        (description)
28 # event data         (ONLY for time adjustments etc.)
29
30
31 # parses an event, and gives out a (begin, end, name, duration, eit)-tuple.
32 # begin and end will be corrected
33 def parseEvent(ev, description = True):
34         if description:
35                 name = ev.getEventName()
36                 description = ev.getShortDescription()
37         else:
38                 name = ""
39                 description = ""
40         begin = ev.getBeginTime()
41         end = begin + ev.getDuration()
42         eit = ev.getEventId()
43         begin -= config.recording.margin_before.value * 60
44         end += config.recording.margin_after.value * 60
45         return (begin, end, name, description, eit)
46
47 class AFTEREVENT:
48         NONE = 0
49         STANDBY = 1
50         DEEPSTANDBY = 2
51
52 # please do not translate log messages
53 class RecordTimerEntry(timer.TimerEntry):
54 ######### the following static methods and members are only in use when the box is in (soft) standby
55         receiveRecordEvents = False
56
57         @staticmethod
58         def shutdown():
59                 quitMainloop(1)
60
61         @staticmethod
62         def gotRecordEvent(recservice, event):
63                 if event == iRecordableService.evEnd:
64                         print "RecordTimer.gotRecordEvent(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"
70                                 else:
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.gotRecordEvent(iRecordableService.evStart)"
75
76         @staticmethod
77         def stopTryQuitMainloop():
78                 print "RecordTimer.stopTryQuitMainloop"
79                 NavigationInstance.instance.record_event.remove(RecordTimerEntry.gotRecordEvent)
80                 RecordTimerEntry.receiveRecordEvents = False
81
82         @staticmethod
83         def TryQuitMainloop():
84                 if not RecordTimerEntry.receiveRecordEvents:
85                         print "RecordTimer.TryQuitMainloop"
86                         NavigationInstance.instance.record_event.append(RecordTimerEntry.gotRecordEvent)
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.gotRecordEvent(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)
93 #################################################################
94
95         def __init__(self, serviceref, begin, end, name, description, eit, disabled = False, justplay = False, afterEvent = AFTEREVENT.NONE, checkOldTimers = False):
96                 timer.TimerEntry.__init__(self, int(begin), int(end))
97                 
98                 if checkOldTimers == True:
99                         if self.begin < time.time() - 1209600:
100                                 self.begin = int(time.time())
101                 
102                 if self.end < self.begin:
103                         self.end = self.begin
104                 
105                 assert isinstance(serviceref, ServiceReference)
106                 
107                 self.service_ref = serviceref
108                 self.eit = eit
109                 self.dontSave = False
110                 self.name = name
111                 self.description = description
112                 self.disabled = disabled
113                 self.timer = None
114                 self.record_service = None
115                 self.start_prepare = 0
116                 self.justplay = justplay
117                 self.afterEvent = afterEvent
118                 
119                 self.log_entries = []
120                 self.resetState()
121         
122         def log(self, code, msg):
123                 self.log_entries.append((int(time.time()), code, msg))
124                 print "[TIMER]", msg
125         
126         def resetState(self):
127                 self.state = self.StateWaiting
128                 self.cancelled = False
129                 self.first_try_prepare = True
130                 self.timeChanged()
131         
132         def calculateFilename(self):
133                 service_name = self.service_ref.getServiceName()
134                 begin_date = time.strftime("%Y%m%d %H%M", time.localtime(self.begin))
135                 
136                 print "begin_date: ", begin_date
137                 print "service_name: ", service_name
138                 print "name:", self.name
139                 print "description: ", self.description
140                 
141                 filename = begin_date + " - " + service_name
142                 if self.name:
143                         filename += " - " + self.name
144
145                 self.Filename = Directories.getRecordingFilename(filename)
146                 self.log(0, "Filename calculated as: '%s'" % self.Filename)
147                 #begin_date + " - " + service_name + description)
148         
149         def tryPrepare(self):
150                 if self.justplay:
151                         return True
152                 else:
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())
157                                 if not rec_ref:
158                                         self.log(1, "'get best playable service for group... record' failed")
159                                         return False
160                                 
161                         self.record_service = rec_ref and NavigationInstance.instance.recordService(rec_ref)
162                         if not self.record_service:
163                                 self.log(1, "'record service' failed")
164                                 return False
165                                 
166                         event_id = self.eit
167                         if event_id is None:
168                                 event_id = -1
169                                 
170                         prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
171                         if prep_res:
172                                 self.log(2, "'prepare' failed: error %d" % prep_res)
173                                 NavigationInstance.instance.stopRecordService(self.record_service)
174                                 self.record_service = None
175                                 return False
176                                 
177                         if self.repeated:
178                                 epgcache = eEPGCache.getInstance()
179                                 queryTime=self.begin+(self.end-self.begin)/2
180                                 evt = epgcache.lookupEventTime(rec_ref, queryTime)
181                                 if evt:
182                                         self.description = evt.getShortDescription()
183                                 
184                         self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
185                         try:
186                                 f = open(self.Filename + ".ts.meta", "w")
187                                 f.write(rec_ref.toString() + "\n")
188                                 f.write(self.name + "\n")
189                                 f.write(self.description + "\n")
190                                 f.write(str(self.begin) + "\n")
191                                 f.close()
192                         except IOError:
193                                 self.log(4, "failed to write meta information")
194                                 NavigationInstance.instance.stopRecordService(self.record_service)
195                                 self.record_service = None
196                                 return False
197                         return True
198
199         def do_backoff(self):
200                 if self.backoff == 0:
201                         self.backoff = 5
202                 else:
203                         self.backoff *= 2
204                         if self.backoff > 100:
205                                 self.backoff = 100
206                 self.log(10, "backoff: retry in %d seconds" % self.backoff)
207
208         def activate(self):
209                 next_state = self.state + 1
210                 self.log(5, "activating state %d" % next_state)
211                 
212                 if next_state == self.StatePrepared:
213                         if self.tryPrepare():
214                                 self.log(6, "prepare ok, waiting for begin")
215                                 # fine. it worked, resources are allocated.
216                                 self.next_activation = self.begin
217                                 self.backoff = 0
218                                 return True
219                         
220                         self.log(7, "prepare failed")
221                         if self.first_try_prepare:
222                                 self.first_try_prepare = False
223                                 if not config.recording.asktozap.value:
224                                         self.log(8, "asking user to zap away")
225                                         Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
226                                 else: # zap without asking
227                                         self.log(9, "zap without asking")
228                                         Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
229                                         self.failureCB(True)
230
231                         self.do_backoff()
232                         # retry
233                         self.start_prepare = time.time() + self.backoff
234                         return False
235                 elif next_state == self.StateRunning:
236                         # if this timer has been cancelled, just go to "end" state.
237                         if self.cancelled:
238                                 return True
239
240                         if self.justplay:
241                                 if Screens.Standby.inStandby:
242                                         self.log(11, "wakeup and zap")
243                                         #set service to zap after standby
244                                         Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
245                                         #wakeup standby
246                                         Screens.Standby.inStandby.Power()
247                                 else:
248                                         self.log(11, "zapping")
249                                         NavigationInstance.instance.playService(self.service_ref.ref)
250                                 return True
251                         else:
252                                 self.log(11, "start recording")
253                                 record_res = self.record_service.start()
254                                 
255                                 if record_res:
256                                         self.log(13, "start record returned %d" % record_res)
257                                         self.do_backoff()
258                                         # retry
259                                         self.begin = time.time() + self.backoff
260                                         return False
261                                 
262                                 return True
263                 elif next_state == self.StateEnded:
264                         self.log(12, "stop recording")
265                         if not self.justplay:
266                                 NavigationInstance.instance.stopRecordService(self.record_service)
267                                 self.record_service = None
268                         if self.afterEvent == AFTEREVENT.STANDBY:
269                                 if not Screens.Standby.inStandby: # not already in standby
270                                         Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
271                         if self.afterEvent == AFTEREVENT.DEEPSTANDBY:
272                                 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
273                                         if Screens.Standby.inStandby: # not in standby
274                                                 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
275                                         else:
276                                                 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
277                         return True
278
279         def sendStandbyNotification(self, answer):
280                 if answer:
281                         Notifications.AddNotification(Screens.Standby.Standby)
282
283         def sendTryQuitMainloopNotification(self, answer):
284                 if answer:
285                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
286
287         def getNextActivation(self):
288                 if self.state == self.StateEnded:
289                         return self.end
290                 
291                 next_state = self.state + 1
292                 
293                 return {self.StatePrepared: self.start_prepare, 
294                                 self.StateRunning: self.begin, 
295                                 self.StateEnded: self.end }[next_state]
296
297         def failureCB(self, answer):
298                 if answer == True:
299                         self.log(13, "ok, zapped away")
300                         #NavigationInstance.instance.stopUserServices()
301                         NavigationInstance.instance.playService(self.service_ref.ref)
302                 else:
303                         self.log(14, "user didn't want to zap away, record will probably fail")
304
305         def timeChanged(self):
306                 old_prepare = self.start_prepare
307                 self.start_prepare = self.begin - self.prepare_time
308                 self.backoff = 0
309                 
310                 if int(old_prepare) != int(self.start_prepare):
311                         self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
312
313 def createTimer(xml):
314         begin = int(xml.getAttribute("begin"))
315         end = int(xml.getAttribute("end"))
316         serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
317         description = xml.getAttribute("description").encode("utf-8")
318         repeated = xml.getAttribute("repeated").encode("utf-8")
319         disabled = long(xml.getAttribute("disabled") or "0")
320         justplay = long(xml.getAttribute("justplay") or "0")
321         afterevent = str(xml.getAttribute("afterevent") or "nothing")
322         afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
323         if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
324                 eit = long(xml.getAttribute("eit"))
325         else:
326                 eit = None
327         
328         name = xml.getAttribute("name").encode("utf-8")
329         #filename = xml.getAttribute("filename").encode("utf-8")
330         entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent)
331         entry.repeated = int(repeated)
332         
333         for l in elementsWithTag(xml.childNodes, "log"):
334                 time = int(l.getAttribute("time"))
335                 code = int(l.getAttribute("code"))
336                 msg = mergeText(l.childNodes).strip().encode("utf-8")
337                 entry.log_entries.append((time, code, msg))
338         
339         return entry
340
341 class RecordTimer(timer.Timer):
342         def __init__(self):
343                 timer.Timer.__init__(self)
344                 
345                 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
346                 
347                 try:
348                         self.loadTimer()
349                 except IOError:
350                         print "unable to load timers from file!"
351                         
352         def isRecording(self):
353                 isRunning = False
354                 for timer in self.timer_list:
355                         if timer.isRunning() and not timer.justplay:
356                                 isRunning = True
357                 return isRunning
358         
359         def loadTimer(self):
360                 # TODO: PATH!
361                 doc = xml.dom.minidom.parse(self.Filename)
362                 
363                 root = doc.childNodes[0]
364                 for timer in elementsWithTag(root.childNodes, "timer"):
365                         self.record(createTimer(timer))
366
367         def saveTimer(self):
368                 #doc = xml.dom.minidom.Document()
369                 #root_element = doc.createElement('timers')
370                 #doc.appendChild(root_element)
371                 #root_element.appendChild(doc.createTextNode("\n"))
372                 
373                 #for timer in self.timer_list + self.processed_timers:
374                         # some timers (instant records) don't want to be saved.
375                         # skip them
376                         #if timer.dontSave:
377                                 #continue
378                         #t = doc.createTextNode("\t")
379                         #root_element.appendChild(t)
380                         #t = doc.createElement('timer')
381                         #t.setAttribute("begin", str(int(timer.begin)))
382                         #t.setAttribute("end", str(int(timer.end)))
383                         #t.setAttribute("serviceref", str(timer.service_ref))
384                         #t.setAttribute("repeated", str(timer.repeated))                        
385                         #t.setAttribute("name", timer.name)
386                         #t.setAttribute("description", timer.description)
387                         #t.setAttribute("eit", str(timer.eit))
388                         
389                         #for time, code, msg in timer.log_entries:
390                                 #t.appendChild(doc.createTextNode("\t\t"))
391                                 #l = doc.createElement('log')
392                                 #l.setAttribute("time", str(time))
393                                 #l.setAttribute("code", str(code))
394                                 #l.appendChild(doc.createTextNode(msg))
395                                 #t.appendChild(l)
396                                 #t.appendChild(doc.createTextNode("\n"))
397
398                         #root_element.appendChild(t)
399                         #t = doc.createTextNode("\n")
400                         #root_element.appendChild(t)
401
402
403                 #file = open(self.Filename, "w")
404                 #doc.writexml(file)
405                 #file.write("\n")
406                 #file.close()
407
408                 list = []
409
410                 list.append('<?xml version="1.0" ?>\n')
411                 list.append('<timers>\n')
412                 
413                 for timer in self.timer_list + self.processed_timers:
414                         if timer.dontSave:
415                                 continue
416
417                         list.append('<timer')
418                         list.append(' begin="' + str(int(timer.begin)) + '"')
419                         list.append(' end="' + str(int(timer.end)) + '"')
420                         list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
421                         list.append(' repeated="' + str(int(timer.repeated)) + '"')
422                         list.append(' name="' + str(stringToXML(timer.name)) + '"')
423                         list.append(' description="' + str(stringToXML(timer.description)) + '"')
424                         list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
425                         if timer.eit is not None:
426                                 list.append(' eit="' + str(timer.eit) + '"')
427                         list.append(' disabled="' + str(int(timer.disabled)) + '"')
428                         list.append(' justplay="' + str(int(timer.justplay)) + '"')
429                         list.append('>\n')
430                         
431                         if config.recording.debug.value:
432                                 for time, code, msg in timer.log_entries:
433                                         list.append('<log')
434                                         list.append(' code="' + str(code) + '"')
435                                         list.append(' time="' + str(time) + '"')
436                                         list.append('>')
437                                         list.append(str(stringToXML(msg)))
438                                         list.append('</log>\n')
439                         
440                         list.append('</timer>\n')
441
442                 list.append('</timers>\n')
443
444                 file = open(self.Filename, "w")
445                 for x in list:
446                         file.write(x)
447                 file.close()
448
449         def getNextZapTime(self):
450                 llen = len(self.timer_list)
451                 idx = 0
452                 now = time.time()
453                 while idx < llen:
454                         timer = self.timer_list[idx]
455                         if not timer.justplay or timer.begin < now:
456                                 idx += 1
457                         else:
458                                 return timer.begin
459                 return -1
460
461         def getNextRecordingTime(self):
462                 llen = len(self.timer_list)
463                 idx = 0
464                 now = time.time()
465                 while idx < llen:
466                         timer = self.timer_list[idx]
467                         if timer.justplay or timer.begin < now:
468                                 idx += 1
469                         else:
470                                 return timer.begin
471                 return -1
472
473         def record(self, entry):
474                 entry.timeChanged()
475                 print "[Timer] Record " + str(entry)
476                 entry.Timer = self
477                 self.addTimerEntry(entry)
478                 
479         def isInTimer(self, eventid, begin, duration, service):
480                 time_match = 0
481                 chktime = None
482                 chktimecmp = None
483                 chktimecmp_end = None
484                 end = begin + duration
485                 for x in self.timer_list:
486                         if str(x.service_ref) == str(service):
487                                 #if x.eit is not None and x.repeated == 0:
488                                 #       if x.eit == eventid:
489                                 #               return duration
490                                 if x.repeated != 0:
491                                         if chktime is None:
492                                                 chktime = localtime(begin)
493                                                 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
494                                                 chktimecmp_end = chktimecmp + (duration / 60)
495                                         time = localtime(x.begin)
496                                         for y in range(7):
497                                                 if x.repeated & (2 ** y):
498                                                         timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
499                                                         if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
500                                                                 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
501                                                         elif chktimecmp <= timecmp < chktimecmp_end:
502                                                                 time_match = (chktimecmp_end - timecmp) * 60
503                                 else: #if x.eit is None:
504                                         if begin <= x.begin <= end:
505                                                 diff = end - x.begin
506                                                 if time_match < diff:
507                                                         time_match = diff
508                                         elif x.begin <= begin <= x.end:
509                                                 diff = x.end - begin
510                                                 if time_match < diff:
511                                                         time_match = diff
512                 return time_match
513
514         def removeEntry(self, entry):
515                 print "[Timer] Remove " + str(entry)
516                 
517                 # avoid re-enqueuing
518                 entry.repeated = False
519
520                 # abort timer.
521                 # this sets the end time to current time, so timer will be stopped.
522                 entry.abort()
523                 
524                 if entry.state != entry.StateEnded:
525                         self.timeChanged(entry)
526                 
527                 print "state: ", entry.state
528                 print "in processed: ", entry in self.processed_timers
529                 print "in running: ", entry in self.timer_list
530                 # now the timer should be in the processed_timers list. remove it from there.
531                 self.processed_timers.remove(entry)
532
533         def shutdown(self):
534                 self.saveTimer()