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