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, isList = True):
32 # How many definitions are present
34 Len = len(definitions)
36 childNodes = definitions[Len-1].childNodes
40 childNodes = definitions.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, False)
186 # strip all after last :
187 pos = value.rfind(':')
189 value = value[:pos+1]
191 servicelist.append(value)
193 # Read out afterevent
194 idx = {"none": AFTEREVENT.NONE, "standby": AFTEREVENT.STANDBY, "shutdown": AFTEREVENT.DEEPSTANDBY, "deepstandby": AFTEREVENT.DEEPSTANDBY}
196 for element in timer.getElementsByTagName("afterevent"):
197 value = getValue(element, None, False)
201 start = element.getAttribute("from")
202 end = element.getAttribute("to")
204 start = [int(x) for x in start.split(':')]
205 end = [int(x) for x in end.split(':')]
206 afterevent.append((value, (start, end)))
208 afterevent.append((value, None))
210 print '[AutoTimer] Erroneous config contains invalid value for "afterevent":', afterevent,', ignoring definition'
214 idx = {"title": 0, "shortdescription": 1, "description": 2, "dayofweek": 3}
215 excludes = ([], [], [], [])
216 for exclude in timer.getElementsByTagName("exclude"):
217 where = exclude.getAttribute("where")
218 value = getValue(exclude, None, False)
219 if not (value and where):
223 excludes[idx[where]].append(value.encode("UTF-8"))
227 # Read out includes (use same idx)
228 includes = ([], [], [], [])
229 for include in timer.getElementsByTagName("include"):
230 where = include.getAttribute("where")
231 value = getValue(include, None, False)
232 if not (value and where):
236 includes[idx[where]].append(value.encode("UTF-8"))
240 # Finally append tuple
241 self.timers.append(AutoTimerComponent(
246 timespan = timetuple,
247 services = servicelist,
249 afterevent = afterevent,
252 maxduration = maxlen,
253 destination = destination,
254 matchCount = counter,
255 matchLeft = counterLeft,
256 matchLimit = counterLimit,
257 matchFormatString = counterFormat,
258 lastBegin = lastBegin,
260 avoidDuplicateDescription = avoidDuplicateDescription
263 def getTimerList(self):
266 def getEnabledTimerList(self):
267 return [x for x in self.timers if x.enabled]
269 def getTupleTimerList(self):
270 return [(x,) for x in self.timers]
272 def getUniqueId(self):
273 self.uniqueTimerId += 1
274 return self.uniqueTimerId
276 def add(self, timer):
277 self.timers.append(timer)
279 def set(self, timer):
281 for stimer in self.timers:
283 self.timers[idx] = timer
286 self.timers.append(timer)
288 def remove(self, uniqueId):
290 for timer in self.timers:
291 if timer.id == uniqueId:
297 # Generate List in RAM
298 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
301 for timer in self.timers:
302 # Common attributes (match, enabled)
303 list.extend([' <timer name="', timer.name, '" match="', timer.match, '" enabled="', timer.getEnabled(), '"'])
306 if timer.hasTimespan():
307 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
310 if timer.hasDuration():
311 list.extend([' maxduration="', str(timer.getDuration()), '"'])
313 # Destination (needs my Location-select patch)
314 if timer.hasDestination():
315 list.extend([' destination="', str(timer.destination), '"'])
318 if timer.hasOffset():
319 if timer.isOffsetEqual():
320 list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
322 list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
325 if timer.hasCounter():
326 list.extend([' lastBegin="', str(timer.getLastBegin()), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
327 if timer.hasCounterFormatString():
328 list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
329 list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
331 # Duplicate Description
332 if timer.getAvoidDuplicateDescription():
333 list.append(' avoidDuplicateDescription="1" ')
335 # Only display justplay if true
337 list.extend([' justplay="', str(timer.getJustplay()), '"'])
339 # Close still opened timer tag
343 for serviceref in timer.getServices():
344 list.extend([' <serviceref>', serviceref, '</serviceref>'])
345 ref = ServiceReference(str(serviceref))
346 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
349 if timer.hasAfterEvent():
350 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
351 for afterevent in timer.getCompleteAfterEvent():
352 action, timespan = afterevent
353 list.append(' <afterevent')
354 if timespan[0] is not None:
355 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
356 list.extend(['>', idx[action], '</afterevent>\n'])
359 for title in timer.getExcludedTitle():
360 list.extend([' <exclude where="title">', title, '</exclude>\n'])
361 for short in timer.getExcludedShort():
362 list.extend([' <exclude where="shortdescription">', short, '</exclude>\n'])
363 for desc in timer.getExcludedDescription():
364 list.extend([' <exclude where="description">', desc, '</exclude>\n'])
365 for day in timer.getExcludedDays():
366 list.extend([' <exclude where="dayofweek">', day, '</exclude>\n'])
369 for title in timer.getIncludedTitle():
370 list.extend([' <include where="title">', title, '</include>\n'])
371 for short in timer.getIncludedShort():
372 list.extend([' <include where="shortdescription">', short, '</include>\n'])
373 for desc in timer.getIncludedDescription():
374 list.extend([' <include where="description">', desc, '</include>\n'])
375 for day in timer.getIncludedDays():
376 list.extend([' <include where="dayofweek">', day, '</include>\n'])
379 list.append(' </timer>\n\n')
381 # End of Configuration
382 list.append('</autotimer>\n')
384 # Try Saving to Flash
387 file = open(XML_CONFIG, 'w')
388 file.writelines(list)
390 # FIXME: This should actually be placed inside a finally-block but python 2.4 does not support this - waiting for some images to upgrade
393 print "[AutoTimer] Error Saving Timer List:", e
395 def parseEPG(self, simulateOnly = False):
396 if NavigationInstance.instance is None:
397 print "[AutoTimer] Navigation is not available, can't parse EPG"
407 # Save Recordings in a dict to speed things up a little
409 for timer in NavigationInstance.instance.RecordTimer.timer_list:
410 if not recorddict.has_key(str(timer.service_ref)):
411 recorddict[str(timer.service_ref)] = [timer]
413 recorddict[str(timer.service_ref)].append(timer)
416 for timer in self.getEnabledTimerList():
417 # Search EPG, default to empty list
418 ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
420 for serviceref, eit in ret:
421 eserviceref = eServiceReference(serviceref)
423 evt = self.epgcache.lookupEventId(eserviceref, eit)
425 print "[AutoTimer] Could not create Event!"
428 # Try to determine real service (we always choose the last one)
429 n = evt.getNumOfLinkageServices()
431 i = evt.getLinkageService(eserviceref, n-1)
432 serviceref = i.toString()
435 name = evt.getEventName()
436 description = evt.getShortDescription()
437 begin = evt.getBeginTime()
438 duration = evt.getDuration()
439 end = begin + duration
441 # If event starts in less than 60 seconds skip it
442 if begin < time() + 60:
446 timestamp = localtime(begin)
449 timer.update(begin, timestamp)
451 # Check Duration, Timespan and Excludes
452 if timer.checkServices(serviceref) or timer.checkDuration(duration) or timer.checkTimespan(timestamp) or timer.checkFilter(name, description, evt.getExtendedDescription(), str(timestamp.tm_wday)):
456 begin -= config.recording.margin_before.value * 60
457 end += config.recording.margin_after.value * 60
459 # Apply custom Offset
460 begin, end = timer.applyOffset(begin, end)
464 # Append to timerlist and abort if simulating
465 timers.append((name, begin, end, serviceref, timer.name))
472 # Check for double Timers
473 # We first check eit and if user wants us to guess event based on time
474 # we try this as backup. The allowed diff should be configurable though.
476 for rtimer in recorddict.get(serviceref, []):
477 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
480 # Abort if we don't want to modify timers or timer is repeated
481 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
482 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
485 if newEntry.isAutoTimer:
486 print "[AutoTimer] Modifying existing AutoTimer!"
487 except AttributeError, ae:
488 if config.plugins.autotimer.refresh.value != "all":
489 raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
490 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
492 func = NavigationInstance.instance.RecordTimer.timeChanged
495 # Modify values saved in timer
497 newEntry.description = description
498 newEntry.begin = int(begin)
499 newEntry.end = int(end)
500 newEntry.service_ref = ServiceReference(serviceref)
503 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
504 raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
506 except AutoTimerIgnoreTimerException, etite:
510 # Event not yet in Timers
512 if timer.checkCounter(timestamp):
517 print "[AutoTimer] Adding an event."
518 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
519 func = NavigationInstance.instance.RecordTimer.record
521 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
522 newEntry.isAutoTimer = True
525 if timer.hasAfterEvent():
526 afterEvent = timer.getAfterEventTimespan(localtime(end))
527 if afterEvent is None:
528 afterEvent = timer.getAfterEvent()
529 if afterEvent is not None:
530 newEntry.afterEvent = afterEvent
532 # Set custom destination directory (needs my Location-select patch)
533 if timer.hasDestination():
534 # TODO: add warning when patch not installed?
535 newEntry.dirname = timer.destination
537 # Make this timer a zap-timer if wanted
538 newEntry.justplay = timer.justplay
540 # Do a sanity check, although it does not do much right now
541 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
542 if not timersanitycheck.check():
543 print "[Autotimer] Sanity check failed"
545 print "[Autotimer] Sanity check passed"
547 # Either add to List or change time
550 return (total, new, modified, timers)