- be a little more sensitive with exceptions
[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):
29         # Initialize Output
30         ret = ""
31
32         # How many definitions are present
33         try:
34                 childNodes = definitions.childNodes
35         except:
36                 Len = len(definitions)
37                 if Len > 0:
38                         childNodes = definitions[Len-1].childNodes
39                 else:
40                         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
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 = int(timer.getAttribute("left") or counter)
171                                 counterLimit = timer.getAttribute("lastActivation")
172                                 counterFormat = timer.getAttribute("counterFormat")
173                                 lastBegin = int(timer.getAttribute("lastBegin") or 0)
174
175                                 # Read out justplay
176                                 justplay = int(timer.getAttribute("justplay") or '0')
177
178                                 # Read out avoidDuplicateDescription
179                                 avoidDuplicateDescription = bool(timer.getAttribute("avoidDuplicateDescription") or False)
180
181                                 # Read out allowed services
182                                 servicelist = []                                        
183                                 for service in timer.getElementsByTagName("serviceref"):
184                                         value = getValue(service, None)
185                                         if value:
186                                                 # strip all after last :
187                                                 pos = value.rfind(':')
188                                                 if pos != -1:
189                                                         value = value[:pos+1]
190
191                                                 servicelist.append(value)
192
193                                 # Read out allowed bouquets
194                                 bouquets = []
195                                 for bouquet in timer.getElementsByTagName("bouquet"):
196                                         value = getValue(bouquet, None)
197                                         if value:
198                                                 bouquets.append(value)
199
200                                 # Read out afterevent
201                                 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
202                                 afterevent = []
203                                 for element in timer.getElementsByTagName("afterevent"):
204                                         value = getValue(element, None)
205
206                                         try:
207                                                 value = idx[value]
208                                                 start = element.getAttribute("from")
209                                                 end = element.getAttribute("to")
210                                                 if start and end:
211                                                         start = [int(x) for x in start.split(':')]
212                                                         end = [int(x) for x in end.split(':')]
213                                                         afterevent.append((value, (start, end)))
214                                                 else:
215                                                         afterevent.append((value, None))
216                                         except KeyError, ke:
217                                                 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
218                                                 continue
219
220                                 # Read out exclude
221                                 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
222                                 excludes = ([], [], [], []) 
223                                 for exclude in timer.getElementsByTagName("exclude"):
224                                         where = exclude.getAttribute("where")
225                                         value = getValue(exclude, None)
226                                         if not (value and where):
227                                                 continue
228
229                                         try:
230                                                 excludes[idx[where]].append(value.encode("UTF-8"))
231                                         except KeyError, ke:
232                                                 pass
233
234                                 # Read out includes (use same idx)
235                                 includes = ([], [], [], []) 
236                                 for include in timer.getElementsByTagName("include"):
237                                         where = include.getAttribute("where")
238                                         value = getValue(include, None)
239                                         if not (value and where):
240                                                 continue
241
242                                         try:
243                                                 includes[idx[where]].append(value.encode("UTF-8"))
244                                         except KeyError, ke:
245                                                 pass
246
247                                 # Read out recording tags (needs my enhanced tag support patch)
248                                 tags = []
249                                 for tag in timer.getElementsByTagName("tag"):
250                                         value = getValue(tag, None)
251                                         if not value:
252                                                 continue
253
254                                         tags.append(value.encode("UTF-8"))
255
256                                 # Finally append tuple
257                                 self.timers.append(AutoTimerComponent(
258                                                 self.uniqueTimerId,
259                                                 name,
260                                                 match,
261                                                 enabled,
262                                                 timespan = timetuple,
263                                                 services = servicelist,
264                                                 offset = offset,
265                                                 afterevent = afterevent,
266                                                 exclude = excludes,
267                                                 include = includes,
268                                                 maxduration = maxlen,
269                                                 destination = destination,
270                                                 matchCount = counter,
271                                                 matchLeft = counterLeft,
272                                                 matchLimit = counterLimit,
273                                                 matchFormatString = counterFormat,
274                                                 lastBegin = lastBegin,
275                                                 justplay = justplay,
276                                                 avoidDuplicateDescription = avoidDuplicateDescription,
277                                                 bouquets = bouquets,
278                                                 tags = tags
279                                 ))
280
281         def getTimerList(self):
282                 return self.timers
283
284         def getEnabledTimerList(self):
285                 return [x for x in self.timers if x.enabled]
286
287         def getTupleTimerList(self):
288                 return [(x,) for x in self.timers]
289
290         def getUniqueId(self):
291                 self.uniqueTimerId += 1
292                 return self.uniqueTimerId
293
294         def add(self, timer):
295                 self.timers.append(timer)
296
297         def set(self, timer):
298                 idx = 0
299                 for stimer in self.timers:
300                         if stimer == timer:
301                                 self.timers[idx] = timer
302                                 return
303                         idx += 1
304                 self.timers.append(timer)
305
306         def remove(self, uniqueId):
307                 idx = 0
308                 for timer in self.timers:
309                         if timer.id == uniqueId:
310                                 self.timers.pop(idx)
311                                 return
312                         idx += 1
313
314         def writeXml(self):
315                 # Generate List in RAM
316                 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
317
318                 # Iterate timers
319                 for timer in self.timers:
320                         # Common attributes (match, enabled)
321                         list.extend([' <timer name="', timer.name, '" match="', timer.match, '" enabled="', timer.getEnabled(), '"'])
322
323                         # Timespan
324                         if timer.hasTimespan():
325                                 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
326
327                         # Duration
328                         if timer.hasDuration():
329                                 list.extend([' maxduration="', str(timer.getDuration()), '"'])
330
331                         # Destination (needs my Location-select patch)
332                         if timer.hasDestination():
333                                 list.extend([' destination="', str(timer.destination), '"'])
334
335                         # Offset
336                         if timer.hasOffset():
337                                 if timer.isOffsetEqual():
338                                         list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
339                                 else:
340                                         list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
341
342                         # Counter
343                         if timer.hasCounter():
344                                 list.extend([' lastBegin="', str(timer.getLastBegin()), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
345                                 if timer.hasCounterFormatString():
346                                         list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
347                                         list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
348
349                         # Duplicate Description
350                         if timer.getAvoidDuplicateDescription():
351                                 list.append(' avoidDuplicateDescription="1" ')
352
353                         # Only display justplay if true
354                         if timer.justplay:
355                                 list.extend([' justplay="', str(timer.getJustplay()), '"'])
356
357                         # Close still opened timer tag
358                         list.append('>\n')
359
360                         # Services
361                         for serviceref in timer.getServices():
362                                 list.extend(['  <serviceref>', serviceref, '</serviceref>'])
363                                 ref = ServiceReference(str(serviceref))
364                                 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
365
366                         # Bouquets
367                         for bouquet in timer.getBouquets():
368                                 list.extend(['  <bouquet>', str(bouquet), '</bouquet>'])
369                                 ref = ServiceReference(str(bouquet))
370                                 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
371
372                         # AfterEvent
373                         if timer.hasAfterEvent():
374                                 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
375                                 for afterevent in timer.getCompleteAfterEvent():
376                                         action, timespan = afterevent
377                                         list.append('  <afterevent')
378                                         if timespan[0] is not None:
379                                                 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
380                                         list.extend(['>', idx[action], '</afterevent>\n'])
381
382                         # Excludes
383                         for title in timer.getExcludedTitle():
384                                 list.extend(['  <exclude where="title">', title, '</exclude>\n'])
385                         for short in timer.getExcludedShort():
386                                 list.extend(['  <exclude where="shortdescription">', short, '</exclude>\n'])
387                         for desc in timer.getExcludedDescription():
388                                 list.extend(['  <exclude where="description">', desc, '</exclude>\n'])
389                         for day in timer.getExcludedDays():
390                                 list.extend(['  <exclude where="dayofweek">', day, '</exclude>\n'])
391
392                         # Includes
393                         for title in timer.getIncludedTitle():
394                                 list.extend(['  <include where="title">', title, '</include>\n'])
395                         for short in timer.getIncludedShort():
396                                 list.extend(['  <include where="shortdescription">', short, '</include>\n'])
397                         for desc in timer.getIncludedDescription():
398                                 list.extend(['  <include where="description">', desc, '</include>\n'])
399                         for day in timer.getIncludedDays():
400                                 list.extend(['  <include where="dayofweek">', day, '</include>\n'])
401
402                         # Tags
403                         for tag in timer.tags:
404                                 list.extend(['  <tag>', str(tag), '</tag>\n'])
405
406                         # End of Timer
407                         list.append(' </timer>\n\n')
408
409                 # End of Configuration
410                 list.append('</autotimer>\n')
411
412                 # Save to Flash
413                 file = open(XML_CONFIG, 'w')
414                 file.writelines(list)
415
416                 file.close()
417
418         def parseEPG(self, simulateOnly = False):
419                 if NavigationInstance.instance is None:
420                         print "[AutoTimer] Navigation is not available, can't parse EPG"
421                         return (0, 0, 0, [])
422
423                 total = 0
424                 new = 0
425                 modified = 0
426                 timers = []
427
428                 self.readXml()
429
430                 # Save Recordings in a dict to speed things up a little
431                 # We include processed timers as we might search for duplicate descriptions
432                 recorddict = {}
433                 for timer in NavigationInstance.instance.RecordTimer.timer_list + NavigationInstance.instance.RecordTimer.processed_timers:
434                         if not recorddict.has_key(str(timer.service_ref)):
435                                 recorddict[str(timer.service_ref)] = [timer]
436                         else:
437                                 recorddict[str(timer.service_ref)].append(timer)
438
439                 # Iterate Timer
440                 for timer in self.getEnabledTimerList():
441                         # Search EPG, default to empty list
442                         ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
443
444                         for serviceref, eit in ret:
445                                 eserviceref = eServiceReference(serviceref)
446
447                                 evt = self.epgcache.lookupEventId(eserviceref, eit)
448                                 if not evt:
449                                         print "[AutoTimer] Could not create Event!"
450                                         continue
451
452                                 # Try to determine real service (we always choose the last one)
453                                 n = evt.getNumOfLinkageServices()
454                                 if n > 0:
455                                         i = evt.getLinkageService(eserviceref, n-1)
456                                         serviceref = i.toString()
457
458                                 # Gather Information
459                                 name = evt.getEventName()
460                                 description = evt.getShortDescription()
461                                 begin = evt.getBeginTime()
462                                 duration = evt.getDuration()
463                                 end = begin + duration
464
465                                 # If event starts in less than 60 seconds skip it
466                                 if begin < time() + 60:
467                                         continue
468
469                                 # Convert begin time
470                                 timestamp = localtime(begin)
471
472                                 # Update timer
473                                 timer.update(begin, timestamp)
474
475                                 # Check Duration, Timespan and Excludes
476                                 if timer.checkServices(serviceref) \
477                                         or timer.checkDuration(duration) \
478                                         or timer.checkTimespan(timestamp) \
479                                         or timer.checkFilter(name, description,
480                                                 evt.getExtendedDescription(), str(timestamp.tm_wday)):
481                                         continue
482
483                                 # Apply E2 Offset
484                                 begin -= config.recording.margin_before.value * 60
485                                 end += config.recording.margin_after.value * 60
486  
487                                 # Apply custom Offset
488                                 begin, end = timer.applyOffset(begin, end)
489
490                                 total += 1
491
492                                 # Append to timerlist and abort if simulating
493                                 timers.append((name, begin, end, serviceref, timer.name))
494                                 if simulateOnly:
495                                         continue
496
497                                 # Initialize
498                                 newEntry = None
499
500                                 # Check for double Timers
501                                 # We first check eit and if user wants us to guess event based on time
502                                 # we try this as backup. The allowed diff should be configurable though.
503                                 try:
504                                         for rtimer in recorddict.get(serviceref, []):
505                                                 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
506                                                         newEntry = rtimer
507
508                                                         # Abort if we don't want to modify timers or timer is repeated
509                                                         if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
510                                                                 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
511
512                                                         try:
513                                                                 if newEntry.isAutoTimer:
514                                                                         print "[AutoTimer] Modifying existing AutoTimer!"
515                                                         except AttributeError, ae:
516                                                                 if config.plugins.autotimer.refresh.value != "all":
517                                                                         raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
518                                                                 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
519
520                                                         func = NavigationInstance.instance.RecordTimer.timeChanged
521                                                         modified += 1
522
523                                                         # Modify values saved in timer
524                                                         newEntry.name = name
525                                                         newEntry.description = description
526                                                         newEntry.begin = int(begin)
527                                                         newEntry.end = int(end)
528                                                         newEntry.service_ref = ServiceReference(serviceref)
529
530                                                         break
531                                                 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
532                                                         raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
533
534                                 except AutoTimerIgnoreTimerException, etite:
535                                         print etite
536                                         continue
537
538                                 # Event not yet in Timers
539                                 if newEntry is None:
540                                         if timer.checkCounter(timestamp):
541                                                 continue
542
543                                         new += 1
544
545                                         print "[AutoTimer] Adding an event."
546                                         newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
547                                         func = NavigationInstance.instance.RecordTimer.record
548
549                                         # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
550                                         newEntry.isAutoTimer = True
551
552                                 # Apply afterEvent
553                                 if timer.hasAfterEvent():
554                                         afterEvent = timer.getAfterEventTimespan(localtime(end))
555                                         if afterEvent is None:
556                                                 afterEvent = timer.getAfterEvent()
557                                         if afterEvent is not None:
558                                                 newEntry.afterEvent = afterEvent
559
560                                 newEntry.dirname = timer.destination
561                                 newEntry.justplay = timer.justplay
562                                 newEntry.tags = timer.tags # This needs my enhanced tag support patch to work
563  
564                                 # Do a sanity check, although it does not do much right now
565                                 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
566                                 if not timersanitycheck.check():
567                                         print "[Autotimer] Sanity check failed"
568                                 else:
569                                         print "[Autotimer] Sanity check passed"
570
571                                 # Either add to List or change time
572                                 func(newEntry)
573
574                 return (total, new, modified, timers)