Autotimer 4.1.7: Raised duplicate check ratio to 0.9 (SequenceMatcher)
[enigma2-plugins.git] / autotimer / src / AutoTimer.py
1 from __future__ import print_function
2
3 # Plugins Config
4 from xml.etree.cElementTree import parse as cet_parse
5 from os import path as os_path
6 from AutoTimerConfiguration import parseConfig, buildConfig
7 from Tools.IO import saveFile
8
9 # Navigation (RecordTimer)
10 import NavigationInstance
11
12 # Timer
13 from ServiceReference import ServiceReference
14 from RecordTimer import RecordTimerEntry
15 from Components.TimerSanityCheck import TimerSanityCheck
16
17 # Timespan
18 from time import localtime, strftime, time, mktime
19 from datetime import timedelta, date
20
21 # EPGCache & Event
22 from enigma import eEPGCache, eServiceReference, eServiceCenter, iServiceInformation
23
24 from twisted.internet import reactor, defer
25 from twisted.python import failure
26 from threading import currentThread
27 import Queue
28
29 # AutoTimer Component
30 from AutoTimerComponent import preferredAutoTimerComponent
31 from Logger import doLog, startLog, getLog, doDebug
32
33 from itertools import chain
34 from collections import defaultdict
35 from difflib import SequenceMatcher
36 from operator import itemgetter
37
38 from Plugins.SystemPlugins.Toolkit.SimpleThread import SimpleThread
39
40 try:
41         from Plugins.Extensions.SeriesPlugin.plugin import getSeasonEpisode4 as sp_getSeasonEpisode
42 except ImportError as ie:
43         sp_getSeasonEpisode = None
44
45 try:
46         from Plugins.Extensions.SeriesPlugin.plugin import showResult as sp_showResult
47 except ImportError as ie:
48         sp_showResult = None
49
50 from . import config, xrange, itervalues
51
52 try:
53         from Components.ServiceRecordingSettings import ServiceRecordingSettings
54 except ImportError as ie:
55         ServiceRecordingSettings = None
56
57 XML_CONFIG = "/etc/enigma2/autotimer.xml"
58
59 TAG = "AutoTimer"
60
61 def getTimeDiff(timerbegin, timerend, begin, end):
62         if begin <= timerbegin <= end:
63                 return end - timerbegin
64         elif timerbegin <= begin <= timerend:
65                 return timerend - begin
66         return 0
67
68 def blockingCallFromMainThread(f, *a, **kw):
69         """
70           Modified version of twisted.internet.threads.blockingCallFromThread
71           which waits 30s for results and otherwise assumes the system to be shut down.
72           This is an ugly workaround for a twisted-internal deadlock.
73           Please keep the look intact in case someone comes up with a way
74           to reliably detect from the outside if twisted is currently shutting
75           down.
76         """
77         queue = Queue.Queue()
78         def _callFromThread():
79                 result = defer.maybeDeferred(f, *a, **kw)
80                 result.addBoth(queue.put)
81         reactor.callFromThread(_callFromThread)
82
83         result = None
84         while True:
85                 try:
86                         result = queue.get(True, config.plugins.autotimer.timeout.value*60)
87                 except Queue.Empty as qe:
88                         if True: #not reactor.running: # reactor.running is only False AFTER shutdown, we are during.
89                                 doLog("Reactor no longer active, aborting.")
90                 else:
91                         break
92
93         if isinstance(result, failure.Failure):
94                 result.raiseException()
95         return result
96
97 typeMap = {
98         "exact": eEPGCache.EXAKT_TITLE_SEARCH,
99         "partial": eEPGCache.PARTIAL_TITLE_SEARCH,
100         "description": eEPGCache.PARTIAL_DESCRIPTION_SEARCH
101 }
102
103 caseMap = {
104         "sensitive": eEPGCache.CASE_CHECK,
105         "insensitive": eEPGCache.NO_CASE_CHECK
106 }
107
108 class AutoTimer:
109         """Read and save xml configuration, query EPGCache"""
110
111         def __init__(self):
112                 # Initialize
113                 self.timers = []
114                 self.configMtime = -1
115                 self.uniqueTimerId = 0
116                 self.defaultTimer = preferredAutoTimerComponent(
117                         0,              # Id
118                         "",             # Name
119                         "",             # Match
120                         True    # Enabled
121                 )
122
123 # Configuration
124
125         def readXml(self):
126                 # Abort if no config found
127                 if not os_path.exists(XML_CONFIG):
128                         doLog("No configuration file present")
129                         return
130
131                 # Parse if mtime differs from whats saved
132                 mtime = os_path.getmtime(XML_CONFIG)
133                 if mtime == self.configMtime:
134                         doLog("No changes in configuration, won't parse")
135                         return
136
137                 # Save current mtime
138                 self.configMtime = mtime
139
140                 # Parse Config
141                 configuration = cet_parse(XML_CONFIG).getroot()
142
143                 # Empty out timers and reset Ids
144                 del self.timers[:]
145                 self.defaultTimer.clear(-1, True)
146
147                 parseConfig(
148                         configuration,
149                         self.timers,
150                         configuration.get("version"),
151                         0,
152                         self.defaultTimer
153                 )
154                 self.uniqueTimerId = len(self.timers)
155
156         def getXml(self):
157                 return buildConfig(self.defaultTimer, self.timers, webif = True)
158
159         def writeXml(self):
160                 # XXX: we probably want to indicate failures in some way :)
161                 saveFile(XML_CONFIG, buildConfig(self.defaultTimer, self.timers))
162
163 # Manage List
164
165         def add(self, timer):
166                 self.timers.append(timer)
167
168         def getEnabledTimerList(self):
169                 return (x for x in self.timers if x.enabled)
170
171         def getTimerList(self):
172                 return self.timers
173
174         def getTupleTimerList(self):
175                 lst = self.timers
176                 return [(x,) for x in lst]
177
178         def getSortedTupleTimerList(self):
179                 lst = self.timers[:]
180                 lst.sort()
181                 return [(x,) for x in lst]
182
183         def getUniqueId(self):
184                 self.uniqueTimerId += 1
185                 return self.uniqueTimerId
186
187         def remove(self, uniqueId):
188                 idx = 0
189                 for timer in self.timers:
190                         if timer.id == uniqueId:
191                                 self.timers.pop(idx)
192                                 return
193                         idx += 1
194
195         def set(self, timer):
196                 idx = 0
197                 for stimer in self.timers:
198                         if stimer == timer:
199                                 self.timers[idx] = timer
200                                 return
201                         idx += 1
202                 self.timers.append(timer)
203
204         def parseEPGAsync(self, simulateOnly = False):
205                 t = SimpleThread(lambda: self.parseEPG(simulateOnly=simulateOnly))
206                 t.start()
207                 return t.deferred
208
209 # Main function
210
211         def parseTimer(self, timer, epgcache, serviceHandler, recordHandler, checkEvtLimit, evtLimit, timers, conflicting, similars, skipped, timerdict, moviedict, simulateOnly=False):
212                 new = 0
213                 modified = 0
214
215                 # Search EPG, default to empty list
216                 epgmatches = epgcache.search( ('RITBDSE', 1000, typeMap[timer.searchType], timer.match, caseMap[timer.searchCase]) ) or []
217
218                 # Sort list of tuples by begin time 'B'
219                 epgmatches.sort(key=itemgetter(3))
220
221                 # Contains the the marked similar eits and the conflicting strings
222                 similardict = defaultdict(list)         
223
224                 # Loop over all EPG matches
225                 for idx, ( serviceref, eit, name, begin, duration, shortdesc, extdesc ) in enumerate( epgmatches ):
226                         
227                         startLog()
228                         
229                         # timer destination dir
230                         dest = timer.destination or config.usage.default_path.value
231                         
232                         evtBegin = begin
233                         evtEnd = end = begin + duration
234
235                         doLog("possible epgmatch %s" % (name))
236                         doLog("Serviceref %s" % (str(serviceref)))
237                         eserviceref = eServiceReference(serviceref)
238                         evt = epgcache.lookupEventId(eserviceref, eit)
239                         if not evt:
240                                 doLog("Could not create Event!")
241                                 skipped.append((name, begin, end, str(serviceref), timer.name, getLog()))
242                                 continue
243                         # Try to determine real service (we always choose the last one)
244                         n = evt.getNumOfLinkageServices()
245                         if n > 0:
246                                 i = evt.getLinkageService(eserviceref, n-1)
247                                 serviceref = i.toString()
248                                 doLog("Serviceref2 %s" % (str(serviceref)))
249
250                         # If event starts in less than 60 seconds skip it
251                         if begin < time() + 60:
252                                 doLog("Skipping an event because it starts in less than 60 seconds")
253                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
254                                 continue
255
256                         # Convert begin time
257                         timestamp = localtime(begin)
258                         # Update timer
259                         timer.update(begin, timestamp)
260
261                         # Check if eit is in similar matches list
262                         # NOTE: ignore evtLimit for similar timers as I feel this makes the feature unintuitive
263                         similarTimer = False
264                         if eit in similardict:
265                                 similarTimer = True
266                                 dayofweek = None # NOTE: ignore day on similar timer
267                         else:
268                                 # If maximum days in future is set then check time
269                                 if checkEvtLimit:
270                                         if begin > evtLimit:
271                                                 doLog("Skipping an event because of maximum days in future is reached")
272                                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
273                                                 continue
274
275                                 dayofweek = str(timestamp.tm_wday)
276
277                         # Check timer conditions
278                         # NOTE: similar matches do not care about the day/time they are on, so ignore them
279                         if timer.checkServices(serviceref):
280                                 doLog("Skipping an event because of check services")
281                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
282                                 continue
283                         if timer.checkDuration(duration):
284                                 doLog("Skipping an event because of duration check")
285                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
286                                 continue
287                         if not similarTimer:
288                                 if timer.checkTimespan(timestamp):
289                                         doLog("Skipping an event because of timestamp check")
290                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
291                                         continue
292                                 if timer.checkTimeframe(begin):
293                                         doLog("Skipping an event because of timeframe check")
294                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
295                                         continue
296
297                         # Initialize
298                         newEntry = None
299                         oldExists = False
300                         allow_modify = True
301                         
302                         # Eventually change service to alternative
303                         if timer.overrideAlternatives:
304                                 serviceref = timer.getAlternative(serviceref)
305
306                         if timer.series_labeling and sp_getSeasonEpisode is not None:
307                                 allow_modify = False
308                                 #doLog("Request name, desc, path %s %s %s" % (name,shortdesc,dest))
309                                 sp = sp_getSeasonEpisode(serviceref, name, evtBegin, evtEnd, shortdesc, dest)
310                                 if sp and type(sp) in (tuple, list) and len(sp) == 4:
311                                         name = sp[0] or name
312                                         shortdesc = sp[1] or shortdesc
313                                         dest = sp[2] or dest
314                                         doLog(str(sp[3]))
315                                         #doLog("Returned name, desc, path %s %s %s" % (name,shortdesc,dest))
316                                         allow_modify = True
317                                 else:
318                                         # Nothing found
319                                         doLog(str(sp))
320                                         
321                                         # If AutoTimer name not equal match, do a second lookup with the name
322                                         if timer.name.lower() != timer.match.lower():
323                                                 #doLog("Request name, desc, path %s %s %s" % (timer.name,shortdesc,dest))
324                                                 sp = sp_getSeasonEpisode(serviceref, timer.name, evtBegin, evtEnd, shortdesc, dest)
325                                                 if sp and type(sp) in (tuple, list) and len(sp) == 4:
326                                                         name = sp[0] or name
327                                                         shortdesc = sp[1] or shortdesc
328                                                         dest = sp[2] or dest
329                                                         doLog(str(sp[3]))
330                                                         #doLog("Returned name, desc, path %s %s %s" % (name,shortdesc,dest))
331                                                         allow_modify = True
332                                                 else:
333                                                         doLog(str(sp))
334
335                         if timer.checkFilter(name, shortdesc, extdesc, dayofweek):
336                                 doLog("Skipping an event because of filter check")
337                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
338                                 continue
339                         
340                         if timer.hasOffset():
341                                 # Apply custom Offset
342                                 begin, end = timer.applyOffset(begin, end)
343                         else:
344                                 # Apply E2 Offset
345                                 if ServiceRecordingSettings:
346                                         begin -= ServiceRecordingSettings.instance.getMarginBefore(eserviceref)
347                                         end += ServiceRecordingSettings.instance.getMarginAfter(eserviceref)
348                                 else:
349                                         begin -= config.recording.margin_before.value * 60
350                                         end += config.recording.margin_after.value * 60
351
352                         # Overwrite endtime if requested
353                         if timer.justplay and not timer.setEndtime:
354                                 end = begin
355
356                         # Check for existing recordings in directory
357                         if timer.avoidDuplicateDescription == 3:
358                                 # Reset movie Exists
359                                 movieExists = False
360
361                                 if dest and dest not in moviedict:
362                                         self.addDirectoryToMovieDict(moviedict, dest, serviceHandler)
363                                 for movieinfo in moviedict.get(dest, ()):
364                                         if self.checkDuplicates(timer, name, movieinfo.get("name"), shortdesc, movieinfo.get("shortdesc"), extdesc, movieinfo.get("extdesc") ):
365                                                 doLog("We found a matching recorded movie, skipping event:", name)
366                                                 movieExists = True
367                                                 break
368                                 if movieExists:
369                                         doLog("Skipping an event because movie already exists")
370                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
371                                         continue
372
373                         # Check for double Timers
374                         # We first check eit and if user wants us to guess event based on time
375                         # we try this as backup. The allowed diff should be configurable though.
376                         for rtimer in timerdict.get(serviceref, ()):
377                                 if rtimer.eit == eit:
378                                         oldExists = True
379                                         doLog("We found a timer based on eit")
380                                         newEntry = rtimer
381                                         break
382                                 elif config.plugins.autotimer.try_guessing.value:
383                                         if timer.hasOffset():
384                                                 # Remove custom Offset
385                                                 rbegin = rtimer.begin + timer.offset[0] * 60
386                                                 rend = rtimer.end - timer.offset[1] * 60
387                                         else:
388                                                 # Remove E2 Offset
389                                                 rbegin = rtimer.begin + config.recording.margin_before.value * 60
390                                                 rend = rtimer.end - config.recording.margin_after.value * 60
391                                         # As alternative we could also do a epg lookup
392                                         #revent = epgcache.lookupEventId(rtimer.service_ref.ref, rtimer.eit)
393                                         #rbegin = revent.getBeginTime() or 0
394                                         #rduration = revent.getDuration() or 0
395                                         #rend = rbegin + rduration or 0
396                                         if getTimeDiff(rbegin, rend, evtBegin, evtEnd) > ((duration/10)*8):
397                                                 oldExists = True
398                                                 doLog("We found a timer based on time guessing")
399                                                 newEntry = rtimer
400                                                 break
401                                 if timer.avoidDuplicateDescription >= 1 \
402                                         and not rtimer.disabled:
403                                                 if self.checkDuplicates(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
404                                                 # if searchForDuplicateDescription > 1 then check short description
405                                                         oldExists = True
406                                                         doLog("We found a timer (similar service) with same description, skipping event")
407                                                         break
408
409                         # We found no timer we want to edit
410                         if newEntry is None:
411                                 # But there is a match
412                                 if oldExists:
413                                         doLog("Skipping an event because a timer on same service exists")
414                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
415                                         continue
416
417                                 # We want to search for possible doubles
418                                 if timer.avoidDuplicateDescription >= 2:
419                                         for rtimer in chain.from_iterable( itervalues(timerdict) ):
420                                                 if not rtimer.disabled:
421                                                         if self.checkDuplicates(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
422                                                                 oldExists = True
423                                                                 doLog("We found a timer (any service) with same description, skipping event")
424                                                                 break
425                                         if oldExists:
426                                                 doLog("Skipping an event because a timer on any service exists")
427                                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
428                                                 continue
429
430                                 if timer.checkCounter(timestamp):
431                                         doLog("Not adding new timer because counter is depleted.")
432                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
433                                         continue
434
435                         # Append to timerlist and abort if simulating
436                         timers.append((name, begin, end, serviceref, timer.name, getLog()))
437                         if simulateOnly:
438                                 continue
439
440                         if newEntry is not None:
441                                 # Abort if we don't want to modify timers or timer is repeated
442                                 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
443                                         doLog("Won't modify existing timer because either no modification allowed or repeated timer")
444                                         continue
445
446                                 if hasattr(newEntry, "isAutoTimer"):
447                                         msg = "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name)
448                                         doLog(msg)
449                                         newEntry.log(501, msg)
450                                 elif config.plugins.autotimer.add_autotimer_to_tags.value and TAG in newEntry.tags:
451                                         msg = "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name)
452                                         doLog(msg)
453                                         newEntry.log(501, msg)
454                                 else:
455                                         if config.plugins.autotimer.refresh.value != "all":
456                                                 doLog("Won't modify existing timer because it's no timer set by us")
457                                                 continue
458
459                                         msg = "[AutoTimer] Warning, AutoTimer %s messed with a timer which might not belong to it: %s ." % (timer.name, newEntry.name)
460                                         doLog(msg)
461                                         newEntry.log(501, msg)
462
463                                 modified += 1
464
465                                 if allow_modify:
466                                         self.modifyTimer(newEntry, name, shortdesc, begin, end, serviceref, eit)
467                                         msg = "[AutoTimer] AutoTimer modified timer: %s ." % (newEntry.name)
468                                         doLog(msg)
469                                         newEntry.log(501, msg)
470                                 else:
471                                         msg = "[AutoTimer] AutoTimer modification not allowed for timer: %s ." % (newEntry.name)
472                                         doLog(msg)
473                         else:
474                                 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, shortdesc, eit)
475                                 msg = "[AutoTimer] Try to add new timer based on AutoTimer %s." % (timer.name)
476                                 doLog(msg)
477                                 newEntry.log(500, msg)
478                                 
479                                 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
480                                 # It is only temporarily, after a restart it will be lost,
481                                 # because it won't be stored in the timer xml file
482                                 newEntry.isAutoTimer = True
483
484                         # Apply afterEvent
485                         if timer.hasAfterEvent():
486                                 afterEvent = timer.getAfterEventTimespan(localtime(end))
487                                 if afterEvent is None:
488                                         afterEvent = timer.getAfterEvent()
489                                 if afterEvent is not None:
490                                         newEntry.afterEvent = afterEvent
491
492                         newEntry.dirname = dest
493                         newEntry.calculateFilename()
494
495                         newEntry.justplay = timer.justplay
496                         newEntry.vpsplugin_enabled = timer.vps_enabled
497                         newEntry.vpsplugin_overwrite = timer.vps_overwrite
498                         tags = timer.tags[:]
499                         if config.plugins.autotimer.add_autotimer_to_tags.value:
500                                 if TAG not in tags:
501                                         tags.append(TAG)
502                         if config.plugins.autotimer.add_name_to_tags.value:
503                                 tagname = timer.name.strip()
504                                 if tagname:
505                                         tagname = tagname[0].upper() + tagname[1:].replace(" ", "_")
506                                         if tagname not in tags:
507                                                 tags.append(tagname)
508                         newEntry.tags = tags
509
510                         if oldExists:
511                                 # XXX: this won't perform a sanity check, but do we actually want to do so?
512                                 recordHandler.timeChanged(newEntry)
513
514                         else:
515                                 conflictString = ""
516                                 if similarTimer:
517                                         conflictString = similardict[eit].conflictString
518                                         msg = "[AutoTimer] Try to add similar Timer because of conflicts with %s." % (conflictString)
519                                         doLog(msg)
520                                         newEntry.log(504, msg)
521
522                                 # Try to add timer
523                                 conflicts = recordHandler.record(newEntry)
524
525                                 if conflicts:
526                                         # Maybe use newEntry.log
527                                         conflictString += ' / '.join(["%s (%s)" % (x.name, strftime("%Y%m%d %H%M", localtime(x.begin))) for x in conflicts])
528                                         doLog("conflict with %s detected" % (conflictString))
529
530                                         if config.plugins.autotimer.addsimilar_on_conflict.value:
531                                                 # We start our search right after our actual index
532                                                 # Attention we have to use a copy of the list, because we have to append the previous older matches
533                                                 lepgm = len(epgmatches)
534                                                 for i in xrange(lepgm):
535                                                         servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS = epgmatches[ (i+idx+1)%lepgm ]
536                                                         if self.checkDuplicates(timer, name, nameS, shortdesc, shortdescS, extdesc, extdescS, force=True ):
537                                                                 # Check if the similar is already known
538                                                                 if eitS not in similardict:
539                                                                         doLog("Found similar Timer: " + name)
540
541                                                                         # Store the actual and similar eit and conflictString, so it can be handled later
542                                                                         newEntry.conflictString = conflictString
543                                                                         similardict[eit] = newEntry
544                                                                         similardict[eitS] = newEntry
545                                                                         similarTimer = True
546                                                                         if beginS <= evtBegin:
547                                                                                 # Event is before our actual epgmatch so we have to append it to the epgmatches list
548                                                                                 epgmatches.append((servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS))
549                                                                         # If we need a second similar it will be found the next time
550                                                                 else:
551                                                                         similarTimer = False
552                                                                         newEntry = similardict[eitS]
553                                                                 break
554
555                                 if conflicts is None:
556                                         timer.decrementCounter()
557                                         new += 1
558                                         newEntry.extdesc = extdesc
559                                         timerdict[serviceref].append(newEntry)
560
561                                         # Similar timers are in new timers list and additionally in similar timers list
562                                         if similarTimer:
563                                                 similars.append((name, begin, end, serviceref, timer.name))
564                                                 similardict.clear()
565
566                                 # Don't care about similar timers
567                                 elif not similarTimer:
568                                         conflicting.append((name, begin, end, serviceref, timer.name))
569
570                                         if config.plugins.autotimer.disabled_on_conflict.value:
571                                                 msg = "[AutoTimer] Timer disabled because of conflicts with %s." % (conflictString)
572                                                 doLog(msg)
573                                                 newEntry.log(503, msg)
574                                                 newEntry.disabled = True
575                                                 # We might want to do the sanity check locally so we don't run it twice - but I consider this workaround a hack anyway
576                                                 conflicts = recordHandler.record(newEntry)
577                 
578                 return (new, modified)
579
580         def parseEPG(self, simulateOnly=False, uniqueId=None, callback=None):
581
582                 from plugin import AUTOTIMER_VERSION
583                 doLog("AutoTimer Version: " + AUTOTIMER_VERSION)
584
585                 if NavigationInstance.instance is None:
586                         doLog("Navigation is not available, can't parse EPG")
587                         return (0, 0, 0, [], [], [])
588
589                 new = 0
590                 modified = 0
591                 timers = []
592                 conflicting = []
593                 similars = []
594                 skipped = []
595
596                 if currentThread().getName() == 'MainThread':
597                         doBlockingCallFromMainThread = lambda f, *a, **kw: f(*a, **kw)
598                 else:
599                         doBlockingCallFromMainThread = blockingCallFromMainThread
600
601                 # NOTE: the config option specifies "the next X days" which means today (== 1) + X
602                 delta = timedelta(days = config.plugins.autotimer.maxdaysinfuture.value + 1)
603                 evtLimit = mktime((date.today() + delta).timetuple())
604                 checkEvtLimit = delta.days > 1
605                 del delta
606
607                 # Read AutoTimer configuration
608                 self.readXml()
609
610                 # Get E2 instances
611                 epgcache = eEPGCache.getInstance()
612                 serviceHandler = eServiceCenter.getInstance()
613                 recordHandler = NavigationInstance.instance.RecordTimer
614
615                 # Save Timer in a dict to speed things up a little
616                 # We include processed timers as we might search for duplicate descriptions
617                 # NOTE: It is also possible to use RecordTimer isInTimer(), but we won't get the timer itself on a match
618                 timerdict = defaultdict(list)
619                 doBlockingCallFromMainThread(self.populateTimerdict, epgcache, recordHandler, timerdict)
620
621                 # Create dict of all movies in all folders used by an autotimer to compare with recordings
622                 # The moviedict will be filled only if one AutoTimer is configured to avoid duplicate description for any recordings
623                 moviedict = defaultdict(list)
624
625                 # Iterate Timer
626                 for timer in self.getEnabledTimerList():
627                         if uniqueId == None or timer.id == uniqueId:
628                                 tup = doBlockingCallFromMainThread(self.parseTimer, timer, epgcache, serviceHandler, recordHandler, checkEvtLimit, evtLimit, timers, conflicting, similars, skipped, timerdict, moviedict, simulateOnly=simulateOnly)
629                                 if callback:
630                                         callback(timers, conflicting, similars, skipped)
631                                         del timers[:]
632                                         del conflicting[:]
633                                         del similars[:]
634                                         del skipped[:]
635                                 else:
636                                         new += tup[0]
637                                         modified += tup[1]
638                 
639                 if not simulateOnly:
640                         if sp_showResult is not None:
641                                 blockingCallFromMainThread(sp_showResult)
642                 
643                 return (len(timers), new, modified, timers, conflicting, similars)
644
645 # Supporting functions
646
647         def populateTimerdict(self, epgcache, recordHandler, timerdict):
648                 remove = []
649                 for timer in chain(recordHandler.timer_list, recordHandler.processed_timers):
650                         if timer and timer.service_ref:
651                                 if timer.eit is not None:
652                                         event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit)
653                                         if event:
654                                                 timer.extdesc = event.getExtendedDescription()
655                                         else:
656                                                 remove.append(timer)
657                                 else:
658                                         remove.append(timer)
659                                         continue
660
661                                 if not hasattr(timer, 'extdesc'):
662                                         timer.extdesc = ''
663
664                                 timerdict[str(timer.service_ref)].append(timer)
665
666                 if config.plugins.autotimer.check_eit_and_remove.value:
667                         for timer in remove:
668                                 if hasattr(timer, "isAutoTimer") or (config.plugins.autotimer.add_autotimer_to_tags.value and TAG in timer.tags):
669                                         try:
670                                                 # Because of the duplicate check, we only want to remove future timer
671                                                 if timer in recordHandler.timer_list:
672                                                         if not timer.isRunning():
673                                                                 global NavigationInstance
674                                                                 doLog("Remove timer because of eit check " + timer.name)
675                                                                 NavigationInstance.instance.RecordTimer.removeEntry(timer)
676                                         except:
677                                                 pass
678                 del remove
679
680         def modifyTimer(self, timer, name, shortdesc, begin, end, serviceref, eit=None):
681                 timer.name = name
682                 timer.description = shortdesc
683                 timer.begin = int(begin)
684                 timer.end = int(end)
685                 timer.service_ref = ServiceReference(serviceref)
686                 if eit:
687                         timer.eit = eit
688
689         def addDirectoryToMovieDict(self, moviedict, dest, serviceHandler):
690                 movielist = serviceHandler.list(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + dest))
691                 if movielist is None:
692                         doLog("listing of movies in " + dest + " failed")
693                 else:
694                         append = moviedict[dest].append
695                         while 1:
696                                 movieref = movielist.getNext()
697                                 if not movieref.valid():
698                                         break
699                                 if movieref.flags & eServiceReference.mustDescent:
700                                         continue
701                                 info = serviceHandler.info(movieref)
702                                 if info is None:
703                                         continue
704                                 event = info.getEvent(movieref)
705                                 if event is None:
706                                         continue
707                                 append({
708                                         "name": info.getName(movieref),
709                                         "shortdesc": info.getInfoString(movieref, iServiceInformation.sDescription),
710                                         "extdesc": event.getExtendedDescription() or '' # XXX: does event.getExtendedDescription() actually return None on no description or an empty string?
711                                 })
712
713         def checkDuplicates(self, timer, name1, name2, shortdesc1, shortdesc2, extdesc1, extdesc2, force=False):
714                 if name1 and name2:
715                         sequenceMatcher = SequenceMatcher(" ".__eq__, name1, name2)
716                 else:
717                         return False
718
719                 ratio = sequenceMatcher.ratio()
720                 doDebug("names ratio %f - %s - %d - %s - %d" % (ratio, name1, len(name1), name2, len(name2)))
721                 if name1 in name2 or (0.9 < ratio): # this is probably a match
722                         foundShort = True
723                         if (force or timer.searchForDuplicateDescription > 0) and shortdesc1 and shortdesc2:
724                                 sequenceMatcher.set_seqs(shortdesc1, shortdesc2)
725                                 ratio = sequenceMatcher.ratio()
726                                 doDebug("shortdesc ratio %f - %s - %d - %s - %d" % (ratio, shortdesc1, len(shortdesc1), shortdesc2, len(shortdesc2)))
727                                 foundShort = shortdesc1 in shortdesc2 or (0.9 < ratio)
728                                 if foundShort:
729                                         doLog("shortdesc ratio %f - %s - %d - %s - %d" % (ratio, shortdesc1, len(shortdesc1), shortdesc2, len(shortdesc2)))
730
731                         foundExt = True
732                         # NOTE: only check extended if short description already is a match because otherwise
733                         # it won't evaluate to True anyway
734                         if foundShort and (force or timer.searchForDuplicateDescription > 1) and extdesc1 and extdesc2:
735                                 sequenceMatcher.set_seqs(extdesc1, extdesc2)
736                                 ratio = sequenceMatcher.ratio()
737                                 doDebug("extdesc ratio %f - %s - %d - %s - %d" % (ratio, extdesc1, len(extdesc1), extdesc2, len(extdesc2)))
738                                 foundExt = (0.9 < ratio)
739                                 if foundExt:
740                                         doLog("extdesc ratio %f - %s - %d - %s - %d" % (ratio, extdesc1, len(extdesc1), extdesc2, len(extdesc2)))
741                         return foundShort and foundExt