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