2 from xml.dom.minidom import parse as minidom_parse
3 from os import path as os_path
5 # Navigation (RecordTimer)
6 import NavigationInstance
9 from ServiceReference import ServiceReference
10 from RecordTimer import RecordTimerEntry, AFTEREVENT
11 from Components.TimerSanityCheck import TimerSanityCheck
14 from time import localtime, time
17 from enigma import eEPGCache, eServiceReference
20 from Components.config import config
23 from AutoTimerComponent import AutoTimerComponent
25 XML_CONFIG = "/etc/enigma2/autotimer.xml"
26 CURRENT_CONFIG_VERSION = "4"
28 def getValue(definitions, default):
32 # How many definitions are present
34 childNodes = definitions.childNodes
36 Len = len(definitions)
38 childNodes = definitions[Len-1].childNodes
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:
48 # Return stripped output or (if empty) default
49 return ret.strip() or default
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
58 class AutoTimerIgnoreTimerException(Exception):
59 def __init__(self, cause):
63 return "[AutoTimer] " + str(self.cause)
66 return str(type(self))
69 """Read and save xml configuration, query EPGCache"""
73 self.epgcache = eEPGCache.getInstance()
78 self.uniqueTimerId = 0
81 # Abort if no config found
82 if not os_path.exists(XML_CONFIG):
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"
92 self.configMtime = mtime
95 dom = minidom_parse(XML_CONFIG)
97 # Empty out timers and reset Ids
99 self.uniqueTimerId = 0
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)
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)
114 # Increment uniqueTimerId
115 self.uniqueTimerId += 1
118 match = timer.getAttribute("match").encode("UTF-8")
120 print '[AutoTimer] Erroneous config is missing attribute "match", skipping entry'
124 name = timer.getAttribute("name").encode("UTF-8")
126 print '[AutoTimer] Timer is missing attribute "name", defaulting to match'
130 enabled = timer.getAttribute("enabled") or "yes"
133 elif enabled == "yes":
136 print '[AutoTimer] Erroneous config contains invalid value for "enabled":', enabled,', disabling'
140 start = timer.getAttribute("from")
141 end = timer.getAttribute("to")
143 start = [int(x) for x in start.split(':')]
144 end = [int(x) for x in end.split(':')]
145 timetuple = (start, end)
149 # Read out max length
150 maxlen = timer.getAttribute("maxduration") or None
152 maxlen = int(maxlen)*60
154 # Read out recording path (needs my Location-select patch)
155 destination = timer.getAttribute("destination").encode("UTF-8") or None
158 offset = timer.getAttribute("offset") or None
160 offset = offset.split(",")
162 before = after = int(offset[0] or 0) * 60
164 before = int(offset[0] or 0) * 60
165 after = int(offset[1] or 0) * 60
166 offset = (before, after)
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)
176 justplay = int(timer.getAttribute("justplay") or '0')
178 # Read out avoidDuplicateDescription
179 avoidDuplicateDescription = bool(timer.getAttribute("avoidDuplicateDescription") or False)
181 # Read out allowed services
183 for service in timer.getElementsByTagName("serviceref"):
184 value = getValue(service, None)
186 # strip all after last :
187 pos = value.rfind(':')
189 value = value[:pos+1]
191 servicelist.append(value)
193 # Read out allowed bouquets
195 for bouquet in timer.getElementsByTagName("bouquet"):
196 value = getValue(bouquet, None)
198 bouquets.append(value)
200 # Read out afterevent
201 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
203 for element in timer.getElementsByTagName("afterevent"):
204 value = getValue(element, None)
208 start = element.getAttribute("from")
209 end = element.getAttribute("to")
211 start = [int(x) for x in start.split(':')]
212 end = [int(x) for x in end.split(':')]
213 afterevent.append((value, (start, end)))
215 afterevent.append((value, None))
217 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
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):
230 excludes[idx[where]].append(value.encode("UTF-8"))
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):
243 includes[idx[where]].append(value.encode("UTF-8"))
247 # Finally append tuple
248 self.timers.append(AutoTimerComponent(
253 timespan = timetuple,
254 services = servicelist,
256 afterevent = afterevent,
259 maxduration = maxlen,
260 destination = destination,
261 matchCount = counter,
262 matchLeft = counterLeft,
263 matchLimit = counterLimit,
264 matchFormatString = counterFormat,
265 lastBegin = lastBegin,
267 avoidDuplicateDescription = avoidDuplicateDescription,
271 def getTimerList(self):
274 def getEnabledTimerList(self):
275 return [x for x in self.timers if x.enabled]
277 def getTupleTimerList(self):
278 return [(x,) for x in self.timers]
280 def getUniqueId(self):
281 self.uniqueTimerId += 1
282 return self.uniqueTimerId
284 def add(self, timer):
285 self.timers.append(timer)
287 def set(self, timer):
289 for stimer in self.timers:
291 self.timers[idx] = timer
294 self.timers.append(timer)
296 def remove(self, uniqueId):
298 for timer in self.timers:
299 if timer.id == uniqueId:
305 # Generate List in RAM
306 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
309 for timer in self.timers:
310 # Common attributes (match, enabled)
311 list.extend([' <timer name="', timer.name, '" match="', timer.match, '" enabled="', timer.getEnabled(), '"'])
314 if timer.hasTimespan():
315 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
318 if timer.hasDuration():
319 list.extend([' maxduration="', str(timer.getDuration()), '"'])
321 # Destination (needs my Location-select patch)
322 if timer.hasDestination():
323 list.extend([' destination="', str(timer.destination), '"'])
326 if timer.hasOffset():
327 if timer.isOffsetEqual():
328 list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
330 list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
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()), '"'])
339 # Duplicate Description
340 if timer.getAvoidDuplicateDescription():
341 list.append(' avoidDuplicateDescription="1" ')
343 # Only display justplay if true
345 list.extend([' justplay="', str(timer.getJustplay()), '"'])
347 # Close still opened timer tag
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'])
357 for bouquet in timer.getBouquets():
358 list.extend([' <bouquet>', str(bouquet), '</bouquet>'])
359 ref = ServiceReference(str(bouquet))
360 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
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'])
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'])
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'])
393 list.append(' </timer>\n\n')
395 # End of Configuration
396 list.append('</autotimer>\n')
398 # Try Saving to Flash
401 file = open(XML_CONFIG, 'w')
402 file.writelines(list)
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
407 print "[AutoTimer] Error Saving Timer List:", e
409 def parseEPG(self, simulateOnly = False):
410 if NavigationInstance.instance is None:
411 print "[AutoTimer] Navigation is not available, can't parse EPG"
421 # Save Recordings in a dict to speed things up a little
422 # We include processed timers as we might search for duplicate descriptions
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]
428 recorddict[str(timer.service_ref)].append(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 []
435 for serviceref, eit in ret:
436 eserviceref = eServiceReference(serviceref)
438 evt = self.epgcache.lookupEventId(eserviceref, eit)
440 print "[AutoTimer] Could not create Event!"
443 # Try to determine real service (we always choose the last one)
444 n = evt.getNumOfLinkageServices()
446 i = evt.getLinkageService(eserviceref, n-1)
447 serviceref = i.toString()
450 name = evt.getEventName()
451 description = evt.getShortDescription()
452 begin = evt.getBeginTime()
453 duration = evt.getDuration()
454 end = begin + duration
456 # If event starts in less than 60 seconds skip it
457 if begin < time() + 60:
461 timestamp = localtime(begin)
464 timer.update(begin, timestamp)
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)):
475 begin -= config.recording.margin_before.value * 60
476 end += config.recording.margin_after.value * 60
478 # Apply custom Offset
479 begin, end = timer.applyOffset(begin, end)
483 # Append to timerlist and abort if simulating
484 timers.append((name, begin, end, serviceref, timer.name))
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.
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):
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")
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"
511 func = NavigationInstance.instance.RecordTimer.timeChanged
514 # Modify values saved in timer
516 newEntry.description = description
517 newEntry.begin = int(begin)
518 newEntry.end = int(end)
519 newEntry.service_ref = ServiceReference(serviceref)
522 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
523 raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
525 except AutoTimerIgnoreTimerException, etite:
529 # Event not yet in Timers
531 if timer.checkCounter(timestamp):
536 print "[AutoTimer] Adding an event."
537 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
538 func = NavigationInstance.instance.RecordTimer.record
540 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
541 newEntry.isAutoTimer = True
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
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
556 # Make this timer a zap-timer if wanted
557 newEntry.justplay = timer.justplay
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"
564 print "[Autotimer] Sanity check passed"
566 # Either add to List or change time
569 return (total, new, modified, timers)