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
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 # Read out recording tags (needs my enhanced tag support patch)
249 for tag in timer.getElementsByTagName("tag"):
250 value = getValue(tag, None)
254 tags.append(value.encode("UTF-8"))
256 # Finally append tuple
257 self.timers.append(AutoTimerComponent(
262 timespan = timetuple,
263 services = servicelist,
265 afterevent = afterevent,
268 maxduration = maxlen,
269 destination = destination,
270 matchCount = counter,
271 matchLeft = counterLeft,
272 matchLimit = counterLimit,
273 matchFormatString = counterFormat,
274 lastBegin = lastBegin,
276 avoidDuplicateDescription = avoidDuplicateDescription,
281 def getTimerList(self):
284 def getEnabledTimerList(self):
285 return [x for x in self.timers if x.enabled]
287 def getTupleTimerList(self):
288 return [(x,) for x in self.timers]
290 def getUniqueId(self):
291 self.uniqueTimerId += 1
292 return self.uniqueTimerId
294 def add(self, timer):
295 self.timers.append(timer)
297 def set(self, timer):
299 for stimer in self.timers:
301 self.timers[idx] = timer
304 self.timers.append(timer)
306 def remove(self, uniqueId):
308 for timer in self.timers:
309 if timer.id == uniqueId:
315 # Generate List in RAM
316 list = ['<?xml version="1.0" ?>\n<autotimer version="', CURRENT_CONFIG_VERSION, '">\n\n']
319 for timer in self.timers:
320 # Common attributes (match, enabled)
321 list.extend([' <timer name="', timer.name, '" match="', timer.match, '" enabled="', timer.getEnabled(), '"'])
324 if timer.hasTimespan():
325 list.extend([' from="', timer.getTimespanBegin(), '" to="', timer.getTimespanEnd(), '"'])
328 if timer.hasDuration():
329 list.extend([' maxduration="', str(timer.getDuration()), '"'])
331 # Destination (needs my Location-select patch)
332 if timer.hasDestination():
333 list.extend([' destination="', str(timer.destination), '"'])
336 if timer.hasOffset():
337 if timer.isOffsetEqual():
338 list.extend([' offset="', str(timer.getOffsetBegin()), '"'])
340 list.extend([' offset="', str(timer.getOffsetBegin()), ',', str(timer.getOffsetEnd()), '"'])
343 if timer.hasCounter():
344 list.extend([' lastBegin="', str(timer.getLastBegin()), '" counter="', str(timer.getCounter()), '" left="', str(timer.getCounterLeft()) ,'"'])
345 if timer.hasCounterFormatString():
346 list.extend([' lastActivation="', str(timer.getCounterLimit()), '"'])
347 list.extend([' counterFormat="', str(timer.getCounterFormatString()), '"'])
349 # Duplicate Description
350 if timer.getAvoidDuplicateDescription():
351 list.append(' avoidDuplicateDescription="1" ')
353 # Only display justplay if true
355 list.extend([' justplay="', str(timer.getJustplay()), '"'])
357 # Close still opened timer tag
361 for serviceref in timer.getServices():
362 list.extend([' <serviceref>', serviceref, '</serviceref>'])
363 ref = ServiceReference(str(serviceref))
364 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
367 for bouquet in timer.getBouquets():
368 list.extend([' <bouquet>', str(bouquet), '</bouquet>'])
369 ref = ServiceReference(str(bouquet))
370 list.extend([' <!-- ', ref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', ''), ' -->\n'])
373 if timer.hasAfterEvent():
374 idx = {AFTEREVENT.NONE: "none", AFTEREVENT.STANDBY: "standby", AFTEREVENT.DEEPSTANDBY: "shutdown"}
375 for afterevent in timer.getCompleteAfterEvent():
376 action, timespan = afterevent
377 list.append(' <afterevent')
378 if timespan[0] is not None:
379 list.append(' from="%02d:%02d" to="%02d:%02d"' % (timespan[0][0], timespan[0][1], timespan[1][0], timespan[1][1]))
380 list.extend(['>', idx[action], '</afterevent>\n'])
383 for title in timer.getExcludedTitle():
384 list.extend([' <exclude where="title">', title, '</exclude>\n'])
385 for short in timer.getExcludedShort():
386 list.extend([' <exclude where="shortdescription">', short, '</exclude>\n'])
387 for desc in timer.getExcludedDescription():
388 list.extend([' <exclude where="description">', desc, '</exclude>\n'])
389 for day in timer.getExcludedDays():
390 list.extend([' <exclude where="dayofweek">', day, '</exclude>\n'])
393 for title in timer.getIncludedTitle():
394 list.extend([' <include where="title">', title, '</include>\n'])
395 for short in timer.getIncludedShort():
396 list.extend([' <include where="shortdescription">', short, '</include>\n'])
397 for desc in timer.getIncludedDescription():
398 list.extend([' <include where="description">', desc, '</include>\n'])
399 for day in timer.getIncludedDays():
400 list.extend([' <include where="dayofweek">', day, '</include>\n'])
403 for tag in timer.tags:
404 list.extend([' <tag>', str(tag), '</tag>\n'])
407 list.append(' </timer>\n\n')
409 # End of Configuration
410 list.append('</autotimer>\n')
413 file = open(XML_CONFIG, 'w')
414 file.writelines(list)
418 def parseEPG(self, simulateOnly = False):
419 if NavigationInstance.instance is None:
420 print "[AutoTimer] Navigation is not available, can't parse EPG"
430 # Save Recordings in a dict to speed things up a little
431 # We include processed timers as we might search for duplicate descriptions
433 for timer in NavigationInstance.instance.RecordTimer.timer_list + NavigationInstance.instance.RecordTimer.processed_timers:
434 if not recorddict.has_key(str(timer.service_ref)):
435 recorddict[str(timer.service_ref)] = [timer]
437 recorddict[str(timer.service_ref)].append(timer)
440 for timer in self.getEnabledTimerList():
441 # Search EPG, default to empty list
442 ret = self.epgcache.search(('RI', 100, eEPGCache.PARTIAL_TITLE_SEARCH, timer.match, eEPGCache.NO_CASE_CHECK)) or []
444 for serviceref, eit in ret:
445 eserviceref = eServiceReference(serviceref)
447 evt = self.epgcache.lookupEventId(eserviceref, eit)
449 print "[AutoTimer] Could not create Event!"
452 # Try to determine real service (we always choose the last one)
453 n = evt.getNumOfLinkageServices()
455 i = evt.getLinkageService(eserviceref, n-1)
456 serviceref = i.toString()
459 name = evt.getEventName()
460 description = evt.getShortDescription()
461 begin = evt.getBeginTime()
462 duration = evt.getDuration()
463 end = begin + duration
465 # If event starts in less than 60 seconds skip it
466 if begin < time() + 60:
470 timestamp = localtime(begin)
473 timer.update(begin, timestamp)
475 # Check Duration, Timespan and Excludes
476 if timer.checkServices(serviceref) \
477 or timer.checkDuration(duration) \
478 or timer.checkTimespan(timestamp) \
479 or timer.checkFilter(name, description,
480 evt.getExtendedDescription(), str(timestamp.tm_wday)):
484 begin -= config.recording.margin_before.value * 60
485 end += config.recording.margin_after.value * 60
487 # Apply custom Offset
488 begin, end = timer.applyOffset(begin, end)
492 # Append to timerlist and abort if simulating
493 timers.append((name, begin, end, serviceref, timer.name))
500 # Check for double Timers
501 # We first check eit and if user wants us to guess event based on time
502 # we try this as backup. The allowed diff should be configurable though.
504 for rtimer in recorddict.get(serviceref, []):
505 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, begin, end) > ((duration/10)*8):
508 # Abort if we don't want to modify timers or timer is repeated
509 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
510 raise AutoTimerIgnoreTimerException("Won't modify existing timer because either no modification allowed or repeated timer")
513 if newEntry.isAutoTimer:
514 print "[AutoTimer] Modifying existing AutoTimer!"
515 except AttributeError, ae:
516 if config.plugins.autotimer.refresh.value != "all":
517 raise AutoTimerIgnoreTimerException("Won't modify existing timer because it's no timer set by us")
518 print "[AutoTimer] Warning, we're messing with a timer which might not have been set by us"
520 func = NavigationInstance.instance.RecordTimer.timeChanged
523 # Modify values saved in timer
525 newEntry.description = description
526 newEntry.begin = int(begin)
527 newEntry.end = int(end)
528 newEntry.service_ref = ServiceReference(serviceref)
531 elif timer.getAvoidDuplicateDescription() and rtimer.description == description:
532 raise AutoTimerIgnoreTimerException("We found a timer with same description, skipping event")
534 except AutoTimerIgnoreTimerException, etite:
538 # Event not yet in Timers
540 if timer.checkCounter(timestamp):
545 print "[AutoTimer] Adding an event."
546 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, description, eit)
547 func = NavigationInstance.instance.RecordTimer.record
549 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
550 newEntry.isAutoTimer = True
553 if timer.hasAfterEvent():
554 afterEvent = timer.getAfterEventTimespan(localtime(end))
555 if afterEvent is None:
556 afterEvent = timer.getAfterEvent()
557 if afterEvent is not None:
558 newEntry.afterEvent = afterEvent
560 newEntry.dirname = timer.destination
561 newEntry.justplay = timer.justplay
562 newEntry.tags = timer.tags # This needs my enhanced tag support patch to work
564 # Do a sanity check, although it does not do much right now
565 timersanitycheck = TimerSanityCheck(NavigationInstance.instance.RecordTimer.timer_list, newEntry)
566 if not timersanitycheck.check():
567 print "[Autotimer] Sanity check failed"
569 print "[Autotimer] Sanity check passed"
571 # Either add to List or change time
574 return (total, new, modified, timers)