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