fix meta file creation since alternative zapping changes
[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                         if self.record_service.prepare(self.Filename + ".ts", self.begin, self.end, event_id):
126                                 self.log(2, "'prepare' failed: error %d" % prep_res)
127                                 self.record_service = None
128                                 return False
129                                 
130                         if self.repeated:
131                                 epgcache = eEPGCache.getInstance()
132                                 queryTime=self.begin+(self.end-self.begin)/2
133                                 evt = epgcache.lookupEventTime(rec_ref, queryTime)
134                                 if evt:
135                                         self.description = evt.getShortDescription()
136                                 
137                         self.log(3, "prepare ok, writing meta information to %s" % self.Filename)
138                         try:
139                                 f = open(self.Filename + ".ts.meta", "w")
140                                 f.write(rec_ref.toString() + "\n")
141                                 f.write(self.name + "\n")
142                                 f.write(self.description + "\n")
143                                 f.write(str(self.begin) + "\n")
144                                 f.close()
145                         except IOError:
146                                 self.log(4, "failed to write meta information")
147                                 self.record_service = None
148                                 return False
149                         return True
150
151         def do_backoff(self):
152                 if self.backoff == 0:
153                         self.backoff = 5
154                 else:
155                         self.backoff *= 2
156                         if self.backoff > 100:
157                                 self.backoff = 100
158                 self.log(10, "backoff: retry in %d seconds" % self.backoff)
159
160         def activate(self):
161                 next_state = self.state + 1
162                 self.log(5, "activating state %d" % next_state)
163                 
164                 if next_state == self.StatePrepared:
165                         if self.tryPrepare():
166                                 self.log(6, "prepare ok, waiting for begin")
167                                 # fine. it worked, resources are allocated.
168                                 self.next_activation = self.begin
169                                 self.backoff = 0
170                                 return True
171                         
172                         self.log(7, "prepare failed")
173                         if self.first_try_prepare:
174                                 self.first_try_prepare = False
175                                 if not config.recording.asktozap.value:
176                                         self.log(8, "asking user to zap away")
177                                         Notifications.AddNotificationWithCallback(self.failureCB, MessageBox, _("A timer failed to record!\nDisable TV and try again?\n"), timeout=20)
178                                 else: # zap without asking
179                                         self.log(9, "zap without asking")
180                                         Notifications.AddNotification(MessageBox, _("In order to record a timer, the TV was switched to the recording service!\n"), type=MessageBox.TYPE_INFO, timeout=20)
181                                         self.failureCB(True)
182
183                         self.do_backoff()
184                         # retry
185                         self.start_prepare = time.time() + self.backoff
186                         return False
187                 elif next_state == self.StateRunning:
188                         # if this timer has been cancelled, just go to "end" state.
189                         if self.cancelled:
190                                 return True
191
192                         if self.justplay:
193                                 self.log(11, "zapping")
194                                 NavigationInstance.instance.playService(self.service_ref.ref)
195                                 return True
196                         else:
197                                 self.log(11, "start recording")
198                                 record_res = self.record_service.start()
199                                 
200                                 if record_res:
201                                         self.log(13, "start record returned %d" % record_res)
202                                         self.do_backoff()
203                                         # retry
204                                         self.begin = time.time() + self.backoff
205                                         return False
206                                 
207                                 return True
208                 elif next_state == self.StateEnded:
209                         self.log(12, "stop recording")
210                         if not self.justplay:
211                                 NavigationInstance.instance.stopRecordService(self.record_service)
212                                 self.record_service = None
213                         if self.afterEvent == AFTEREVENT.STANDBY:
214                                 if self.session is not None:
215                                         self.session.open(Standby, self)
216                         elif self.afterEvent == AFTEREVENT.DEEPSTANDBY:
217                                 quitMainloop(1)
218                         return True
219
220         def getNextActivation(self):
221                 if self.state == self.StateEnded:
222                         return self.end
223                 
224                 next_state = self.state + 1
225                 
226                 return {self.StatePrepared: self.start_prepare, 
227                                 self.StateRunning: self.begin, 
228                                 self.StateEnded: self.end }[next_state]
229
230         def failureCB(self, answer):
231                 if answer == True:
232                         self.log(13, "ok, zapped away")
233                         #NavigationInstance.instance.stopUserServices()
234                         NavigationInstance.instance.playService(self.service_ref.ref)
235                 else:
236                         self.log(14, "user didn't want to zap away, record will probably fail")
237
238         def timeChanged(self):
239                 old_prepare = self.start_prepare
240                 self.start_prepare = self.begin - self.prepare_time
241                 self.backoff = 0
242                 
243                 if int(old_prepare) != int(self.start_prepare):
244                         self.log(15, "record time changed, start prepare is now: %s" % time.ctime(self.start_prepare))
245
246 def createTimer(xml):
247         begin = int(xml.getAttribute("begin"))
248         end = int(xml.getAttribute("end"))
249         serviceref = ServiceReference(xml.getAttribute("serviceref").encode("utf-8"))
250         description = xml.getAttribute("description").encode("utf-8")
251         repeated = xml.getAttribute("repeated").encode("utf-8")
252         disabled = long(xml.getAttribute("disabled") or "0")
253         justplay = long(xml.getAttribute("justplay") or "0")
254         afterevent = str(xml.getAttribute("afterevent") or "nothing")
255         afterevent = { "nothing": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY }[afterevent]
256         if xml.hasAttribute("eit") and xml.getAttribute("eit") != "None":
257                 eit = long(xml.getAttribute("eit"))
258         else:
259                 eit = None
260         
261         name = xml.getAttribute("name").encode("utf-8")
262         #filename = xml.getAttribute("filename").encode("utf-8")
263         entry = RecordTimerEntry(serviceref, begin, end, name, description, eit, disabled, justplay, afterevent)
264         entry.repeated = int(repeated)
265         
266         for l in elementsWithTag(xml.childNodes, "log"):
267                 time = int(l.getAttribute("time"))
268                 code = int(l.getAttribute("code"))
269                 msg = mergeText(l.childNodes).strip().encode("utf-8")
270                 entry.log_entries.append((time, code, msg))
271         
272         return entry
273
274 class RecordTimer(timer.Timer):
275         def __init__(self):
276                 timer.Timer.__init__(self)
277                 
278                 self.Filename = Directories.resolveFilename(Directories.SCOPE_CONFIG, "timers.xml")
279                 
280                 try:
281                         self.loadTimer()
282                 except IOError:
283                         print "unable to load timers from file!"
284                         
285         def isRecording(self):
286                 isRunning = False
287                 for timer in self.timer_list:
288                         if timer.isRunning() and not timer.justplay:
289                                 isRunning = True
290                 return isRunning
291         
292         def loadTimer(self):
293                 # TODO: PATH!
294                 doc = xml.dom.minidom.parse(self.Filename)
295                 
296                 root = doc.childNodes[0]
297                 for timer in elementsWithTag(root.childNodes, "timer"):
298                         self.record(createTimer(timer))
299
300         def saveTimer(self):
301                 #doc = xml.dom.minidom.Document()
302                 #root_element = doc.createElement('timers')
303                 #doc.appendChild(root_element)
304                 #root_element.appendChild(doc.createTextNode("\n"))
305                 
306                 #for timer in self.timer_list + self.processed_timers:
307                         # some timers (instant records) don't want to be saved.
308                         # skip them
309                         #if timer.dontSave:
310                                 #continue
311                         #t = doc.createTextNode("\t")
312                         #root_element.appendChild(t)
313                         #t = doc.createElement('timer')
314                         #t.setAttribute("begin", str(int(timer.begin)))
315                         #t.setAttribute("end", str(int(timer.end)))
316                         #t.setAttribute("serviceref", str(timer.service_ref))
317                         #t.setAttribute("repeated", str(timer.repeated))                        
318                         #t.setAttribute("name", timer.name)
319                         #t.setAttribute("description", timer.description)
320                         #t.setAttribute("eit", str(timer.eit))
321                         
322                         #for time, code, msg in timer.log_entries:
323                                 #t.appendChild(doc.createTextNode("\t\t"))
324                                 #l = doc.createElement('log')
325                                 #l.setAttribute("time", str(time))
326                                 #l.setAttribute("code", str(code))
327                                 #l.appendChild(doc.createTextNode(msg))
328                                 #t.appendChild(l)
329                                 #t.appendChild(doc.createTextNode("\n"))
330
331                         #root_element.appendChild(t)
332                         #t = doc.createTextNode("\n")
333                         #root_element.appendChild(t)
334
335
336                 #file = open(self.Filename, "w")
337                 #doc.writexml(file)
338                 #file.write("\n")
339                 #file.close()
340
341                 list = []
342
343                 list.append('<?xml version="1.0" ?>\n')
344                 list.append('<timers>\n')
345                 
346                 for timer in self.timer_list + self.processed_timers:
347                         if timer.dontSave:
348                                 continue
349
350                         list.append('<timer')
351                         list.append(' begin="' + str(int(timer.begin)) + '"')
352                         list.append(' end="' + str(int(timer.end)) + '"')
353                         list.append(' serviceref="' + stringToXML(str(timer.service_ref)) + '"')
354                         list.append(' repeated="' + str(int(timer.repeated)) + '"')
355                         list.append(' name="' + str(stringToXML(timer.name)) + '"')
356                         list.append(' description="' + str(stringToXML(timer.description)) + '"')
357                         list.append(' afterevent="' + str(stringToXML({ AFTEREVENT.NONE: "nothing", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "deepstandby" }[timer.afterEvent])) + '"')
358                         if timer.eit is not None:
359                                 list.append(' eit="' + str(timer.eit) + '"')
360                         list.append(' disabled="' + str(int(timer.disabled)) + '"')
361                         list.append(' justplay="' + str(int(timer.justplay)) + '"')
362                         list.append('>\n')
363                         
364                         if config.recording.debug.value:
365                                 for time, code, msg in timer.log_entries:
366                                         list.append('<log')
367                                         list.append(' code="' + str(code) + '"')
368                                         list.append(' time="' + str(time) + '"')
369                                         list.append('>')
370                                         list.append(str(stringToXML(msg)))
371                                         list.append('</log>\n')
372                         
373                         list.append('</timer>\n')
374
375                 list.append('</timers>\n')
376
377                 file = open(self.Filename, "w")
378                 for x in list:
379                         file.write(x)
380                 file.close()
381
382         def record(self, entry):
383                 entry.timeChanged()
384                 print "[Timer] Record " + str(entry)
385                 entry.Timer = self
386                 self.addTimerEntry(entry)
387                 
388         def isInTimer(self, eventid, begin, duration, service):
389                 time_match = 0
390                 chktime = None
391                 chktimecmp = None
392                 chktimecmp_end = None
393                 end = begin + duration
394                 for x in self.timer_list:
395                         if str(x.service_ref) == str(service):
396                                 #if x.eit is not None and x.repeated == 0:
397                                 #       if x.eit == eventid:
398                                 #               return duration
399                                 if x.repeated != 0:
400                                         if chktime is None:
401                                                 chktime = localtime(begin)
402                                                 chktimecmp = chktime.tm_wday * 1440 + chktime.tm_hour * 60 + chktime.tm_min
403                                                 chktimecmp_end = chktimecmp + (duration / 60)
404                                         time = localtime(x.begin)
405                                         for y in range(7):
406                                                 if x.repeated & (2 ** y):
407                                                         timecmp = y * 1440 + time.tm_hour * 60 + time.tm_min
408                                                         if timecmp <= chktimecmp < (timecmp + ((x.end - x.begin) / 60)):
409                                                                 time_match = ((timecmp + ((x.end - x.begin) / 60)) - chktimecmp) * 60
410                                                         elif chktimecmp <= timecmp < chktimecmp_end:
411                                                                 time_match = (chktimecmp_end - timecmp) * 60
412                                 else: #if x.eit is None:
413                                         if begin <= x.begin <= end:
414                                                 diff = end - x.begin
415                                                 if time_match < diff:
416                                                         time_match = diff
417                                         elif x.begin <= begin <= x.end:
418                                                 diff = x.end - begin
419                                                 if time_match < diff:
420                                                         time_match = diff
421                 return time_match
422
423         def removeEntry(self, entry):
424                 print "[Timer] Remove " + str(entry)
425                 
426                 # avoid re-enqueuing
427                 entry.repeated = False
428
429                 # abort timer.
430                 # this sets the end time to current time, so timer will be stopped.
431                 entry.abort()
432                 
433                 if entry.state != entry.StateEnded:
434                         self.timeChanged(entry)
435                 
436                 print "state: ", entry.state
437                 print "in processed: ", entry in self.processed_timers
438                 print "in running: ", entry in self.timer_list
439                 # now the timer should be in the processed_timers list. remove it from there.
440                 self.processed_timers.remove(entry)
441
442         def shutdown(self):
443                 self.saveTimer()