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