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
82 # Abort if no config found
83 if not os_path.exists(XML_CONFIG):
86 # Parse if mtime differs from whats saved
87 mtime = os_path.getmtime(XML_CONFIG)
88 if mtime == self.configMtime:
89 print "[AutoTimer] No changes in configuration, won't parse"
93 self.configMtime = mtime
96 dom = minidom_parse(XML_CONFIG)
98 # Empty out timers and reset Ids
100 self.uniqueTimerId = 0
101 self.defaultTimer = AutoTimerComponent(
109 for configuration in dom.getElementsByTagName("autotimer"):
110 # Parse old configuration files
111 if configuration.getAttribute("version") != CURRENT_CONFIG_VERSION:
112 from OldConfigurationParser import parseConfig
113 parseConfig(configuration, self.timers, configuration.getAttribute("version"), self.uniqueTimerId)
114 if not self.uniqueTimerId:
115 self.uniqueTimerId = len(self.timers)
117 # Read in defaults for a new timer
118 for defaults in configuration.getElementsByTagName("defaults"):
120 start = defaults.getAttribute("from")
121 end = defaults.getAttribute("to")
123 start = [int(x) for x in start.split(':')]
124 end = [int(x) for x in end.split(':')]
125 self.defaultTimer.timespan = (start, end)
127 # Read out max length
128 maxduration = defaults.getAttribute("maxduration") or None
130 self.defaultTimer.maxduration = int(maxlen)*60
132 # Read out recording path
133 self.defaultTimer.destination = defaults.getAttribute("location").encode("UTF-8") or None
136 offset = defaults.getAttribute("offset") or None
138 offset = offset.split(",")
140 before = after = int(offset[0] or 0) * 60
142 before = int(offset[0] or 0) * 60
143 after = int(offset[1] or 0) * 60
144 self.defaultTimer.offset = (before, after)
147 self.defaultTimer.matchCount = int(defaults.getAttribute("counter") or '0')
148 self.defaultTimer.matchFormatString = defaults.getAttribute("counterFormat")
151 justplay = int(defaults.getAttribute("justplay") or '0')
153 # Read out avoidDuplicateDescription
154 self.defaultTimer.avoidDuplicateDescription = bool(defaults.getAttribute("avoidDuplicateDescription") or False)
156 # Read out allowed services
157 servicelist = self.defaultTimer.services
158 for service in defaults.getElementsByTagName("serviceref"):
159 value = getValue(service, None)
161 # strip all after last :
162 pos = value.rfind(':')
164 value = value[:pos+1]
166 servicelist.append(value)
167 self.defaultTimer.services = servicelist # We might have got a dummy list above
169 # Read out allowed bouquets
170 bouquets = self.defaultTimer.bouquets
171 for bouquet in defaults.getElementsByTagName("bouquet"):
172 value = getValue(bouquet, None)
174 bouquets.append(value)
175 self.defaultTimer.bouquets = bouquets
177 # Read out afterevent
178 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
179 afterevent = self.defaultTimer.afterevent
180 for element in defaults.getElementsByTagName("afterevent"):
181 value = getValue(element, None)
185 start = element.getAttribute("from")
186 end = element.getAttribute("to")
188 start = [int(x) for x in start.split(':')]
189 end = [int(x) for x in end.split(':')]
190 afterevent.append((value, (start, end)))
192 afterevent.append((value, None))
194 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
196 self.defaultTimer.afterevent = afterevent
199 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
200 excludes = (self.defaultTimer.getExcludedTitle(), self.defaultTimer.getExcludedShort(), self.defaultTimer.getExcludedDescription(), self.defaultTimer.getExcludedDays())
201 for exclude in defaults.getElementsByTagName("exclude"):
202 where = exclude.getAttribute("where")
203 value = getValue(exclude, None)
204 if not (value and where):
208 excludes[idx[where]].append(value.encode("UTF-8"))
211 self.defaultTimer.excludes = excludes
213 # Read out includes (use same idx)
214 includes = (self.defaultTimer.getIncludedTitle(), self.defaultTimer.getIncludedShort(), self.defaultTimer.getIncludedDescription(), self.defaultTimer.getIncludedDays())
215 for include in defaults.getElementsByTagName("include"):
216 where = include.getAttribute("where")
217 value = getValue(include, None)
218 if not (value and where):
222 includes[idx[where]].append(value.encode("UTF-8"))
225 self.defaultTimer.includes = includes
227 # Read out recording tags (needs my enhanced tag support patch)
228 tags = self.defaultTimer.tags
229 for tag in defaults.getElementsByTagName("tag"):
230 value = getValue(tag, None)
234 tags.append(value.encode("UTF-8"))
237 for timer in configuration.getElementsByTagName("timer"):
238 # Increment uniqueTimerId
239 self.uniqueTimerId += 1
242 match = timer.getAttribute("match").encode("UTF-8")
244 print '[AutoTimer] Erroneous config is missing attribute "match", skipping entry'
248 name = timer.getAttribute("name").encode("UTF-8")
250 print '[AutoTimer] Timer is missing attribute "name", defaulting to match'
254 enabled = timer.getAttribute("enabled") or "yes"
257 elif enabled == "yes":
260 print '[AutoTimer] Erroneous config contains invalid value for "enabled":', enabled,', disabling'
264 start = timer.getAttribute("from")
265 end = timer.getAttribute("to")
267 start = [int(x) for x in start.split(':')]
268 end = [int(x) for x in end.split(':')]
269 timetuple = (start, end)
273 # Read out max length
274 maxlen = timer.getAttribute("maxduration") or None
276 maxlen = int(maxlen)*60
278 # Read out recording path
279 destination = timer.getAttribute("location").encode("UTF-8") or None
282 offset = timer.getAttribute("offset") or None
284 offset = offset.split(",")
286 before = after = int(offset[0] or 0) * 60
288 before = int(offset[0] or 0) * 60
289 after = int(offset[1] or 0) * 60
290 offset = (before, after)
293 counter = int(timer.getAttribute("counter") or '0')
294 counterLeft = int(timer.getAttribute("left") or counter)
295 counterLimit = timer.getAttribute("lastActivation")
296 counterFormat = timer.getAttribute("counterFormat")
297 lastBegin = int(timer.getAttribute("lastBegin") or 0)
300 justplay = int(timer.getAttribute("justplay") or '0')
302 # Read out avoidDuplicateDescription
303 avoidDuplicateDescription = bool(timer.getAttribute("avoidDuplicateDescription") or False)
305 # Read out allowed services
307 for service in timer.getElementsByTagName("serviceref"):
308 value = getValue(service, None)
310 # strip all after last :
311 pos = value.rfind(':')
313 value = value[:pos+1]
315 servicelist.append(value)
317 # Read out allowed bouquets
319 for bouquet in timer.getElementsByTagName("bouquet"):
320 value = getValue(bouquet, None)
322 bouquets.append(value)
324 # Read out afterevent
325 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
327 for element in timer.getElementsByTagName("afterevent"):
328 value = getValue(element, None)
332 start = element.getAttribute("from")
333 end = element.getAttribute("to")
335 start = [int(x) for x in start.split(':')]
336 end = [int(x) for x in end.split(':')]
337 afterevent.append((value, (start, end)))
339 afterevent.append((value, None))
341 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
345 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
346 excludes = ([], [], [], [])
347 for exclude in timer.getElementsByTagName("exclude"):
348 where = exclude.getAttribute("where")
349 value = getValue(exclude, None)
350 if not (value and where):
354 excludes[idx[where]].append(value.encode("UTF-8"))
358 # Read out includes (use same idx)
359 includes = ([], [], [], [])
360 for include in timer.getElementsByTagName("include"):
361 where = include.getAttribute("where")
362 value = getValue(include, None)
363 if not (value and where):
367 includes[idx[where]].append(value.encode("UTF-8"))
371 # Read out recording tags (needs my enhanced tag support patch)
373 for tag in timer.getElementsByTagName("tag"):
374 value = getValue(tag, None)
378 tags.append(value.encode("UTF-8"))
380 # Finally append tuple
381 self.timers.append(AutoTimerComponent(
386 timespan = timetuple,
387 services = servicelist,
389 afterevent = afterevent,
392 maxduration = maxlen,
393 destination = destination,
394 matchCount = counter,
395 matchLeft = counterLeft,
396 matchLimit = counterLimit,
397 matchFormatString = counterFormat,
398 lastBegin = lastBegin,
400 avoidDuplicateDescription = avoidDuplicateDescription,
405 def getTimerList(self):
408 def getEnabledTimerList(self):
409 return [x for x in self.timers if x.enabled]
411 def getTupleTimerList(self):
412 return [(x,) for x in self.timers]
414 def getUniqueId(self):
415 self.uniqueTimerId += 1
416 return self.uniqueTimerId
418 def add(self, timer):
419 self.timers.append(timer)
421 def set(self, timer):
423 for stimer in self.timers:
425 self.timers[idx] = timer
428 self.timers.append(timer)
430 def remove(self, uniqueId):
432 for timer in self.timers:
433 if timer.id == uniqueId:
439 # Generate List in RAM
440 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
442 # XXX: we might want to make sure that we don't save empty default here
443 list.extend([' <defaults'])
446 if self.defaultTimer.hasTimespan():
447 list.extend([' from="', self.defaultTimer.getTimespanBegin(), '" to="', self.defaultTimer.getTimespanEnd(), '"'])
450 if self.defaultTimer.hasDuration():
451 list.extend([' maxduration="', str(self.defaultTimer.getDuration()), '"'])
454 if self.defaultTimer.hasDestination():
455 list.extend([' destination="', stringToXML(self.defaultTimer.destination), '"'])
458 if self.defaultTimer.hasOffset():
459 if self.defaultTimer.isOffsetEqual():
460 list.extend([' offset="', str(self.defaultTimer.getOffsetBegin()), '"'])
462 list.extend([' offset="', str(self.defaultTimer.getOffsetBegin()), ',', str(self.defaultTimer.getOffsetEnd()), '"'])
465 if self.defaultTimer.hasCounter():
466 list.extend([' counter="', str(self.defaultTimer.getCounter()), '"'])
467 if self.defaultTimer.hasCounterFormatString():
468 list.extend([' counterFormat="', str(self.defaultTimer.getCounterFormatString()), '"'])
470 # Duplicate Description
471 if self.defaultTimer.getAvoidDuplicateDescription():
472 list.append(' avoidDuplicateDescription="1" ')
474 # Only display justplay if true
475 if self.defaultTimer.justplay:
476 list.extend([' justplay="', str(self.defaultTimer.getJustplay()), '"'])
478 # Close still opened defaults tag
482 for serviceref in self.defaultTimer.getServices():
483 list.extend([' <serviceref>', serviceref, '</serviceref>'])
484 ref = ServiceReference(str(serviceref))
485 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
488 for bouquet in self.defaultTimer.getBouquets():
489 list.extend([' <bouquet>', str(bouquet), '</bouquet>'])
490 ref = ServiceReference(str(bouquet))
491 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
494 if self.defaultTimer.hasAfterEvent():
495 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
496 for afterevent in self.defaultTimer.getCompleteAfterEvent():
497 action, timespan = afterevent
498 list.append(' <afterevent')
499 if timespan[0] is not None:
500 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
501 list.extend(['>', idx[action], '</afterevent>\n'])
504 for title in self.defaultTimer.getExcludedTitle():
505 list.extend([' <exclude where="title">', stringToXML(title), '</exclude>\n'])
506 for short in self.defaultTimer.getExcludedShort():
507 list.extend([' <exclude where="shortdescription">', stringToXML(short), '</exclude>\n'])
508 for desc in self.defaultTimer.getExcludedDescription():
509 list.extend([' <exclude where="description">', stringToXML(desc), '</exclude>\n'])
510 for day in self.defaultTimer.getExcludedDays():
511 list.extend([' <exclude where="dayofweek">', stringToXML(day), '</exclude>\n'])
514 for title in self.defaultTimer.getIncludedTitle():
515 list.extend([' <include where="title">', stringToXML(title), '</include>\n'])
516 for short in self.defaultTimer.getIncludedShort():
517 list.extend([' <include where="shortdescription">', stringToXML(short), '</include>\n'])
518 for desc in self.defaultTimer.getIncludedDescription():
519 list.extend([' <include where="description">', stringToXML(desc), '</include>\n'])
520 for day in self.defaultTimer.getIncludedDays():
521 list.extend([' <include where="dayofweek">', stringToXML(day), '</include>\n'])
524 for tag in self.defaultTimer.tags:
525 list.extend([' <tag>', stringToXML(tag), '</tag>\n'])
528 list.append(' </defaults>\n\n')
531 for timer in self.timers:
532 # Common attributes (match, enabled)
533 list.extend([' <timer name="', stringToXML(timer.name), '" match="', stringToXML(timer.match), '" enabled="', timer.getEnabled(), '"'])
536 if timer.hasTimespan():
537 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
540 if timer.hasDuration():
541 list.extend([' maxduration="', str(timer.getDuration()), '"'])
544 if timer.hasDestination():
545 list.extend([' destination="', stringToXML(timer.destination), '"'])
548 if timer.hasOffset():
549 if timer.isOffsetEqual():
550 list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
552 list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
555 if timer.hasCounter():
556 list.extend([' lastBegin="', str(timer.getLastBegin()), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
557 if timer.hasCounterFormatString():
558 list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
559 list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
561 # Duplicate Description
562 if timer.getAvoidDuplicateDescription():
563 list.append(' avoidDuplicateDescription="1" ')
565 # Only display justplay if true
567 list.extend([' justplay="', str(timer.getJustplay()), '"'])
569 # Close still opened timer tag
573 for serviceref in timer.getServices():
574 list.extend([' <serviceref>', serviceref, '</serviceref>'])
575 ref = ServiceReference(str(serviceref))
576 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
579 for bouquet in timer.getBouquets():
580 list.extend([' <bouquet>', str(bouquet), '</bouquet>'])
581 ref = ServiceReference(str(bouquet))
582 list.extend([' <!-- ', stringToXML(ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '')), ' -->\n'])
585 if timer.hasAfterEvent():
586 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
587 for afterevent in timer.getCompleteAfterEvent():
588 action, timespan = afterevent
589 list.append(' <afterevent')
590 if timespan[0] is not None:
591 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
592 list.extend(['>', idx[action], '</afterevent>\n'])
595 for title in timer.getExcludedTitle():
596 list.extend([' <exclude where="title">', stringToXML(title), '</exclude>\n'])
597 for short in timer.getExcludedShort():
598 list.extend([' <exclude where="shortdescription">', stringToXML(short), '</exclude>\n'])
599 for desc in timer.getExcludedDescription():
600 list.extend([' <exclude where="description">', stringToXML(desc), '</exclude>\n'])
601 for day in timer.getExcludedDays():
602 list.extend([' <exclude where="dayofweek">', stringToXML(day), '</exclude>\n'])
605 for title in timer.getIncludedTitle():
606 list.extend([' <include where="title">', stringToXML(title), '</include>\n'])
607 for short in timer.getIncludedShort():
608 list.extend([' <include where="shortdescription">', stringToXML(short), '</include>\n'])
609 for desc in timer.getIncludedDescription():
610 list.extend([' <include where="description">', stringToXML(desc), '</include>\n'])
611 for day in timer.getIncludedDays():
612 list.extend([' <include where="dayofweek">', stringToXML(day), '</include>\n'])
615 for tag in timer.tags:
616 list.extend([' <tag>', stringToXML(tag), '</tag>\n'])
619 list.append(' </timer>\n\n')
621 # End of Configuration
622 list.append('</autotimer>\n')
625 file = open(XML_CONFIG, 'w')
626 file.writelines(list)
630 def parseEPG(self, simulateOnly = False):
631 if NavigationInstance.instance is None:
632 print "[AutoTimer] Navigation is not available, can't parse EPG"
642 # Save Recordings in a dict to speed things up a little
643 # We include processed timers as we might search for duplicate descriptions
645 for timer in NavigationInstance.instance.RecordTimer.timer_list + NavigationInstance.instance.RecordTimer.processed_timers:
646 if not recorddict.has_key(str(timer.service_ref)):
647 recorddict[str(timer.service_ref)] = [timer]
649 recorddict[str(timer.service_ref)].append(timer)
652 for timer in self.getEnabledTimerList():
653 # Search EPG, default to empty list
654 ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
656 for serviceref, eit in ret:
657 eserviceref = eServiceReference(serviceref)
659 evt = self.epgcache.lookupEventId(eserviceref, eit)
661 print "[AutoTimer] Could not create Event!"
664 # Try to determine real service (we always choose the last one)
665 n = evt.getNumOfLinkageServices()
667 i = evt.getLinkageService(eserviceref, n-1)
668 serviceref = i.toString()
671 name = evt.getEventName()
672 description = evt.getShortDescription()
673 begin = evt.getBeginTime()
674 duration = evt.getDuration()
675 end = begin + duration
677 # If event starts in less than 60 seconds skip it
678 if begin < time() + 60:
682 timestamp = localtime(begin)
685 timer.update(begin, timestamp)
687 # Check Duration, Timespan and Excludes
688 if timer.checkServices(serviceref) \
689 or timer.checkDuration(duration) \
690 or timer.checkTimespan(timestamp) \
691 or timer.checkFilter(name, description,
692 evt.getExtendedDescription(), str(timestamp.tm_wday)):
696 begin -= config.recording.margin_before.value * 60
697 end += config.recording.margin_after.value * 60
699 # Apply custom Offset
700 begin, end = timer.applyOffset(begin, end)
704 # Append to timerlist and abort if simulating
705 timers.append((name, begin, end, serviceref, timer.name))
712 # Check for double Timers
713 # We first check eit and if user wants us to guess event based on time
714 # we try this as backup. The allowed diff should be configurable though.
716 for rtimer in recorddict.get(serviceref, []):
717 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
720 # Abort if we don't want to modify timers or timer is repeated
721 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
722 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
725 if newEntry.isAutoTimer:
726 print "[AutoTimer] Modifying existing AutoTimer!"
727 except AttributeError, ae:
728 if config.plugins.autotimer.refresh.value != "all":
729 raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
730 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
732 func = NavigationInstance.instance.RecordTimer.timeChanged
735 # Modify values saved in timer
737 newEntry.description = description
738 newEntry.begin = int(begin)
739 newEntry.end = int(end)
740 newEntry.service_ref = ServiceReference(serviceref)
743 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
744 raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
746 except AutoTimerIgnoreTimerException, etite:
750 # Event not yet in Timers
752 if timer.checkCounter(timestamp):
757 print "[AutoTimer] Adding an event."
758 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
759 func = NavigationInstance.instance.RecordTimer.record
761 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
762 newEntry.isAutoTimer = True
765 if timer.hasAfterEvent():
766 afterEvent = timer.getAfterEventTimespan(localtime(end))
767 if afterEvent is None:
768 afterEvent = timer.getAfterEvent()
769 if afterEvent is not None:
770 newEntry.afterEvent = afterEvent
772 newEntry.dirname = timer.destination
773 newEntry.justplay = timer.justplay
774 newEntry.tags = timer.tags # This needs my enhanced tag support patch to work
776 # Do a sanity check, although it does not do much right now
777 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
778 if not timersanitycheck.check():
779 print "[Autotimer] Sanity check failed"
781 print "[Autotimer] Sanity check passed"
783 # Either add to List or change time
786 return (total, new, modified, timers)