whitespace fixup
[enigma2.git] / 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, object):
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 staticGotRecordEvent(recservice, event):
63                 if event == iRecordableService.evEnd:
64                         print "RecordTimer.staticGotRecordEvent(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.staticGotRecordEvent(iRecordableService.evStart)"
75
76         @staticmethod
77         def stopTryQuitMainloop():
78                 print "RecordTimer.stopTryQuitMainloop"
79                 NavigationInstance.instance.record_event.remove(RecordTimerEntry.staticGotRecordEvent)
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.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)
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
163                         if not self.record_service:
164                                 self.log(1, "'record service' failed")
165                                 return False
166
167                         event_id = self.eit
168                         if event_id is None:
169                                 event_id = -1
170                                 
171                         prep_res=self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id)
172                         if prep_res:
173                                 self.log(2, "'prepare' failed: error %d" % prep_res)
174                                 NavigationInstance.instance.stopRecordService(self.record_service)
175                                 self.record_service = None
176                                 return False
177                                 
178                         if self.repeated:
179                                 epgcache = eEPGCache.getInstance()
180                                 queryTime=self.begin+(self.end-self.begin)/2
181                                 evt = epgcache.lookupEventTime(rec_ref, queryTime)
182                                 if evt:
183                                         self.description = evt.getShortDescription()
184
185                         self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
186                         try:
187                                 f = open(self.Filename + ".ts.meta", "w")
188                                 f.write(rec_ref.toString() + "\n")
189                                 f.write(self.name + "\n")
190                                 f.write(self.description + "\n")
191                                 f.write(str(self.begin) + "\n")
192                                 f.close()
193                         except IOError:
194                                 self.log(4, "failed to write meta information")
195                                 NavigationInstance.instance.stopRecordService(self.record_service)
196                                 self.record_service = None
197                                 return False
198                         return True
199
200         def do_backoff(self):
201                 if self.backoff == 0:
202                         self.backoff = 5
203                 else:
204                         self.backoff *= 2
205                         if self.backoff > 100:
206                                 self.backoff = 100
207                 self.log(10, "backoff: retry in %d seconds" % self.backoff)
208
209         def activate(self):
210                 next_state = self.state + 1
211                 self.log(5, "activating state %d" % next_state)
212                 
213                 if next_state == self.StatePrepared:
214                         if self.tryPrepare():
215                                 self.log(6, "prepare ok, waiting for begin")
216                                 # fine. it worked, resources are allocated.
217                                 self.next_activation = self.begin
218                                 self.backoff = 0
219                                 return True
220                         
221                         self.log(7, "prepare failed")
222                         if self.first_try_prepare:
223                                 self.first_try_prepare = False
224                                 if not config.recording.asktozap.value:
225                                         self.log(8, "asking user to zap away")
226                                         Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
227                                 else: # zap without asking
228                                         self.log(9, "zap without asking")
229                                         Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
230                                         self.failureCB(True)
231
232                         self.do_backoff()
233                         # retry
234                         self.start_prepare = time.time() + self.backoff
235                         return False
236                 elif next_state == self.StateRunning:
237                         # if this timer has been cancelled, just go to "end" state.
238                         if self.cancelled:
239                                 return True
240
241                         if self.justplay:
242                                 if Screens.Standby.inStandby:
243                                         self.log(11, "wakeup and zap")
244                                         #set service to zap after standby
245                                         Screens.Standby.inStandby.prev_running_service = self.service_ref.ref
246                                         #wakeup standby
247                                         Screens.Standby.inStandby.Power()
248                                 else:
249                                         self.log(11, "zapping")
250                                         NavigationInstance.instance.playService(self.service_ref.ref)
251                                 return True
252                         else:
253                                 self.log(11, "start recording")
254                                 record_res = self.record_service.start()
255                                 
256                                 if record_res:
257                                         self.log(13, "start record returned %d" % record_res)
258                                         self.do_backoff()
259                                         # retry
260                                         self.begin = time.time() + self.backoff
261                                         return False
262
263                                 return True
264                 elif next_state == self.StateEnded:
265                         self.log(12, "stop recording")
266                         if not self.justplay:
267                                 NavigationInstance.instance.stopRecordService(self.record_service)
268                                 self.record_service = None
269                         if self.afterEvent == AFTEREVENT.STANDBY:
270                                 if not Screens.Standby.inStandby: # not already in standby
271                                         Notifications.AddNotificationWithCallback(self.sendStandbyNotification, MessageBox, _("A finished record timer wants to set your\nDreambox to standby. Do that now?"), timeout = 20)
272                         if self.afterEvent == AFTEREVENT.DEEPSTANDBY:
273                                 if not Screens.Standby.inTryQuitMainloop: # not a shutdown messagebox is open
274                                         if Screens.Standby.inStandby: # not in standby
275                                                 RecordTimerEntry.TryQuitMainloop() # start shutdown handling without screen
276                                         else:
277                                                 Notifications.AddNotificationWithCallback(self.sendTryQuitMainloopNotification, MessageBox, _("A finished record timer wants to shut down\nyour Dreambox. Shutdown now?"), timeout = 20)
278                         return True
279
280         def sendStandbyNotification(self, answer):
281                 if answer:
282                         Notifications.AddNotification(Screens.Standby.Standby)
283
284         def sendTryQuitMainloopNotification(self, answer):
285                 if answer:
286                         Notifications.AddNotification(Screens.Standby.TryQuitMainloop, 1)
287
288         def getNextActivation(self):
289                 if self.state == self.StateEnded:
290                         return self.end
291                 
292                 next_state = self.state + 1
293                 
294                 return {self.StatePrepared: self.start_prepare, 
295                                 self.StateRunning: self.begin, 
296                                 self.StateEnded: self.end }[next_state]
297
298         def failureCB(self, answer):
299                 if answer == True:
300                         self.log(13, "ok, zapped away")
301                         #NavigationInstance.instance.stopUserServices()
302                         NavigationInstance.instance.playService(self.service_ref.ref)
303                 else:
304                         self.log(14, "user didn't want to zap away, record will probably fail")
305
306         def timeChanged(self):
307                 old_prepare = self.start_prepare
308                 self.start_prepare = self.begin - self.prepare_time
309                 self.backoff = 0
310                 
311                 if int(old_prepare) != int(self.start_prepare):
312                         self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
313
314         def gotRecordEvent(self, record, event):
315                 # TODO: this is not working (never true), please fix. (comparing two swig wrapped ePtrs)
316                 if self.__record_service.__deref__() != record.__deref__():
317                         return
318                 self.log(16, "record event %d" % event)
319                 if event == iRecordableService.evRecordWriteError:
320                         print "WRITE ERROR on recording, disk full?"
321                         # show notification. the 'id' will make sure that it will be
322                         # displayed only once, even if more timers are failing at the
323                         # same time. (which is very likely in case of disk fullness)
324                         Notifications.AddPopup(text = _("Write error while recording. Disk full?\n"), type = MessageBox.TYPE_ERROR, timeout = 0, id = "DiskFullMessage")
325                         # ok, the recording has been stopped. we need to properly note 
326                         # that in our state, with also keeping the possibility to re-try.
327                         # TODO: this has to be done.
328                 elif event == iRecordableService.evStart:
329                         # maybe this should be configurable?
330                         Notifications.AddPopup(text = _("A record has been started:\n%s") % self.name, type = MessageBox.TYPE_INFO, timeout = 3)
331
332         # we have record_service as property to automatically subscribe to record service events
333         def setRecordService(self, service):
334                 if self.__record_service is not None:
335                         print "[remove callback]"
336                         NavigationInstance.instance.record_event.remove(self.gotRecordEvent)
337
338                 self.__record_service = service
339
340                 if self.__record_service is not None:
341                         print "[add callback]"
342                         NavigationInstance.instance.record_event.append(self.gotRecordEvent)
343
344         record_service = property(lambda self: self.__record_service, setRecordService)
345
346 def createTimer(xml):
347         begin = int(xml.getAttribute("begin"))
348         end = int(xml.getAttribute("end"))
349         serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
350         description = xml.getAttribute("description").encode("utf-8")
351         repeated = xml.getAttribute("repeated").encode("utf-8")
352         disabled = long(xml.getAttribute("disabled") or "0")
353         justplay = long(xml.getAttribute("justplay") or "0")
354         afterevent = str(xml.getAttribute("afterevent") or "nothing")
355         afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
356         if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
357                 eit = long(xml.getAttribute("eit"))
358         else:
359                 eit = None
360         
361         name = xml.getAttribute("name").encode("utf-8")
362         #filename = xml.getAttribute("filename").encode("utf-8")
363         entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent)
364         entry.repeated = int(repeated)
365         
366         for l in elementsWithTag(xml.childNodes, "log"):
367                 time = int(l.getAttribute("time"))
368                 code = int(l.getAttribute("code"))
369                 msg = mergeText(l.childNodes).strip().encode("utf-8")
370                 entry.log_entries.append((time, code, msg))
371         
372         return entry
373
374 class RecordTimer(timer.Timer):
375         def __init__(self):
376                 timer.Timer.__init__(self)
377                 
378                 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
379                 
380                 try:
381                         self.loadTimer()
382                 except IOError:
383                         print "unable to load timers from file!"
384                         
385         def isRecording(self):
386                 isRunning = False
387                 for timer in self.timer_list:
388                         if timer.isRunning() and not timer.justplay:
389                                 isRunning = True
390                 return isRunning
391         
392         def loadTimer(self):
393                 # TODO: PATH!
394                 doc = xml.dom.minidom.parse(self.Filename)
395                 
396                 root = doc.childNodes[0]
397                 for timer in elementsWithTag(root.childNodes, "timer"):
398                         self.record(createTimer(timer))
399
400         def saveTimer(self):
401                 #doc = xml.dom.minidom.Document()
402                 #root_element = doc.createElement('timers')
403                 #doc.appendChild(root_element)
404                 #root_element.appendChild(doc.createTextNode("\n"))
405                 
406                 #for timer in self.timer_list + self.processed_timers:
407                         # some timers (instant records) don't want to be saved.
408                         # skip them
409                         #if timer.dontSave:
410                                 #continue
411                         #t = doc.createTextNode("\t")
412                         #root_element.appendChild(t)
413                         #t = doc.createElement('timer')
414                         #t.setAttribute("begin", str(int(timer.begin)))
415                         #t.setAttribute("end", str(int(timer.end)))
416                         #t.setAttribute("serviceref", str(timer.service_ref))
417                         #t.setAttribute("repeated", str(timer.repeated))                        
418                         #t.setAttribute("name", timer.name)
419                         #t.setAttribute("description", timer.description)
420                         #t.setAttribute("eit", str(timer.eit))
421                         
422                         #for time, code, msg in timer.log_entries:
423                                 #t.appendChild(doc.createTextNode("\t\t"))
424                                 #l = doc.createElement('log')
425                                 #l.setAttribute("time", str(time))
426                                 #l.setAttribute("code", str(code))
427                                 #l.appendChild(doc.createTextNode(msg))
428                                 #t.appendChild(l)
429                                 #t.appendChild(doc.createTextNode("\n"))
430
431                         #root_element.appendChild(t)
432                         #t = doc.createTextNode("\n")
433                         #root_element.appendChild(t)
434
435
436                 #file = open(self.Filename, "w")
437                 #doc.writexml(file)
438                 #file.write("\n")
439                 #file.close()
440
441                 list = []
442
443                 list.append('<?xml version="1.0" ?>\n')
444                 list.append('<timers>\n')
445                 
446                 for timer in self.timer_list + self.processed_timers:
447                         if timer.dontSave:
448                                 continue
449
450                         list.append('<timer')
451                         list.append(' begin="' + str(int(timer.begin)) + '"')
452                         list.append(' end="' + str(int(timer.end)) + '"')
453                         list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
454                         list.append(' repeated="' + str(int(timer.repeated)) + '"')
455                         list.append(' name="' + str(stringToXML(timer.name)) + '"')
456                         list.append(' description="' + str(stringToXML(timer.description)) + '"')
457                         list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
458                         if timer.eit is not None:
459                                 list.append(' eit="' + str(timer.eit) + '"')
460                         list.append(' disabled="' + str(int(timer.disabled)) + '"')
461                         list.append(' justplay="' + str(int(timer.justplay)) + '"')
462                         list.append('>\n')
463                         
464                         if config.recording.debug.value:
465                                 for time, code, msg in timer.log_entries:
466                                         list.append('<log')
467                                         list.append(' code="' + str(code) + '"')
468                                         list.append(' time="' + str(time) + '"')
469                                         list.append('>')
470                                         list.append(str(stringToXML(msg)))
471                                         list.append('</log>\n')
472                         
473                         list.append('</timer>\n')
474
475                 list.append('</timers>\n')
476
477                 file = open(self.Filename, "w")
478                 for x in list:
479                         file.write(x)
480                 file.close()
481
482         def getNextZapTime(self):
483                 llen = len(self.timer_list)
484                 idx = 0
485                 now = time.time()
486                 while idx < llen:
487                         timer = self.timer_list[idx]
488                         if not timer.justplay or timer.begin < now:
489                                 idx += 1
490                         else:
491                                 return timer.begin
492                 return -1
493
494         def getNextRecordingTime(self):
495                 llen = len(self.timer_list)
496                 idx = 0
497                 now = time.time()
498                 while idx < llen:
499                         timer = self.timer_list[idx]
500                         if timer.justplay or timer.begin < now:
501                                 idx += 1
502                         else:
503                                 return timer.begin
504                 return -1
505
506         def record(self, entry):
507                 entry.timeChanged()
508                 print "[Timer] Record " + str(entry)
509                 entry.Timer = self
510                 self.addTimerEntry(entry)
511                 
512         def isInTimer(self, eventid, begin, duration, service):
513                 time_match = 0
514                 chktime = None
515                 chktimecmp = None
516                 chktimecmp_end = None
517                 end = begin + duration
518                 for x in self.timer_list:
519                         if str(x.service_ref) == str(service):
520                                 #if x.eit is not None and x.repeated == 0:
521                                 #       if x.eit == eventid:
522                                 #               return duration
523                                 if x.repeated != 0:
524                                         if chktime is None:
525                                                 chktime = localtime(begin)
526                                                 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
527                                                 chktimecmp_end = chktimecmp + (duration / 60)
528                                         time = localtime(x.begin)
529                                         for y in range(7):
530                                                 if x.repeated & (2 ** y):
531                                                         timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
532                                                         if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
533                                                                 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
534                                                         elif chktimecmp <= timecmp < chktimecmp_end:
535                                                                 time_match = (chktimecmp_end - timecmp) * 60
536                                 else: #if x.eit is None:
537                                         if begin <= x.begin <= end:
538                                                 diff = end - x.begin
539                                                 if time_match < diff:
540                                                         time_match = diff
541                                         elif x.begin <= begin <= x.end:
542                                                 diff = x.end - begin
543                                                 if time_match < diff:
544                                                         time_match = diff
545                 return time_match
546
547         def removeEntry(self, entry):
548                 print "[Timer] Remove " + str(entry)
549                 
550                 # avoid re-enqueuing
551                 entry.repeated = False
552
553                 # abort timer.
554                 # this sets the end time to current time, so timer will be stopped.
555                 entry.abort()
556                 
557                 if entry.state != entry.StateEnded:
558                         self.timeChanged(entry)
559                 
560                 print "state: ", entry.state
561                 print "in processed: ", entry in self.processed_timers
562                 print "in running: ", entry in self.timer_list
563                 # now the timer should be in the processed_timers list. remove it from there.
564                 self.processed_timers.remove(entry)
565
566         def shutdown(self):
567                 self.saveTimer()