2 from xml.etree.cElementTree import parse as cet_parse
3 from os import path as os_path
4 from AutoTimerConfiguration import parseConfig, buildConfig
6 # Navigation (RecordTimer)
7 import NavigationInstance
10 from ServiceReference import ServiceReference
11 from RecordTimer import RecordTimerEntry
12 from Components.TimerSanityCheck import TimerSanityCheck
15 from time import localtime, strftime, time, mktime
16 from datetime import timedelta, date
19 from enigma import eEPGCache, eServiceReference, eServiceCenter, iServiceInformation
22 from Components.config import config
25 from AutoTimerComponent import preferredAutoTimerComponent
27 from itertools import chain
28 from collections import defaultdict
30 XML_CONFIG = "/etc/enigma2/autotimer.xml"
32 def getTimeDiff(timer, begin, end):
33 if begin <= timer.begin <= end:
34 return end - timer.begin
35 elif timer.begin <= begin <= timer.end:
36 return timer.end - begin
40 "exact": eEPGCache.EXAKT_TITLE_SEARCH,
41 "partial": eEPGCache.PARTIAL_TITLE_SEARCH
45 "sensitive": eEPGCache.CASE_CHECK,
46 "insensitive": eEPGCache.NO_CASE_CHECK
50 """Read and save xml configuration, query EPGCache"""
56 self.uniqueTimerId = 0
57 self.defaultTimer = preferredAutoTimerComponent(
67 # Abort if no config found
68 if not os_path.exists(XML_CONFIG):
69 print "[AutoTimer] No configuration file present"
72 # Parse if mtime differs from whats saved
73 mtime = os_path.getmtime(XML_CONFIG)
74 if mtime == self.configMtime:
75 print "[AutoTimer] No changes in configuration, won't parse"
79 self.configMtime = mtime
82 configuration = cet_parse(XML_CONFIG).getroot()
84 # Empty out timers and reset Ids
86 self.defaultTimer.clear(-1, True)
91 configuration.get("version"),
95 self.uniqueTimerId = len(self.timers)
98 return buildConfig(self.defaultTimer, self.timers, webif = True)
101 file = open(XML_CONFIG, 'w')
102 file.writelines(buildConfig(self.defaultTimer, self.timers))
107 def add(self, timer):
108 self.timers.append(timer)
110 def getEnabledTimerList(self):
111 return (x for x in self.timers if x.enabled)
113 def getTimerList(self):
116 def getTupleTimerList(self):
118 return [(x,) for x in lst]
120 def getSortedTupleTimerList(self):
123 return [(x,) for x in lst]
125 def getUniqueId(self):
126 self.uniqueTimerId += 1
127 return self.uniqueTimerId
129 def remove(self, uniqueId):
131 for timer in self.timers:
132 if timer.id == uniqueId:
137 def set(self, timer):
139 for stimer in self.timers:
141 self.timers[idx] = timer
144 self.timers.append(timer)
148 def parseEPG(self, simulateOnly = False):
149 if NavigationInstance.instance is None:
150 print "[AutoTimer] Navigation is not available, can't parse EPG"
151 return (0, 0, 0, [], [], [])
158 similar = {} # Contains the the marked similar servicerefs and the conflicting string
159 similars = [] # Contains the added similar timers
161 # NOTE: the config option specifies "the next X days" which means today (== 1) + X
162 delta = timedelta(days = config.plugins.autotimer.maxdaysinfuture.value + 1)
163 evtLimit = mktime((date.today() + delta).timetuple())
164 checkEvtLimit = delta.days > 1
167 # Read AutoTimer configuration
171 epgcache = eEPGCache.getInstance()
172 serviceHandler = eServiceCenter.getInstance()
173 recordHandler = NavigationInstance.instance.RecordTimer
175 # Create dict of all movies in all folders used by an autotimer to compare with recordings
176 # The moviedict will be filled only if one AutoTimer is configured to avoid duplicate description for any recordings
177 moviedict = defaultdict(list)
179 # Save Recordings in a dict to speed things up a little
180 # We include processed timers as we might search for duplicate descriptions
181 # The recordict is always filled
182 recorddict = defaultdict(list)
183 for timer in ( recordHandler.timer_list + recordHandler.processed_timers ):
184 if timer and timer.service_ref:
185 event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit)
186 extdesc = event and event.getExtendedDescription()
187 timer.extdesc = extdesc
188 recorddict[str(timer.service_ref)].append(timer)
191 for timer in self.getEnabledTimerList():
192 # Precompute timer destination dir
193 dest = timer.destination or config.usage.default_path.value
195 # Workaround to allow search for umlauts if we know the encoding
197 if timer.encoding != 'UTF-8':
199 match = match.decode('UTF-8').encode(timer.encoding)
200 except UnicodeDecodeError:
203 # Search EPG, default to empty list
204 epgmatches = epgcache.search(('RITBDSE', 500, typeMap[timer.searchType], match, caseMap[timer.searchCase])) or []
205 # Sort list of tuples by begin time 'B'
206 epgmatches.sort(key = lambda x: x[3])
208 # Reset the the marked similar servicerefs
211 # Loop over all EPG matches
212 for idx, ( serviceref, eit, name, begin, duration, shortdesc, extdesc ) in enumerate( epgmatches ):
213 #print "TEST AT epgmatches idx " + str(idx)
214 # Reset similarMatch and conflictString
218 # Check if serviceref is in similar matches list
221 conflictString = similar[eit]
223 eserviceref = eServiceReference(serviceref)
224 evt = epgcache.lookupEventId(eserviceref, eit)
226 print "[AutoTimer] Could not create Event!"
229 # Try to determine real service (we always choose the last one)
230 n = evt.getNumOfLinkageServices()
232 i = evt.getLinkageService(eserviceref, n-1)
233 serviceref = i.toString()
236 evtEnd = end = begin + duration
238 # If event starts in less than 60 seconds skip it
239 if begin < time() + 60:
240 print "[AutoTimer] Skipping an event because it starts in less than 60 seconds"
243 # If maximum days in future is set then check time
249 timestamp = localtime(begin)
251 timer.update(begin, timestamp)
253 # Check Duration, Timespan, Timeframe and Excludes
254 # Remove similarMatch if You want perform the check also on similar timers
255 if timer.checkServices(serviceref) \
256 or timer.checkDuration(duration) \
257 or ( not similarMatch and timer.checkTimespan(timestamp) ) \
258 or ( not similarMatch and timer.checkTimeframe(begin) ) \
259 or timer.checkFilter(name, shortdesc, extdesc, ( not similarMatch and str(timestamp.tm_wday) ) ):
262 if timer.hasOffset():
263 # Apply custom Offset
264 begin, end = timer.applyOffset(begin, end)
267 begin -= config.recording.margin_before.value * 60
268 end += config.recording.margin_after.value * 60
270 # Eventually change service to alternative
271 if timer.overrideAlternatives:
272 serviceref = timer.getAlternative(serviceref)
276 # Append to timerlist and abort if simulating
277 #if not similarMatch: # Do we want the similar timers in the timers list?
278 timers.append((name, begin, end, serviceref, timer.name))
282 # Check for existing recordings in directory
283 if timer.avoidDuplicateDescription == 3:
287 # Eventually create cache
288 if dest and dest not in moviedict:
289 movielist = serviceHandler.list(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + dest))
290 if movielist is None:
291 print "[AutoTimer] listing of movies in " + dest + " failed"
293 append = moviedict[dest].append
295 movieref = movielist.getNext()
296 if not movieref.valid():
298 if movieref.flags & eServiceReference.mustDescent:
300 info = serviceHandler.info(movieref)
303 event = info.getEvent(movieref)
307 "name": info.getName(movieref),
308 "shortdesc": info.getInfoString(movieref, iServiceInformation.sDescription),
309 "extdesc": event.getExtendedDescription()
313 for movieinfo in moviedict.get(dest, []):
314 if movieinfo.get("name") == name \
315 and movieinfo.get("shortdesc") == shortdesc \
316 and movieinfo.get("extdesc") == extdesc:
317 print "[AutoTimer] We found a matching recorded movie, skipping event:", name
327 similarExists = False # Indicates if we found a matching similar timer
329 # Check for double Timers
330 # We first check eit and if user wants us to guess event based on time
331 # we try this as backup. The allowed diff should be configurable though.
332 for rtimer in recorddict.get(serviceref, []):
333 if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, evtBegin, evtEnd) > ((duration/10)*8):
336 # Abort if we don't want to modify timers or timer is repeated
337 if config.plugins.autotimer.refresh.value == "none" or rtimer.repeated:
338 print "[AutoTimer] Won't modify existing timer because either no modification allowed or repeated timer"
341 if hasattr(rtimer, "isAutoTimer"):
342 rtimer.log(501, "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name,))
344 if config.plugins.autotimer.refresh.value != "all":
345 print "[AutoTimer] Won't modify existing timer because it's no timer set by us"
348 rtimer.log(501, "[AutoTimer] Warning, AutoTimer %s messed with a timer which might not belong to it." % (timer.name,))
353 # Modify values saved in timer
355 newEntry.description = shortdesc
356 newEntry.begin = int(begin)
357 newEntry.end = int(end)
358 newEntry.service_ref = ServiceReference(serviceref)
360 # Don't store the extended description within the timer
361 if hasattr(newEntry, "extdesc"):
365 elif timer.avoidDuplicateDescription >= 1 \
366 and not rtimer.disabled \
367 and rtimer.name == name \
368 and rtimer.description == shortdesc \
369 and hasattr(rtimer, "extdesc") and rtimer.extdesc == extdesc:
371 print "[AutoTimer] We found a timer (similar service) with same description, skipping event"
374 # We found no timer we want to edit
376 # But there is a match
380 # We want to search for possible doubles
381 if timer.avoidDuplicateDescription >= 2:
382 for rtimer in chain.from_iterable( recorddict.values() ):
383 if not rtimer.disabled \
384 and rtimer.name == name \
385 and rtimer.description == shortdesc \
386 and hasattr(rtimer, "extdesc") and rtimer.extdesc == extdesc:
388 print "[AutoTimer] We found a timer (any service) with same description, skipping event"
393 if timer.checkCounter(timestamp):
394 print "[AutoTimer] Not adding new timer because counter is depleted."
397 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, shortdesc, eit)
398 newEntry.log(500, "[AutoTimer] Adding new timer based on AutoTimer %s." % (timer.name,))
400 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
401 newEntry.isAutoTimer = True
404 if timer.hasAfterEvent():
405 afterEvent = timer.getAfterEventTimespan(localtime(end))
406 if afterEvent is None:
407 afterEvent = timer.getAfterEvent()
408 if afterEvent is not None:
409 newEntry.afterEvent = afterEvent
411 newEntry.dirname = timer.destination
412 newEntry.justplay = timer.justplay
413 newEntry.tags = timer.tags
414 newEntry.vpsplugin_enabled = timer.vps_enabled
415 newEntry.vpsplugin_overwrite = timer.vps_overwrite
418 # XXX: this won't perform a sanity check, but do we actually want to do so?
419 recordHandler.timeChanged(newEntry)
422 newEntry.log(504, "[AutoTimer] Similar Timer is added because of conflicts with %s." % (conflictString))
424 conflicts = recordHandler.record(newEntry)
427 conflictString += ' / '.join(["%s (%s)" % (x.name, strftime("%Y%m%d %H%M", localtime(x.begin))) for x in conflicts])
428 print "[AutoTimer] conflict with %s detected" % (conflictString)
430 if conflicts and config.plugins.autotimer.addsimilar_on_conflict.value:
431 # We start our search right after our actual index
432 for servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS in ( epgmatches[idx+1:] + epgmatches[:idx] ):
433 # Match only if the descriptions are equal
434 if extdesc == extdescS and shortdesc == shortdescS:
435 # Check if we already know it
436 if eitS not in similar:
437 print "[AutoTimer] Found similar Timer: " + name
439 # Indicate that there exists a similar timer
442 # Store the similar eit and conflictString, so it can be handled later
443 similar[eitS] = conflictString
445 print "TEST AT beginS < evtBegin " + str(beginS) + " " +str(evtBegin)
446 if beginS < evtBegin:
447 #print "TEST AT epgmatches len " + str(len(epgmatches))
448 # Event is before our actual epgmatch so we have to append it to the epgmatches list
449 epgmatches.append((servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS))
450 #print "TEST AT epgmatches len " + str(len(epgmatches))
451 #print "TEST AT epgmatches " + str(epgmatches)
453 # If this one also will conflict and there are more matches, they will be marked the next time
456 if conflicts and config.plugins.autotimer.disabled_on_conflict.value and not similarExists: # Don't add disabled timer if a similar timer exists
457 #if conflicts and config.plugins.autotimer.disabled_on_conflict.value: # Add disabled timer even if a similar timer exists
458 newEntry.log(503, "[AutoTimer] Timer disabled because of conflicts with %s." % (conflictString,))
459 newEntry.disabled = True
460 # We might want to do the sanity check locally so we don't run it twice - but I consider this workaround a hack anyway
461 conflicts = recordHandler.record(newEntry)
462 conflicting.append((name, begin, end, serviceref, timer.name))
464 if conflicts is None: # Similar timers will be added to the new timer list
465 #if conflicts is None and not similarMatch: # Similar timers won't be added to the new timer list
466 timer.decrementCounter()
468 timer.extdesc = extdesc
469 recorddict[serviceref].append(newEntry)
471 elif conflicts: # Conflicting timers will be added also if a similar timer exists
472 #elif conflicts and not similarExists: # Conflicting timers won't be added if a similar timer exists
473 conflicting.append((name, begin, end, serviceref, timer.name))
475 if similarMatch and conflicts is None: # Similar timers can be in new timers list and additionally in similar timers list
476 #elif similarMatch and conflicts is None: # Similar timers will be only in new timers list or similar timers list
477 similars.append((name, begin, end, serviceref, timer.name))
479 return (total, new, modified, timers, conflicting, similars)