intial commit of autotimer, epgrefresh and werbezapper to enigma2-plugins
[enigma2-plugins.git] / autotimer / src / AutoTimer.py
1 # Plugins Config
2 from xml.dom.minidom import parse as minidom_parse
3 from os import path as os_path
4
5 # Navigation (RecordTimer)
6 import NavigationInstance
7
8 # Timer
9 from ServiceReference import ServiceReference
10 from RecordTimer import RecordTimerEntry, AFTEREVENT
11 from Components.TimerSanityCheck import TimerSanityCheck
12
13 # Timespan
14 from time import localtime, time
15
16 # EPGCache & Event
17 from enigma import eEPGCache, eServiceReference
18
19 # Enigma2 Config
20 from Components.config import config
21
22 # AutoTimer Component
23 from AutoTimerComponent import AutoTimerComponent
24
25 XML_CONFIG = "/etc/enigma2/autotimer.xml"
26 CURRENT_CONFIG_VERSION = "4"
27
28 def getValue(definitions, default, isList = True):
29         # Initialize Output
30         ret = ""
31
32         # How many definitions are present
33         if isList:
34                 Len = len(definitions)
35                 if Len > 0:
36                         childNodes = definitions[Len-1].childNodes
37                 else:
38                         childNodes = []
39         else:
40                 childNodes = definitions.childNodes
41
42         # Iterate through nodes of last one
43         for node in childNodes:
44                 # Append text if we have a text node
45                 if node.nodeType == node.TEXT_NODE:
46                         ret = ret + node.data
47
48         # Return stripped output or (if empty) default
49         return ret.strip() or default
50
51 def getTimeDiff(timer, begin, end):
52         if begin <= timer.begin <= end:
53                 return end - timer.begin
54         elif timer.begin <= begin <= timer.end:
55                 return timer.end - begin
56         return 0
57
58 class AutoTimerIgnoreTimerException(Exception):
59         def __init__(self, cause):
60                 self.cause = cause
61
62         def __str__(self):
63                 return "[AutoTimer] " + str(self.cause)
64
65         def __repr__(self):
66                 return str(type(self))
67
68 class AutoTimer:
69         """Read and save xml configuration, query EPGCache"""
70
71         def __init__(self):
72                 # Keep EPGCache
73                 self.epgcache = eEPGCache.getInstance()
74
75                 # Initialize
76                 self.timers = []
77                 self.configMtime = -1
78                 self.uniqueTimerId = 0
79
80         def readXml(self):
81                 # Abort if no config found
82                 if not os_path.exists(XML_CONFIG):
83                         return
84
85                 # Parse if mtime differs from whats saved
86                 mtime = os_path.getmtime(XML_CONFIG)
87                 if mtime == self.configMtime:
88                         print "[AutoTimer] No changes in configuration, won't parse"
89                         return
90
91                 # Save current mtime
92                 self.configMtime = mtime
93
94                 # Parse Config
95                 dom = minidom_parse(XML_CONFIG)
96                 
97                 # Empty out timers and reset Ids
98                 del self.timers[:]
99                 self.uniqueTimerId = 0
100
101                 # Get Config Element
102                 for configuration in dom.getElementsByTagName("autotimer"):
103                         # Parse old configuration files
104                         if configuration.getAttribute("version") != CURRENT_CONFIG_VERSION:
105                                 from OldConfigurationParser import parseConfig
106                                 parseConfig(configuration, self.timers, configuration.getAttribute("version"), self.uniqueTimerId)
107                                 if not self.uniqueTimerId:
108                                         self.uniqueTimerId = len(self.timers)
109                                 continue
110                         # Iterate Timers
111                         for timer in configuration.getElementsByTagName("timer"):
112                                 # Timers are saved as tuple (name, allowedtime (from, to) or None, list of services or None, timeoffset in m (before, after) or None, afterevent)
113
114                                 # Increment uniqueTimerId
115                                 self.uniqueTimerId += 1
116
117                                 # Read out match
118                                 match = timer.getAttribute("match").encode("UTF-8")
119                                 if not match:
120                                         print '[AutoTimer] Erroneous config is missing attribute "match", skipping entry'
121                                         continue
122
123                                 # Read out name
124                                 name = timer.getAttribute("name").encode("UTF-8")
125                                 if not name:
126                                         print '[AutoTimer] Timer is missing attribute "name", defaulting to match'
127                                         name = match
128
129                                 # Read out enabled
130                                 enabled = timer.getAttribute("enabled") or "yes"
131                                 if enabled == "no":
132                                         enabled = False
133                                 elif enabled == "yes":
134                                         enabled = True
135                                 else:
136                                         print '[AutoTimer] Erroneous config contains invalid value for "enabled":', enabled,', disabling'
137                                         enabled = False
138
139                                 # Read out timespan
140                                 start = timer.getAttribute("from")
141                                 end = timer.getAttribute("to")
142                                 if start and end:
143                                         start = [int(x) for x in start.split(':')]
144                                         end = [int(x) for x in end.split(':')]
145                                         timetuple = (start, end)
146                                 else:
147                                         timetuple = None
148
149                                 # Read out max length
150                                 maxlen = timer.getAttribute("maxduration") or None
151                                 if maxlen:
152                                         maxlen = int(maxlen)*60
153
154                                 # Read out recording path (needs my Location-select patch)
155                                 destination = timer.getAttribute("destination").encode("UTF-8") or None
156
157                                 # Read out offset
158                                 offset = timer.getAttribute("offset") or None
159                                 if offset:
160                                         offset = offset.split(",")
161                                         if len(offset) == 1:
162                                                 before = after = int(offset[0] or 0) * 60
163                                         else:
164                                                 before = int(offset[0] or 0) * 60
165                                                 after = int(offset[1] or 0) * 60
166                                         offset = (before, after)
167
168                                 # Read out counter
169                                 counter = int(timer.getAttribute("counter") or '0')
170                                 counterLeft = timer.getAttribute("left") or counter
171                                 counterLimit = timer.getAttribute("lastActivation")
172                                 counterFormat = timer.getAttribute("counterFormat")
173                                 lastBegin = timer.getAttribute("lastBegin") or 0
174
175                                 # Read out justplay
176                                 justplay = int(timer.getAttribute("justplay") or '0')
177
178                                 # Read out allowed services
179                                 servicelist = []                                        
180                                 for service in timer.getElementsByTagName("serviceref"):
181                                         value = getValue(service, None, False)
182                                         if value:
183                                                 # strip all after last :
184                                                 pos = value.rfind(':')
185                                                 if pos != -1:
186                                                         value = value[:pos+1]
187
188                                                 servicelist.append(value)
189
190                                 # Read out afterevent
191                                 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
192                                 afterevent = []
193                                 for element in timer.getElementsByTagName("afterevent"):
194                                         value = getValue(element, None, False)
195
196                                         try:
197                                                 value = idx[value]
198                                                 start = element.getAttribute("from")
199                                                 end = element.getAttribute("to")
200                                                 if start and end:
201                                                         start = [int(x) for x in start.split(':')]
202                                                         end = [int(x) for x in end.split(':')]
203                                                         afterevent.append((value, (start, end)))
204                                                 else:
205                                                         afterevent.append((value, None))
206                                         except KeyError, ke:
207                                                 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
208                                                 continue
209
210                                 # Read out exclude
211                                 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
212                                 excludes = ([], [], [], []) 
213                                 for exclude in timer.getElementsByTagName("exclude"):
214                                         where = exclude.getAttribute("where")
215                                         value = getValue(exclude, None, False)
216                                         if not (value and where):
217                                                 continue
218
219                                         try:
220                                                 excludes[idx[where]].append(value.encode("UTF-8"))
221                                         except KeyError, ke:
222                                                 pass
223
224                                 # Read out includes (use same idx)
225                                 includes = ([], [], [], []) 
226                                 for include in timer.getElementsByTagName("include"):
227                                         where = include.getAttribute("where")
228                                         value = getValue(include, None, False)
229                                         if not (value and where):
230                                                 continue
231
232                                         try:
233                                                 includes[idx[where]].append(value.encode("UTF-8"))
234                                         except KeyError, ke:
235                                                 pass
236
237                                 # Finally append tuple
238                                 self.timers.append(AutoTimerComponent(
239                                                 self.uniqueTimerId,
240                                                 name,
241                                                 match,
242                                                 enabled,
243                                                 timespan = timetuple,
244                                                 services = servicelist,
245                                                 offset = offset,
246                                                 afterevent = afterevent,
247                                                 exclude = excludes,
248                                                 include = includes,
249                                                 maxduration = maxlen,
250                                                 destination = destination,
251                                                 matchCount = counter,
252                                                 matchLeft = counterLeft,
253                                                 matchLimit = counterLimit,
254                                                 matchFormatString = counterFormat,
255                                                 lastBegin = int(lastBegin),
256                                                 justplay = justplay
257                                 ))
258
259         def getTimerList(self):
260                 return self.timers
261
262         def getEnabledTimerList(self):
263                 return [x for x in self.timers if x.enabled]
264
265         def getTupleTimerList(self):
266                 return [(x,) for x in self.timers]
267
268         def getUniqueId(self):
269                 self.uniqueTimerId += 1
270                 return self.uniqueTimerId
271
272         def add(self, timer):
273                 self.timers.append(timer)
274
275         def set(self, timer):
276                 idx = 0
277                 for stimer in self.timers:
278                         if stimer == timer:
279                                 self.timers[idx] = timer
280                                 return
281                         idx += 1
282                 self.timers.append(timer)
283
284         def remove(self, uniqueId):
285                 idx = 0
286                 for timer in self.timers:
287                         if timer.id == uniqueId:
288                                 self.timers.pop(idx)
289                                 return
290                         idx += 1
291
292         def writeXml(self):
293                 # Generate List in RAM
294                 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
295
296                 # Iterate timers
297                 for timer in self.timers:
298                         # Common attributes (match, enabled)
299                         list.extend([' <timer name="', timer.name, '" match="', timer.match, '" enabled="', timer.getEnabled(), '"'])
300
301                         # Timespan
302                         if timer.hasTimespan():
303                                 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
304
305                         # Duration
306                         if timer.hasDuration():
307                                 list.extend([' maxduration="', str(timer.getDuration()), '"'])
308
309                         # Destination (needs my Location-select patch)
310                         if timer.hasDestination():
311                                 list.extend([' destination="', str(timer.destination), '"'])
312
313                         # Offset
314                         if timer.hasOffset():
315                                 if timer.isOffsetEqual():
316                                         list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
317                                 else:
318                                         list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
319
320                         # Counter
321                         if timer.hasCounter():
322                                 list.extend([' lastBegin="', str(timer.lastBegin), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
323                                 if timer.hasCounterFormatString():
324                                         list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
325                                         list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
326
327                         # Only display justplay if true
328                         if timer.justplay:
329                                 list.extend([' justplay="', str(timer.getJustplay()), '"'])
330
331                         # Close still opened timer tag
332                         list.append('>\n')
333
334                         # Services
335                         for serviceref in timer.getServices():
336                                 list.extend(['  <serviceref>', serviceref, '</serviceref>'])
337                                 ref = ServiceReference(str(serviceref))
338                                 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
339
340                         # AfterEvent
341                         if timer.hasAfterEvent():
342                                 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
343                                 for afterevent in timer.getCompleteAfterEvent():
344                                         action, timespan = afterevent
345                                         list.append('  <afterevent')
346                                         if timespan[0] is not None:
347                                                 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
348                                         list.extend(['>', idx[action], '</afterevent>\n'])
349
350                         # Excludes
351                         for title in timer.getExcludedTitle():
352                                 list.extend(['  <exclude where="title">', title, '</exclude>\n'])
353                         for short in timer.getExcludedShort():
354                                 list.extend(['  <exclude where="shortdescription">', short, '</exclude>\n'])
355                         for desc in timer.getExcludedDescription():
356                                 list.extend(['  <exclude where="description">', desc, '</exclude>\n'])
357                         for day in timer.getExcludedDays():
358                                 list.extend(['  <exclude where="dayofweek">', day, '</exclude>\n'])
359
360                         # Includes
361                         for title in timer.getIncludedTitle():
362                                 list.extend(['  <include where="title">', title, '</include>\n'])
363                         for short in timer.getIncludedShort():
364                                 list.extend(['  <include where="shortdescription">', short, '</include>\n'])
365                         for desc in timer.getIncludedDescription():
366                                 list.extend(['  <include where="description">', desc, '</include>\n'])
367                         for day in timer.getIncludedDays():
368                                 list.extend(['  <include where="dayofweek">', day, '</include>\n'])
369
370                         # End of Timer
371                         list.append(' </timer>\n\n')
372
373                 # End of Configuration
374                 list.append('</autotimer>\n')
375
376                 # Try Saving to Flash
377                 file = None
378                 try:
379                         file = open(XML_CONFIG, 'w')
380                         file.writelines(list)
381
382                         # FIXME: This should actually be placed inside a finally-block but python 2.4 does not support this - waiting for some images to upgrade
383                         file.close()
384                 except Exception, e:
385                         print "[AutoTimer] Error Saving Timer List:", e
386
387         def parseEPG(self, simulateOnly = False):
388                 if NavigationInstance.instance is None:
389                         print "[AutoTimer] Navigation is not available, can't parse EPG"
390                         return (0, 0, 0, [])
391
392                 total = 0
393                 new = 0
394                 modified = 0
395                 timers = []
396
397                 self.readXml()
398
399                 # Save Recordings in a dict to speed things up a little
400                 recorddict = {}
401                 for timer in NavigationInstance.instance.RecordTimer.timer_list:
402                         if not recorddict.has_key(str(timer.service_ref)):
403                                 recorddict[str(timer.service_ref)] = [timer]
404                         else:
405                                 recorddict[str(timer.service_ref)].append(timer)
406
407                 # Iterate Timer
408                 for timer in self.getEnabledTimerList():
409                         # Search EPG, default to empty list
410                         ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
411
412                         for serviceref, eit in ret:
413                                 # Check if Service is disallowed first as its the only property available here
414                                 if timer.checkServices(serviceref):
415                                         continue
416
417                                 evt = self.epgcache.lookupEventId(eServiceReference(serviceref), eit)
418                                 if not evt:
419                                         print "[AutoTimer] Could not create Event!"
420                                         continue
421
422                                 # Gather Information
423                                 name = evt.getEventName()
424                                 description = evt.getShortDescription()
425                                 begin = evt.getBeginTime()
426                                 duration = evt.getDuration()
427                                 end = begin + duration
428
429                                 # If event starts in less than 60 seconds skip it
430                                 if begin < time() + 60:
431                                         continue
432
433                                 # Convert begin time
434                                 timestamp = localtime(begin)
435
436                                 # Update timer
437                                 timer.update(begin, timestamp)
438
439                                 # Check Duration, Timespan and Excludes
440                                 if timer.checkDuration(duration) or timer.checkTimespan(timestamp) or timer.checkFilter(name, description, evt.getExtendedDescription(), str(timestamp[6])):
441                                         continue
442
443                                 # Apply E2 Offset
444                                 begin -= config.recording.margin_before.value * 60
445                                 end += config.recording.margin_after.value * 60
446  
447                                 # Apply custom Offset
448                                 begin, end = timer.applyOffset(begin, end)
449
450                                 total += 1
451
452                                 # Append to timerlist and abort if simulating
453                                 timers.append((name, begin, end, serviceref, timer.name))
454                                 if simulateOnly:
455                                         continue
456
457                                 # Initialize
458                                 newEntry = None
459
460                                 # Check for double Timers
461                                 # We first check eit and if user wants us to guess event based on time
462                                 # we try this as backup. The allowed diff should be configurable though.
463                                 try:
464                                         for rtimer in recorddict.get(serviceref, []):
465                                                 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
466                                                         newEntry = rtimer
467
468                                                         # Abort if we don't want to modify timers or timer is repeated
469                                                         if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
470                                                                 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
471
472                                                         try:
473                                                                 if newEntry.isAutoTimer:
474                                                                         print "[AutoTimer] Modifying existing AutoTimer!"
475                                                         except AttributeError, ae:
476                                                                 if config.plugins.autotimer.refresh.value != "all":
477                                                                         raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
478                                                                 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
479
480                                                         func = NavigationInstance.instance.RecordTimer.timeChanged
481                                                         modified += 1
482
483                                                         # Modify values saved in timer
484                                                         newEntry.name = name
485                                                         newEntry.description = description
486                                                         newEntry.begin = int(begin)
487                                                         newEntry.end = int(end)
488
489                                                         break
490                                 except AutoTimerIgnoreTimerException, etite:
491                                         print etite
492                                         continue
493
494                                 # Event not yet in Timers
495                                 if newEntry is None:
496                                         if timer.checkCounter(timestamp):
497                                                 continue
498
499                                         new += 1
500
501                                         print "[AutoTimer] Adding an event."
502                                         newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
503                                         func = NavigationInstance.instance.RecordTimer.record
504
505                                         # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
506                                         newEntry.isAutoTimer = True
507
508                                 # Apply afterEvent
509                                 if timer.hasAfterEvent():
510                                         afterEvent = timer.getAfterEventTimespan(localtime(end))
511                                         if afterEvent is None:
512                                                 afterEvent = timer.getAfterEvent()
513                                         if afterEvent is not None:
514                                                 newEntry.afterEvent = afterEvent
515
516                                 # Set custom destination directory (needs my Location-select patch)
517                                 if timer.hasDestination():
518                                         # TODO: add warning when patch not installed?
519                                         newEntry.dirname = timer.destination
520  
521                                 # Make this timer a zap-timer if wanted
522                                 newEntry.justplay = timer.justplay
523  
524                                 # Do a sanity check, although it does not do much right now
525                                 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
526                                 if not timersanitycheck.check():
527                                         print "[Autotimer] Sanity check failed"
528                                 else:
529                                         print "[Autotimer] Sanity check passed"
530
531                                 # Either add to List or change time
532                                 func(newEntry)
533
534                 return (total, new, modified, timers)