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