2 from xml.dom.minidom import parse as minidom_parse
3 from Tools.XMLTools import stringToXML
4 from os import path as os_path
6 # Navigation (RecordTimer)
7 import NavigationInstance
10 from ServiceReference import ServiceReference
11 from RecordTimer import RecordTimerEntry, AFTEREVENT
12 from Components.TimerSanityCheck import TimerSanityCheck
15 from time import localtime, time
18 from enigma import eEPGCache, eServiceReference
21 from Components.config import config
24 from AutoTimerComponent import AutoTimerComponent
26 XML_CONFIG = "/etc/enigma2/autotimer.xml"
27 CURRENT_CONFIG_VERSION = "5"
29 def getValue(definitions, default):
33 # How many definitions are present
35 childNodes = definitions.childNodes
37 Len = len(definitions)
39 childNodes = definitions[Len-1].childNodes
43 # Iterate through nodes of last one
44 for node in childNodes:
45 # Append text if we have a text node
46 if node.nodeType == node.TEXT_NODE:
49 # Return stripped output or (if empty) default
50 return ret.strip() or default
52 def getTimeDiff(timer, begin, end):
53 if begin <= timer.begin <= end:
54 return end - timer.begin
55 elif timer.begin <= begin <= timer.end:
56 return timer.end - begin
59 class AutoTimerIgnoreTimerException(Exception):
60 def __init__(self, cause):
64 return "[AutoTimer] " + str(self.cause)
67 return str(type(self))
70 """Read and save xml configuration, query EPGCache"""
74 self.epgcache = eEPGCache.getInstance()
79 self.uniqueTimerId = 0
80 self.defaultTimer = AutoTimerComponent(
88 # Abort if no config found
89 if not os_path.exists(XML_CONFIG):
92 # Parse if mtime differs from whats saved
93 mtime = os_path.getmtime(XML_CONFIG)
94 if mtime == self.configMtime:
95 print "[AutoTimer] No changes in configuration, won't parse"
99 self.configMtime = mtime
102 dom = minidom_parse(XML_CONFIG)
104 # Empty out timers and reset Ids
106 self.uniqueTimerId = 0
107 self.defaultTimer = AutoTimerComponent(
115 for configuration in dom.getElementsByTagName("autotimer"):
116 # Parse old configuration files
117 if configuration.getAttribute("version") != CURRENT_CONFIG_VERSION:
118 from OldConfigurationParser import parseConfig
119 parseConfig(configuration, self.timers, configuration.getAttribute("version"), self.uniqueTimerId)
120 if not self.uniqueTimerId:
121 self.uniqueTimerId = len(self.timers)
123 # Read in defaults for a new timer
124 for defaults in configuration.getElementsByTagName("defaults"):
126 start = defaults.getAttribute("from")
127 end = defaults.getAttribute("to")
129 start = [int(x) for x in start.split(':')]
130 end = [int(x) for x in end.split(':')]
131 self.defaultTimer.timespan = (start, end)
133 # Read out max length
134 maxduration = defaults.getAttribute("maxduration") or None
136 self.defaultTimer.maxduration = int(maxlen)*60
138 # Read out recording path
139 self.defaultTimer.destination = defaults.getAttribute("location").encode("UTF-8") or None
142 offset = defaults.getAttribute("offset") or None
144 offset = offset.split(",")
146 before = after = int(offset[0] or 0) * 60
148 before = int(offset[0] or 0) * 60
149 after = int(offset[1] or 0) * 60
150 self.defaultTimer.offset = (before, after)
153 self.defaultTimer.matchCount = int(defaults.getAttribute("counter") or '0')
154 self.defaultTimer.matchFormatString = defaults.getAttribute("counterFormat")
157 justplay = int(defaults.getAttribute("justplay") or '0')
159 # Read out avoidDuplicateDescription
160 self.defaultTimer.avoidDuplicateDescription = bool(defaults.getAttribute("avoidDuplicateDescription") or False)
162 # Read out allowed services
163 servicelist = self.defaultTimer.services
164 for service in defaults.getElementsByTagName("serviceref"):
165 value = getValue(service, None)
167 # strip all after last :
168 pos = value.rfind(':')
170 value = value[:pos+1]
172 servicelist.append(value)
173 self.defaultTimer.services = servicelist # We might have got a dummy list above
175 # Read out allowed bouquets
176 bouquets = self.defaultTimer.bouquets
177 for bouquet in defaults.getElementsByTagName("bouquet"):
178 value = getValue(bouquet, None)
180 bouquets.append(value)
181 self.defaultTimer.bouquets = bouquets
183 # Read out afterevent
184 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
185 afterevent = self.defaultTimer.afterevent
186 for element in defaults.getElementsByTagName("afterevent"):
187 value = getValue(element, None)
191 start = element.getAttribute("from")
192 end = element.getAttribute("to")
194 start = [int(x) for x in start.split(':')]
195 end = [int(x) for x in end.split(':')]
196 afterevent.append((value, (start, end)))
198 afterevent.append((value, None))
200 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
202 self.defaultTimer.afterevent = afterevent
205 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
206 excludes = (self.defaultTimer.getExcludedTitle(), self.defaultTimer.getExcludedShort(), self.defaultTimer.getExcludedDescription(), self.defaultTimer.getExcludedDays())
207 for exclude in defaults.getElementsByTagName("exclude"):
208 where = exclude.getAttribute("where")
209 value = getValue(exclude, None)
210 if not (value and where):
214 excludes[idx[where]].append(value.encode("UTF-8"))
217 self.defaultTimer.excludes = excludes
219 # Read out includes (use same idx)
220 includes = (self.defaultTimer.getIncludedTitle(), self.defaultTimer.getIncludedShort(), self.defaultTimer.getIncludedDescription(), self.defaultTimer.getIncludedDays())
221 for include in defaults.getElementsByTagName("include"):
222 where = include.getAttribute("where")
223 value = getValue(include, None)
224 if not (value and where):
228 includes[idx[where]].append(value.encode("UTF-8"))
231 self.defaultTimer.includes = includes
233 # Read out recording tags (needs my enhanced tag support patch)
234 tags = self.defaultTimer.tags
235 for tag in defaults.getElementsByTagName("tag"):
236 value = getValue(tag, None)
240 tags.append(value.encode("UTF-8"))
243 for timer in configuration.getElementsByTagName("timer"):
244 # Increment uniqueTimerId
245 self.uniqueTimerId += 1
248 match = timer.getAttribute("match").encode("UTF-8")
250 print '[AutoTimer] Erroneous config is missing attribute "match", skipping entry'
254 name = timer.getAttribute("name").encode("UTF-8")
256 print '[AutoTimer] Timer is missing attribute "name", defaulting to match'
260 enabled = timer.getAttribute("enabled") or "yes"
263 elif enabled == "yes":
266 print '[AutoTimer] Erroneous config contains invalid value for "enabled":', enabled,', disabling'
270 start = timer.getAttribute("from")
271 end = timer.getAttribute("to")
273 start = [int(x) for x in start.split(':')]
274 end = [int(x) for x in end.split(':')]
275 timetuple = (start, end)
279 # Read out max length
280 maxlen = timer.getAttribute("maxduration") or None
282 maxlen = int(maxlen)*60
284 # Read out recording path
285 destination = timer.getAttribute("location").encode("UTF-8") or None
288 offset = timer.getAttribute("offset") or None
290 offset = offset.split(",")
292 before = after = int(offset[0] or 0) * 60
294 before = int(offset[0] or 0) * 60
295 after = int(offset[1] or 0) * 60
296 offset = (before, after)
299 counter = int(timer.getAttribute("counter") or '0')
300 counterLeft = int(timer.getAttribute("left") or counter)
301 counterLimit = timer.getAttribute("lastActivation")
302 counterFormat = timer.getAttribute("counterFormat")
303 lastBegin = int(timer.getAttribute("lastBegin") or 0)
306 justplay = int(timer.getAttribute("justplay") or '0')
308 # Read out avoidDuplicateDescription
309 avoidDuplicateDescription = bool(timer.getAttribute("avoidDuplicateDescription") or False)
311 # Read out allowed services
313 for service in timer.getElementsByTagName("serviceref"):
314 value = getValue(service, None)
316 # strip all after last :
317 pos = value.rfind(':')
319 value = value[:pos+1]
321 servicelist.append(value)
323 # Read out allowed bouquets
325 for bouquet in timer.getElementsByTagName("bouquet"):
326 value = getValue(bouquet, None)
328 bouquets.append(value)
330 # Read out afterevent
331 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
333 for element in timer.getElementsByTagName("afterevent"):
334 value = getValue(element, None)
338 start = element.getAttribute("from")
339 end = element.getAttribute("to")
341 start = [int(x) for x in start.split(':')]
342 end = [int(x) for x in end.split(':')]
343 afterevent.append((value, (start, end)))
345 afterevent.append((value, None))
347 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
351 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
352 excludes = ([], [], [], [])
353 for exclude in timer.getElementsByTagName("exclude"):
354 where = exclude.getAttribute("where")
355 value = getValue(exclude, None)
356 if not (value and where):
360 excludes[idx[where]].append(value.encode("UTF-8"))
364 # Read out includes (use same idx)
365 includes = ([], [], [], [])
366 for include in timer.getElementsByTagName("include"):
367 where = include.getAttribute("where")
368 value = getValue(include, None)
369 if not (value and where):
373 includes[idx[where]].append(value.encode("UTF-8"))
377 # Read out recording tags (needs my enhanced tag support patch)
379 for tag in timer.getElementsByTagName("tag"):
380 value = getValue(tag, None)
384 tags.append(value.encode("UTF-8"))
386 # Finally append tuple
387 self.timers.append(AutoTimerComponent(
392 timespan = timetuple,
393 services = servicelist,
395 afterevent = afterevent,
398 maxduration = maxlen,
399 destination = destination,
400 matchCount = counter,
401 matchLeft = counterLeft,
402 matchLimit = counterLimit,
403 matchFormatString = counterFormat,
404 lastBegin = lastBegin,
406 avoidDuplicateDescription = avoidDuplicateDescription,
411 def getTimerList(self):
414 def getEnabledTimerList(self):
415 return [x for x in self.timers if x.enabled]
417 def getTupleTimerList(self):
418 return [(x,) for x in self.timers]
420 def getUniqueId(self):
421 self.uniqueTimerId += 1
422 return self.uniqueTimerId
424 def add(self, timer):
425 self.timers.append(timer)
427 def set(self, timer):
429 for stimer in self.timers:
431 self.timers[idx] = timer
434 self.timers.append(timer)
436 def remove(self, uniqueId):
438 for timer in self.timers:
439 if timer.id == uniqueId:
445 # Generate List in RAM
446 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
448 # XXX: we might want to make sure that we don't save empty default here
449 list.extend([' <defaults'])
452 if self.defaultTimer.hasTimespan():
453 list.extend([' from="', self.defaultTimer.getTimespanBegin(), '" to="', self.defaultTimer.getTimespanEnd(), '"'])
456 if self.defaultTimer.hasDuration():
457 list.extend([' maxduration="', str(self.defaultTimer.getDuration()), '"'])
460 if self.defaultTimer.hasDestination():
461 list.extend([' location="', stringToXML(self.defaultTimer.destination), '"'])
464 if self.defaultTimer.hasOffset():
465 if self.defaultTimer.isOffsetEqual():
466 list.extend([' offset="', str(self.defaultTimer.getOffsetBegin()), '"'])
468 list.extend([' offset="', str(self.defaultTimer.getOffsetBegin()), ',', str(self.defaultTimer.getOffsetEnd()), '"'])
471 if self.defaultTimer.hasCounter():
472 list.extend([' counter="', str(self.defaultTimer.getCounter()), '"'])
473 if self.defaultTimer.hasCounterFormatString():
474 list.extend([' counterFormat="', str(self.defaultTimer.getCounterFormatString()), '"'])
476 # Duplicate Description
477 if self.defaultTimer.getAvoidDuplicateDescription():
478 list.append(' avoidDuplicateDescription="1" ')
480 # Only display justplay if true
481 if self.defaultTimer.justplay:
482 list.extend([' justplay="', str(self.defaultTimer.getJustplay()), '"'])
484 # Close still opened defaults tag
488 for serviceref in self.defaultTimer.getServices():
489 list.extend([' <serviceref>', serviceref, '</serviceref>'])
490 ref = ServiceReference(str(serviceref))
491 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
494 for bouquet in self.defaultTimer.getBouquets():
495 list.extend([' <bouquet>', str(bouquet), '</bouquet>'])
496 ref = ServiceReference(str(bouquet))
497 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
500 if self.defaultTimer.hasAfterEvent():
501 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
502 for afterevent in self.defaultTimer.getCompleteAfterEvent():
503 action, timespan = afterevent
504 list.append(' <afterevent')
505 if timespan[0] is not None:
506 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
507 list.extend(['>', idx[action], '</afterevent>\n'])
510 for title in self.defaultTimer.getExcludedTitle():
511 list.extend([' <exclude where="title">', stringToXML(title), '</exclude>\n'])
512 for short in self.defaultTimer.getExcludedShort():
513 list.extend([' <exclude where="shortdescription">', stringToXML(short), '</exclude>\n'])
514 for desc in self.defaultTimer.getExcludedDescription():
515 list.extend([' <exclude where="description">', stringToXML(desc), '</exclude>\n'])
516 for day in self.defaultTimer.getExcludedDays():
517 list.extend([' <exclude where="dayofweek">', stringToXML(day), '</exclude>\n'])
520 for title in self.defaultTimer.getIncludedTitle():
521 list.extend([' <include where="title">', stringToXML(title), '</include>\n'])
522 for short in self.defaultTimer.getIncludedShort():
523 list.extend([' <include where="shortdescription">', stringToXML(short), '</include>\n'])
524 for desc in self.defaultTimer.getIncludedDescription():
525 list.extend([' <include where="description">', stringToXML(desc), '</include>\n'])
526 for day in self.defaultTimer.getIncludedDays():
527 list.extend([' <include where="dayofweek">', stringToXML(day), '</include>\n'])
530 for tag in self.defaultTimer.tags:
531 list.extend([' <tag>', stringToXML(tag), '</tag>\n'])
534 list.append(' </defaults>\n\n')
537 for timer in self.timers:
538 # Common attributes (match, enabled)
539 list.extend([' <timer name="', stringToXML(timer.name), '" match="', stringToXML(timer.match), '" enabled="', timer.getEnabled(), '"'])
542 if timer.hasTimespan():
543 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
546 if timer.hasDuration():
547 list.extend([' maxduration="', str(timer.getDuration()), '"'])
550 if timer.hasDestination():
551 list.extend([' location="', stringToXML(timer.destination), '"'])
554 if timer.hasOffset():
555 if timer.isOffsetEqual():
556 list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
558 list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
561 if timer.hasCounter():
562 list.extend([' lastBegin="', str(timer.getLastBegin()), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
563 if timer.hasCounterFormatString():
564 list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
565 list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
567 # Duplicate Description
568 if timer.getAvoidDuplicateDescription():
569 list.append(' avoidDuplicateDescription="1" ')
571 # Only display justplay if true
573 list.extend([' justplay="', str(timer.getJustplay()), '"'])
575 # Close still opened timer tag
579 for serviceref in timer.getServices():
580 list.extend([' <serviceref>', serviceref, '</serviceref>'])
581 ref = ServiceReference(str(serviceref))
582 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
585 for bouquet in timer.getBouquets():
586 list.extend([' <bouquet>', str(bouquet), '</bouquet>'])
587 ref = ServiceReference(str(bouquet))
588 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
591 if timer.hasAfterEvent():
592 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
593 for afterevent in timer.getCompleteAfterEvent():
594 action, timespan = afterevent
595 list.append(' <afterevent')
596 if timespan[0] is not None:
597 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
598 list.extend(['>', idx[action], '</afterevent>\n'])
601 for title in timer.getExcludedTitle():
602 list.extend([' <exclude where="title">', stringToXML(title), '</exclude>\n'])
603 for short in timer.getExcludedShort():
604 list.extend([' <exclude where="shortdescription">', stringToXML(short), '</exclude>\n'])
605 for desc in timer.getExcludedDescription():
606 list.extend([' <exclude where="description">', stringToXML(desc), '</exclude>\n'])
607 for day in timer.getExcludedDays():
608 list.extend([' <exclude where="dayofweek">', stringToXML(day), '</exclude>\n'])
611 for title in timer.getIncludedTitle():
612 list.extend([' <include where="title">', stringToXML(title), '</include>\n'])
613 for short in timer.getIncludedShort():
614 list.extend([' <include where="shortdescription">', stringToXML(short), '</include>\n'])
615 for desc in timer.getIncludedDescription():
616 list.extend([' <include where="description">', stringToXML(desc), '</include>\n'])
617 for day in timer.getIncludedDays():
618 list.extend([' <include where="dayofweek">', stringToXML(day), '</include>\n'])
621 for tag in timer.tags:
622 list.extend([' <tag>', stringToXML(tag), '</tag>\n'])
625 list.append(' </timer>\n\n')
627 # End of Configuration
628 list.append('</autotimer>\n')
631 file = open(XML_CONFIG, 'w')
632 file.writelines(list)
636 def parseEPG(self, simulateOnly = False):
637 if NavigationInstance.instance is None:
638 print "[AutoTimer] Navigation is not available, can't parse EPG"
648 # Save Recordings in a dict to speed things up a little
649 # We include processed timers as we might search for duplicate descriptions
651 for timer in NavigationInstance.instance.RecordTimer.timer_list + NavigationInstance.instance.RecordTimer.processed_timers:
652 if not recorddict.has_key(str(timer.service_ref)):
653 recorddict[str(timer.service_ref)] = [timer]
655 recorddict[str(timer.service_ref)].append(timer)
658 for timer in self.getEnabledTimerList():
659 # Search EPG, default to empty list
660 ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
662 for serviceref, eit in ret:
663 eserviceref = eServiceReference(serviceref)
665 evt = self.epgcache.lookupEventId(eserviceref, eit)
667 print "[AutoTimer] Could not create Event!"
670 # Try to determine real service (we always choose the last one)
671 n = evt.getNumOfLinkageServices()
673 i = evt.getLinkageService(eserviceref, n-1)
674 serviceref = i.toString()
677 name = evt.getEventName()
678 description = evt.getShortDescription()
679 begin = evt.getBeginTime()
680 duration = evt.getDuration()
681 end = begin + duration
683 # If event starts in less than 60 seconds skip it
684 if begin < time() + 60:
688 timestamp = localtime(begin)
691 timer.update(begin, timestamp)
693 # Check Duration, Timespan and Excludes
694 if timer.checkServices(serviceref) \
695 or timer.checkDuration(duration) \
696 or timer.checkTimespan(timestamp) \
697 or timer.checkFilter(name, description,
698 evt.getExtendedDescription(), str(timestamp.tm_wday)):
701 if timer.hasOffset():
702 # Apply custom Offset
703 begin, end = timer.applyOffset(begin, end)
706 begin -= config.recording.margin_before.value * 60
707 end += config.recording.margin_after.value * 60
712 # Append to timerlist and abort if simulating
713 timers.append((name, begin, end, serviceref, timer.name))
720 # Check for double Timers
721 # We first check eit and if user wants us to guess event based on time
722 # we try this as backup. The allowed diff should be configurable though.
724 for rtimer in recorddict.get(serviceref, []):
725 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
728 # Abort if we don't want to modify timers or timer is repeated
729 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
730 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
733 if newEntry.isAutoTimer:
734 print "[AutoTimer] Modifying existing AutoTimer!"
735 except AttributeError, ae:
736 if config.plugins.autotimer.refresh.value != "all":
737 raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
738 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
740 func = NavigationInstance.instance.RecordTimer.timeChanged
743 # Modify values saved in timer
745 newEntry.description = description
746 newEntry.begin = int(begin)
747 newEntry.end = int(end)
748 newEntry.service_ref = ServiceReference(serviceref)
751 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
752 raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
754 except AutoTimerIgnoreTimerException, etite:
758 # Event not yet in Timers
760 if timer.checkCounter(timestamp):
765 print "[AutoTimer] Adding an event."
766 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
767 func = NavigationInstance.instance.RecordTimer.record
769 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
770 newEntry.isAutoTimer = True
772 if not recorddict.has_key(serviceref):
773 recorddict[serviceref] = [newEntry]
775 recorddict[serviceref].append(newEntry)
778 if timer.hasAfterEvent():
779 afterEvent = timer.getAfterEventTimespan(localtime(end))
780 if afterEvent is None:
781 afterEvent = timer.getAfterEvent()
782 if afterEvent is not None:
783 newEntry.afterEvent = afterEvent
785 newEntry.dirname = timer.destination
786 newEntry.justplay = timer.justplay
787 newEntry.tags = timer.tags # This needs my enhanced tag support patch to work
789 # Do a sanity check, although it does not do much right now
790 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
791 if not timersanitycheck.check():
792 print "[Autotimer] Sanity check failed"
794 print "[Autotimer] Sanity check passed"
796 # Either add to List or change time
799 return (total, new, modified, timers)