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