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