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 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)
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)
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)
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
408 # We include processed timers as we might search for duplicate descriptions
410 for timer in NavigationInstance.instance.RecordTimer.timer_list + NavigationInstance.instance.RecordTimer.processed_timers:
411 if not recorddict.has_key(str(timer.service_ref)):
412 recorddict[str(timer.service_ref)] = [timer]
414 recorddict[str(timer.service_ref)].append(timer)
417 for timer in self.getEnabledTimerList():
418 # Search EPG, default to empty list
419 ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
421 for serviceref, eit in ret:
422 eserviceref = eServiceReference(serviceref)
424 evt = self.epgcache.lookupEventId(eserviceref, eit)
426 print "[AutoTimer] Could not create Event!"
429 # Try to determine real service (we always choose the last one)
430 n = evt.getNumOfLinkageServices()
432 i = evt.getLinkageService(eserviceref, n-1)
433 serviceref = i.toString()
436 name = evt.getEventName()
437 description = evt.getShortDescription()
438 begin = evt.getBeginTime()
439 duration = evt.getDuration()
440 end = begin + duration
442 # If event starts in less than 60 seconds skip it
443 if begin < time() + 60:
447 timestamp = localtime(begin)
450 timer.update(begin, timestamp)
452 # Check Duration, Timespan and Excludes
453 if timer.checkServices(serviceref) or timer.checkDuration(duration) or \
454 timer.checkTimespan(timestamp) or \
455 timer.checkFilter(name, description, evt.getExtendedDescription(), str(timestamp.tm_wday)):
459 begin -= config.recording.margin_before.value * 60
460 end += config.recording.margin_after.value * 60
462 # Apply custom Offset
463 begin, end = timer.applyOffset(begin, end)
467 # Append to timerlist and abort if simulating
468 timers.append((name, begin, end, serviceref, timer.name))
475 # Check for double Timers
476 # We first check eit and if user wants us to guess event based on time
477 # we try this as backup. The allowed diff should be configurable though.
479 for rtimer in recorddict.get(serviceref, []):
480 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
483 # Abort if we don't want to modify timers or timer is repeated
484 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
485 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
488 if newEntry.isAutoTimer:
489 print "[AutoTimer] Modifying existing AutoTimer!"
490 except AttributeError, ae:
491 if config.plugins.autotimer.refresh.value != "all":
492 raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
493 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
495 func = NavigationInstance.instance.RecordTimer.timeChanged
498 # Modify values saved in timer
500 newEntry.description = description
501 newEntry.begin = int(begin)
502 newEntry.end = int(end)
503 newEntry.service_ref = ServiceReference(serviceref)
506 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
507 raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
509 except AutoTimerIgnoreTimerException, etite:
513 # Event not yet in Timers
515 if timer.checkCounter(timestamp):
520 print "[AutoTimer] Adding an event."
521 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
522 func = NavigationInstance.instance.RecordTimer.record
524 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
525 newEntry.isAutoTimer = True
528 if timer.hasAfterEvent():
529 afterEvent = timer.getAfterEventTimespan(localtime(end))
530 if afterEvent is None:
531 afterEvent = timer.getAfterEvent()
532 if afterEvent is not None:
533 newEntry.afterEvent = afterEvent
535 # Set custom destination directory (needs my Location-select patch)
536 if timer.hasDestination():
537 # TODO: add warning when patch not installed?
538 newEntry.dirname = timer.destination
540 # Make this timer a zap-timer if wanted
541 newEntry.justplay = timer.justplay
543 # Do a sanity check, although it does not do much right now
544 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
545 if not timersanitycheck.check():
546 print "[Autotimer] Sanity check failed"
548 print "[Autotimer] Sanity check passed"
550 # Either add to List or change time
553 return (total, new, modified, timers)