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