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