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