AutoTimer: Only remove timer from us
[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                                 raise ValueError("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                                         doLog(str(sp))
310
311                         if timer.checkFilter(name, shortdesc, extdesc, dayofweek):
312                                 doLog("Skipping an event because of filter check")
313                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
314                                 continue
315                         
316                         if timer.hasOffset():
317                                 # Apply custom Offset
318                                 begin, end = timer.applyOffset(begin, end)
319                         else:
320                                 # Apply E2 Offset
321                                 begin -= config.recording.margin_before.value * 60
322                                 end += config.recording.margin_after.value * 60
323
324                         # Overwrite endtime if requested
325                         if timer.justplay and not timer.setEndtime:
326                                 end = begin
327
328                         # Check for existing recordings in directory
329                         if timer.avoidDuplicateDescription == 3:
330                                 # Reset movie Exists
331                                 movieExists = False
332
333                                 if dest and dest not in moviedict:
334                                         self.addDirectoryToMovieDict(moviedict, dest, serviceHandler)
335                                 for movieinfo in moviedict.get(dest, ()):
336                                         if self.checkDuplicates(timer, name, movieinfo.get("name"), shortdesc, movieinfo.get("shortdesc"), extdesc, movieinfo.get("extdesc") ):
337                                                 doLog("We found a matching recorded movie, skipping event:", name)
338                                                 movieExists = True
339                                                 break
340                                 if movieExists:
341                                         doLog("Skipping an event because movie already exists")
342                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
343                                         continue
344
345                         # Check for double Timers
346                         # We first check eit and if user wants us to guess event based on time
347                         # we try this as backup. The allowed diff should be configurable though.
348                         for rtimer in timerdict.get(serviceref, ()):
349                                 if rtimer.eit == eit:
350                                         oldExists = True
351                                         doLog("We found a timer based on eit")
352                                         newEntry = rtimer
353                                         break
354                                 elif config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, evtBegin, evtEnd) > ((duration/10)*8):
355                                         oldExists = True
356                                         doLog("We found a timer based on time guessing")
357                                         newEntry = rtimer
358                                         break
359                                 elif timer.avoidDuplicateDescription >= 1 \
360                                         and not rtimer.disabled:
361                                                 if self.checkDuplicates(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
362                                                 # if searchForDuplicateDescription > 1 then check short description
363                                                         oldExists = True
364                                                         doLog("We found a timer (similar service) with same description, skipping event")
365                                                         break
366
367                         # We found no timer we want to edit
368                         if newEntry is None:
369                                 # But there is a match
370                                 if oldExists:
371                                         doLog("Skipping an event because a timer on same service exists")
372                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
373                                         continue
374
375                                 # We want to search for possible doubles
376                                 if timer.avoidDuplicateDescription >= 2:
377                                         for rtimer in chain.from_iterable( itervalues(timerdict) ):
378                                                 if not rtimer.disabled:
379                                                         if self.checkDuplicates(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
380                                                                 oldExists = True
381                                                                 doLog("We found a timer (any service) with same description, skipping event")
382                                                                 break
383                                         if oldExists:
384                                                 doLog("Skipping an event because a timer on any service exists")
385                                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
386                                                 continue
387
388                                 if timer.checkCounter(timestamp):
389                                         doLog("Not adding new timer because counter is depleted.")
390                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
391                                         continue
392
393                         # Append to timerlist and abort if simulating
394                         timers.append((name, begin, end, serviceref, timer.name, getLog()))
395                         if simulateOnly:
396                                 continue
397
398                         if newEntry is not None:
399                                 # Abort if we don't want to modify timers or timer is repeated
400                                 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
401                                         doLog("Won't modify existing timer because either no modification allowed or repeated timer")
402                                         continue
403
404                                 if hasattr(newEntry, "isAutoTimer") or TAG in newEntry.tags:
405                                         newEntry.log(501, "AutoTimer %s modified this automatically generated timer." % (timer.name))
406                                 else:
407                                         if config.plugins.autotimer.refresh.value != "all":
408                                                 doLog("Won't modify existing timer because it's no timer set by us")
409                                                 continue
410
411                                         newEntry.log(501, "Warning, AutoTimer %s messed with a timer which might not belong to it: %s ." % (timer.name, newEntry.name))
412
413                                 modified += 1
414
415                                 self.modifyTimer(newEntry, name, shortdesc, begin, end, serviceref, eit)
416                                 newEntry.log(501, "AutoTimer modified timer: %s ." % (newEntry.name))
417                                 
418                         else:
419                                 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, shortdesc, eit)
420                                 newEntry.log(500, "Try to add new timer based on AutoTimer %s." % (timer.name))
421
422                                 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
423                                 # It is only temporarily, after a restart it will be lost,
424                                 # because it won't be stored in the timer xml file
425                                 newEntry.isAutoTimer = True
426                                 newEntry.tags.append(TAG)
427
428
429                         # Apply afterEvent
430                         if timer.hasAfterEvent():
431                                 afterEvent = timer.getAfterEventTimespan(localtime(end))
432                                 if afterEvent is None:
433                                         afterEvent = timer.getAfterEvent()
434                                 if afterEvent is not None:
435                                         newEntry.afterEvent = afterEvent
436
437                         newEntry.dirname = dirname or timer.destination
438                         newEntry.justplay = timer.justplay
439                         newEntry.vpsplugin_enabled = timer.vps_enabled
440                         newEntry.vpsplugin_overwrite = timer.vps_overwrite
441                         tags = timer.tags[:]
442                         if config.plugins.autotimer.add_autotimer_to_tags.value:
443                                 tags.append('AutoTimer')
444                         if config.plugins.autotimer.add_name_to_tags.value:
445                                 tagname = timer.name.strip()
446                                 if tagname:
447                                         tagname = tagname[0].upper() + tagname[1:].replace(" ", "_")
448                                         tags.append(tagname)
449                         newEntry.tags = tags
450
451                         if oldExists:
452                                 # XXX: this won't perform a sanity check, but do we actually want to do so?
453                                 recordHandler.timeChanged(newEntry)
454
455                                 #if renameTimer is not None and timer.series_labeling:
456                                 #       renameTimer(newEntry, name, evtBegin, evtEnd)
457
458                         else:
459                                 conflictString = ""
460                                 if similarTimer:
461                                         conflictString = similardict[eit].conflictString
462                                         newEntry.log(504, "Try to add similar Timer because of conflicts with %s." % (conflictString))
463
464                                 # Try to add timer
465                                 conflicts = recordHandler.record(newEntry)
466
467                                 if conflicts:
468                                         # Maybe use newEntry.log
469                                         conflictString += ' / '.join(["%s (%s)" % (x.name, strftime("%Y%m%d %H%M", localtime(x.begin))) for x in conflicts])
470                                         doLog("conflict with %s detected" % (conflictString))
471
472                                         if config.plugins.autotimer.addsimilar_on_conflict.value:
473                                                 # We start our search right after our actual index
474                                                 # Attention we have to use a copy of the list, because we have to append the previous older matches
475                                                 lepgm = len(epgmatches)
476                                                 for i in xrange(lepgm):
477                                                         servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS = epgmatches[ (i+idx+1)%lepgm ]
478                                                         if self.checkDuplicates(timer, name, nameS, shortdesc, shortdescS, extdesc, extdescS, force=True ):
479                                                                 # Check if the similar is already known
480                                                                 if eitS not in similardict:
481                                                                         doLog("Found similar Timer: " + name)
482
483                                                                         # Store the actual and similar eit and conflictString, so it can be handled later
484                                                                         newEntry.conflictString = conflictString
485                                                                         similardict[eit] = newEntry
486                                                                         similardict[eitS] = newEntry
487                                                                         similarTimer = True
488                                                                         if beginS <= evtBegin:
489                                                                                 # Event is before our actual epgmatch so we have to append it to the epgmatches list
490                                                                                 epgmatches.append((servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS))
491                                                                         # If we need a second similar it will be found the next time
492                                                                 else:
493                                                                         similarTimer = False
494                                                                         newEntry = similardict[eitS]
495                                                                 break
496
497                                 if conflicts is None:
498                                         timer.decrementCounter()
499                                         new += 1
500                                         newEntry.extdesc = extdesc
501                                         timerdict[serviceref].append(newEntry)
502
503                                         #if renameTimer is not None and timer.series_labeling:
504                                         #       renameTimer(newEntry, name, evtBegin, evtEnd)
505
506                                         # Similar timers are in new timers list and additionally in similar timers list
507                                         if similarTimer:
508                                                 similars.append((name, begin, end, serviceref, timer.name))
509                                                 similardict.clear()
510
511                                 # Don't care about similar timers
512                                 elif not similarTimer:
513                                         conflicting.append((name, begin, end, serviceref, timer.name))
514
515                                         if config.plugins.autotimer.disabled_on_conflict.value:
516                                                 newEntry.log(503, "Timer disabled because of conflicts with %s." % (conflictString))
517                                                 newEntry.disabled = True
518                                                 # We might want to do the sanity check locally so we don't run it twice - but I consider this workaround a hack anyway
519                                                 conflicts = recordHandler.record(newEntry)
520                 
521                 if sp_showResult is not None:
522                         sp_showResult()
523                 
524                 return (new, modified)
525
526         def parseEPG(self, simulateOnly=False, uniqueId=None, callback=None):
527                 if NavigationInstance.instance is None:
528                         doLog("Navigation is not available, can't parse EPG")
529                         return (0, 0, 0, [], [], [])
530
531                 new = 0
532                 modified = 0
533                 timers = []
534                 conflicting = []
535                 similars = []
536                 skipped = []
537
538                 if currentThread().getName() == 'MainThread':
539                         doBlockingCallFromMainThread = lambda f, *a, **kw: f(*a, **kw)
540                 else:
541                         doBlockingCallFromMainThread = blockingCallFromMainThread
542
543                 # NOTE: the config option specifies "the next X days" which means today (== 1) + X
544                 delta = timedelta(days = config.plugins.autotimer.maxdaysinfuture.value + 1)
545                 evtLimit = mktime((date.today() + delta).timetuple())
546                 checkEvtLimit = delta.days > 1
547                 del delta
548
549                 # Read AutoTimer configuration
550                 self.readXml()
551
552                 # Get E2 instances
553                 epgcache = eEPGCache.getInstance()
554                 serviceHandler = eServiceCenter.getInstance()
555                 recordHandler = NavigationInstance.instance.RecordTimer
556
557                 # Save Timer in a dict to speed things up a little
558                 # We include processed timers as we might search for duplicate descriptions
559                 # NOTE: It is also possible to use RecordTimer isInTimer(), but we won't get the timer itself on a match
560                 timerdict = defaultdict(list)
561                 doBlockingCallFromMainThread(self.populateTimerdict, epgcache, recordHandler, timerdict)
562
563                 # Create dict of all movies in all folders used by an autotimer to compare with recordings
564                 # The moviedict will be filled only if one AutoTimer is configured to avoid duplicate description for any recordings
565                 moviedict = defaultdict(list)
566
567                 # Iterate Timer
568                 for timer in self.getEnabledTimerList():
569                         if uniqueId == None or timer.id == uniqueId:
570                                 tup = doBlockingCallFromMainThread(self.parseTimer, timer, epgcache, serviceHandler, recordHandler, checkEvtLimit, evtLimit, timers, conflicting, similars, skipped, timerdict, moviedict, simulateOnly=simulateOnly)
571                                 if callback:
572                                         callback(timers, conflicting, similars, skipped)
573                                         del timers[:]
574                                         del conflicting[:]
575                                         del similars[:]
576                                         del skipped[:]
577                                 else:
578                                         new += tup[0]
579                                         modified += tup[1]
580
581                 return (len(timers), new, modified, timers, conflicting, similars)
582
583 # Supporting functions
584
585         def populateTimerdict(self, epgcache, recordHandler, timerdict):
586                 remove = []
587                 for timer in chain(recordHandler.timer_list, recordHandler.processed_timers):
588                         if timer and timer.service_ref:
589                                 if timer.eit is not None:
590                                         event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit)
591                                         if event:
592                                                 timer.extdesc = event.getExtendedDescription()
593                                         else:
594                                                 remove.append(timer)
595                                 else:
596                                         remove.append(timer)
597                                         continue
598
599                                 if not hasattr(timer, 'extdesc'):
600                                         timer.extdesc = ''
601
602                                 timerdict[str(timer.service_ref)].append(timer)
603
604                 if config.plugins.autotimer.check_eit_and_remove.value:
605                         for timer in remove:
606                                 if hasattr(timer, "isAutoTimer") or TAG in timer.tags:
607                                         try:
608                                                 # Because of the duplicate check, we only want to remove future timer
609                                                 if timer in recordHandler.timer_list:
610                                                         if not timer.isRunning():
611                                                                 recordHandler.timer_list.remove(timer)
612                                         except:
613                                                 pass
614                 del remove
615
616         def modifyTimer(self, timer, name, shortdesc, begin, end, serviceref, eit=None):
617                 # Don't update the name, it will overwrite the name of the SeriesPlugin
618                 #timer.name = name
619                 #timer.description = shortdesc
620                 timer.begin = int(begin)
621                 timer.end = int(end)
622                 timer.service_ref = ServiceReference(serviceref)
623                 if eit:
624                         timer.eit = eit
625
626         def addDirectoryToMovieDict(self, moviedict, dest, serviceHandler):
627                 movielist = serviceHandler.list(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + dest))
628                 if movielist is None:
629                         doLog("listing of movies in " + dest + " failed")
630                 else:
631                         append = moviedict[dest].append
632                         while 1:
633                                 movieref = movielist.getNext()
634                                 if not movieref.valid():
635                                         break
636                                 if movieref.flags & eServiceReference.mustDescent:
637                                         continue
638                                 info = serviceHandler.info(movieref)
639                                 if info is None:
640                                         continue
641                                 event = info.getEvent(movieref)
642                                 if event is None:
643                                         continue
644                                 append({
645                                         "name": info.getName(movieref),
646                                         "shortdesc": info.getInfoString(movieref, iServiceInformation.sDescription),
647                                         "extdesc": event.getExtendedDescription() or '' # XXX: does event.getExtendedDescription() actually return None on no description or an empty string?
648                                 })
649
650         def checkDuplicates(self, timer, name1, name2, shortdesc1, shortdesc2, extdesc1, extdesc2, force=False):
651                 if name1 and name2:
652                         sequenceMatcher = SequenceMatcher(" ".__eq__, name1, name2)
653                 else:
654                         return False
655
656                 ratio = sequenceMatcher.ratio()
657                 doDebug("names ratio %f - %s - %d - %s - %d" % (ratio, name1, len(name1), name2, len(name2)))
658                 if name1 in name2 or (0.8 < ratio): # this is probably a match
659                         foundShort = True
660                         if (force or timer.searchForDuplicateDescription > 0) and shortdesc1 and shortdesc2:
661                                 sequenceMatcher.set_seqs(shortdesc1, shortdesc2)
662                                 ratio = sequenceMatcher.ratio()
663                                 doDebug("shortdesc ratio %f - %s - %d - %s - %d" % (ratio, shortdesc1, len(shortdesc1), shortdesc2, len(shortdesc2)))
664                                 foundShort = shortdesc1 in shortdesc2 or (0.8 < ratio)
665                                 if foundShort:
666                                         doLog("shortdesc ratio %f - %s - %d - %s - %d" % (ratio, shortdesc1, len(shortdesc1), shortdesc2, len(shortdesc2)))
667
668                         foundExt = True
669                         # NOTE: only check extended if short description already is a match because otherwise
670                         # it won't evaluate to True anyway
671                         if foundShort and (force or timer.searchForDuplicateDescription > 1) and extdesc1 and extdesc2:
672                                 sequenceMatcher.set_seqs(extdesc1, extdesc2)
673                                 ratio = sequenceMatcher.ratio()
674                                 doDebug("extdesc ratio %f - %s - %d - %s - %d" % (ratio, extdesc1, len(extdesc1), extdesc2, len(extdesc2)))
675                                 foundExt = (0.8 < ratio)
676                                 if foundExt:
677                                         doLog("extdesc ratio %f - %s - %d - %s - %d" % (ratio, extdesc1, len(extdesc1), extdesc2, len(extdesc2)))
678                         return foundShort and foundExt