AutoTimer: Show SeriesPlugin result after all autotimers are handled and if simulate...
[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(timerbegin, timerend, begin, end):
57         if begin <= timerbegin <= end:
58                 return end - timerbegin
59         elif timerbegin <= begin <= timerend:
60                 return timerend - 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                 # Search EPG, default to empty list
211                 epgmatches = epgcache.search( ('RITBDSE', 1000, typeMap[timer.searchType], timer.match, caseMap[timer.searchCase]) ) or []
212
213                 # Sort list of tuples by begin time 'B'
214                 epgmatches.sort(key=itemgetter(3))
215
216                 # Contains the the marked similar eits and the conflicting strings
217                 similardict = defaultdict(list)         
218
219                 # Loop over all EPG matches
220                 for idx, ( serviceref, eit, name, begin, duration, shortdesc, extdesc ) in enumerate( epgmatches ):
221                         
222                         startLog()
223                         
224                         # timer destination dir
225                         dest = timer.destination or config.usage.default_path.value
226                         
227                         evtBegin = begin
228                         evtEnd = end = begin + duration
229
230                         doLog("possible epgmatch %s" % (name))
231                         doLog("Serviceref %s" % (str(serviceref)))
232                         eserviceref = eServiceReference(serviceref)
233                         evt = epgcache.lookupEventId(eserviceref, eit)
234                         if not evt:
235                                 doLog("Could not create Event!")
236                                 skipped.append((name, begin, end, str(serviceref), timer.name, getLog()))
237                                 continue
238                         # Try to determine real service (we always choose the last one)
239                         n = evt.getNumOfLinkageServices()
240                         if n > 0:
241                                 i = evt.getLinkageService(eserviceref, n-1)
242                                 serviceref = i.toString()
243                                 doLog("Serviceref2 %s" % (str(serviceref)))
244
245                         # If event starts in less than 60 seconds skip it
246                         if begin < time() + 60:
247                                 doLog("Skipping an event because it starts in less than 60 seconds")
248                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
249                                 continue
250
251                         # Convert begin time
252                         timestamp = localtime(begin)
253                         # Update timer
254                         timer.update(begin, timestamp)
255
256                         # Check if eit is in similar matches list
257                         # NOTE: ignore evtLimit for similar timers as I feel this makes the feature unintuitive
258                         similarTimer = False
259                         if eit in similardict:
260                                 similarTimer = True
261                                 dayofweek = None # NOTE: ignore day on similar timer
262                         else:
263                                 # If maximum days in future is set then check time
264                                 if checkEvtLimit:
265                                         if begin > evtLimit:
266                                                 doLog("Skipping an event because of maximum days in future is reached")
267                                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
268                                                 continue
269
270                                 dayofweek = str(timestamp.tm_wday)
271
272                         # Check timer conditions
273                         # NOTE: similar matches do not care about the day/time they are on, so ignore them
274                         if timer.checkServices(serviceref):
275                                 doLog("Skipping an event because of check services")
276                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
277                                 continue
278                         if timer.checkDuration(duration):
279                                 doLog("Skipping an event because of duration check")
280                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
281                                 continue
282                         if not similarTimer:
283                                 if timer.checkTimespan(timestamp):
284                                         doLog("Skipping an event because of timestamp check")
285                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
286                                         continue
287                                 if timer.checkTimeframe(begin):
288                                         doLog("Skipping an event because of timeframe check")
289                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
290                                         continue
291
292                         # Initialize
293                         newEntry = None
294                         oldExists = False
295                         
296                         # Eventually change service to alternative
297                         if timer.overrideAlternatives:
298                                 serviceref = timer.getAlternative(serviceref)
299
300                         if timer.series_labeling and sp_getSeasonEpisode is not None:
301                                 #doLog("Request name, desc, path %s %s %s" % (name,shortdesc,dest))
302                                 sp = sp_getSeasonEpisode(serviceref, name, evtBegin, evtEnd, shortdesc, dest)
303                                 if sp and type(sp) in (tuple, list) and len(sp) == 4:
304                                         name = sp[0] or name
305                                         shortdesc = sp[1] or shortdesc
306                                         dest = sp[2] or dest
307                                         doLog(str(sp[3]))
308                                         #doLog("Returned name, desc, path %s %s %s" % (name,shortdesc,dest))
309                                 else:
310                                         # Nothing found
311                                         doLog(str(sp))
312                                         
313                                         # If AutoTimer name not equal match, do a second lookup with the name
314                                         if timer.name.lower() != timer.match.lower():
315                                                 #doLog("Request name, desc, path %s %s %s" % (timer.name,shortdesc,dest))
316                                                 sp = sp_getSeasonEpisode(serviceref, timer.name, evtBegin, evtEnd, shortdesc, dest)
317                                                 if sp and type(sp) in (tuple, list) and len(sp) == 4:
318                                                         name = sp[0] or name
319                                                         shortdesc = sp[1] or shortdesc
320                                                         dest = sp[2] or dest
321                                                         doLog(str(sp[3]))
322                                                         #doLog("Returned name, desc, path %s %s %s" % (name,shortdesc,dest))
323                                                 else:
324                                                         doLog(str(sp))
325
326                         if timer.checkFilter(name, shortdesc, extdesc, dayofweek):
327                                 doLog("Skipping an event because of filter check")
328                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
329                                 continue
330                         
331                         if timer.hasOffset():
332                                 # Apply custom Offset
333                                 begin, end = timer.applyOffset(begin, end)
334                         else:
335                                 # Apply E2 Offset
336                                 begin -= config.recording.margin_before.value * 60
337                                 end += config.recording.margin_after.value * 60
338
339                         # Overwrite endtime if requested
340                         if timer.justplay and not timer.setEndtime:
341                                 end = begin
342
343                         # Check for existing recordings in directory
344                         if timer.avoidDuplicateDescription == 3:
345                                 # Reset movie Exists
346                                 movieExists = False
347
348                                 if dest and dest not in moviedict:
349                                         self.addDirectoryToMovieDict(moviedict, dest, serviceHandler)
350                                 for movieinfo in moviedict.get(dest, ()):
351                                         if self.checkDuplicates(timer, name, movieinfo.get("name"), shortdesc, movieinfo.get("shortdesc"), extdesc, movieinfo.get("extdesc") ):
352                                                 doLog("We found a matching recorded movie, skipping event:", name)
353                                                 movieExists = True
354                                                 break
355                                 if movieExists:
356                                         doLog("Skipping an event because movie already exists")
357                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
358                                         continue
359
360                         # Check for double Timers
361                         # We first check eit and if user wants us to guess event based on time
362                         # we try this as backup. The allowed diff should be configurable though.
363                         for rtimer in timerdict.get(serviceref, ()):
364                                 if rtimer.eit == eit:
365                                         oldExists = True
366                                         doLog("We found a timer based on eit")
367                                         newEntry = rtimer
368                                         break
369                                 elif config.plugins.autotimer.try_guessing.value:
370                                         if timer.hasOffset():
371                                                 # Remove custom Offset
372                                                 rbegin = rtimer.begin + timer.offset[0] * 60
373                                                 rend = rtimer.end - timer.offset[1] * 60
374                                         else:
375                                                 # Remove E2 Offset
376                                                 rbegin = rtimer.begin + config.recording.margin_before.value * 60
377                                                 rend = rtimer.end - config.recording.margin_after.value * 60
378                                         # As alternative we could also do a epg lookup
379                                         #revent = epgcache.lookupEventId(rtimer.service_ref.ref, rtimer.eit)
380                                         #rbegin = revent.getBeginTime() or 0
381                                         #rduration = revent.getDuration() or 0
382                                         #rend = rbegin + rduration or 0
383                                         if getTimeDiff(rbegin, rend, evtBegin, evtEnd) > ((duration/10)*8):
384                                                 oldExists = True
385                                                 doLog("We found a timer based on time guessing")
386                                                 newEntry = rtimer
387                                                 break
388                                 if timer.avoidDuplicateDescription >= 1 \
389                                         and not rtimer.disabled:
390                                                 if self.checkDuplicates(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
391                                                 # if searchForDuplicateDescription > 1 then check short description
392                                                         oldExists = True
393                                                         doLog("We found a timer (similar service) with same description, skipping event")
394                                                         break
395
396                         # We found no timer we want to edit
397                         if newEntry is None:
398                                 # But there is a match
399                                 if oldExists:
400                                         doLog("Skipping an event because a timer on same service exists")
401                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
402                                         continue
403
404                                 # We want to search for possible doubles
405                                 if timer.avoidDuplicateDescription >= 2:
406                                         for rtimer in chain.from_iterable( itervalues(timerdict) ):
407                                                 if not rtimer.disabled:
408                                                         if self.checkDuplicates(timer, name, rtimer.name, shortdesc, rtimer.description, extdesc, rtimer.extdesc ):
409                                                                 oldExists = True
410                                                                 doLog("We found a timer (any service) with same description, skipping event")
411                                                                 break
412                                         if oldExists:
413                                                 doLog("Skipping an event because a timer on any service exists")
414                                                 skipped.append((name, begin, end, serviceref, timer.name, getLog()))
415                                                 continue
416
417                                 if timer.checkCounter(timestamp):
418                                         doLog("Not adding new timer because counter is depleted.")
419                                         skipped.append((name, begin, end, serviceref, timer.name, getLog()))
420                                         continue
421
422                         # Append to timerlist and abort if simulating
423                         timers.append((name, begin, end, serviceref, timer.name, getLog()))
424                         if simulateOnly:
425                                 continue
426
427                         if newEntry is not None:
428                                 # Abort if we don't want to modify timers or timer is repeated
429                                 if config.plugins.autotimer.refresh.value == "none" or newEntry.repeated:
430                                         doLog("Won't modify existing timer because either no modification allowed or repeated timer")
431                                         continue
432
433                                 if hasattr(newEntry, "isAutoTimer"):
434                                         msg = "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name)
435                                         doLog(msg)
436                                         newEntry.log(501, msg)
437                                 elif config.plugins.autotimer.add_autotimer_to_tags.value and TAG in newEntry.tags:
438                                         msg = "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name)
439                                         doLog(msg)
440                                         newEntry.log(501, msg)
441                                 else:
442                                         if config.plugins.autotimer.refresh.value != "all":
443                                                 doLog("Won't modify existing timer because it's no timer set by us")
444                                                 continue
445
446                                         msg = "[AutoTimer] Warning, AutoTimer %s messed with a timer which might not belong to it: %s ." % (timer.name, newEntry.name)
447                                         doLog(msg)
448                                         newEntry.log(501, msg)
449
450                                 modified += 1
451
452                                 self.modifyTimer(newEntry, name, shortdesc, begin, end, serviceref, eit)
453                                 msg = "[AutoTimer] AutoTimer modified timer: %s ." % (newEntry.name)
454                                 doLog(msg)
455                                 newEntry.log(501, msg)
456                                 
457                         else:
458                                 newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, shortdesc, eit)
459                                 msg = "[AutoTimer] Try to add new timer based on AutoTimer %s." % (timer.name)
460                                 doLog(msg)
461                                 newEntry.log(500, msg)
462                                 
463                                 # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
464                                 # It is only temporarily, after a restart it will be lost,
465                                 # because it won't be stored in the timer xml file
466                                 newEntry.isAutoTimer = True
467
468                         # Apply afterEvent
469                         if timer.hasAfterEvent():
470                                 afterEvent = timer.getAfterEventTimespan(localtime(end))
471                                 if afterEvent is None:
472                                         afterEvent = timer.getAfterEvent()
473                                 if afterEvent is not None:
474                                         newEntry.afterEvent = afterEvent
475
476                         newEntry.dirname = dest
477                         newEntry.calculateFilename()
478
479                         newEntry.justplay = timer.justplay
480                         newEntry.vpsplugin_enabled = timer.vps_enabled
481                         newEntry.vpsplugin_overwrite = timer.vps_overwrite
482                         tags = timer.tags[:]
483                         if config.plugins.autotimer.add_autotimer_to_tags.value:
484                                 if TAG not in tags:
485                                         tags.append(TAG)
486                         if config.plugins.autotimer.add_name_to_tags.value:
487                                 tagname = timer.name.strip()
488                                 if tagname:
489                                         tagname = tagname[0].upper() + tagname[1:].replace(" ", "_")
490                                         if tagname not in tags:
491                                                 tags.append(tagname)
492                         newEntry.tags = tags
493
494                         if oldExists:
495                                 # XXX: this won't perform a sanity check, but do we actually want to do so?
496                                 recordHandler.timeChanged(newEntry)
497
498                         else:
499                                 conflictString = ""
500                                 if similarTimer:
501                                         conflictString = similardict[eit].conflictString
502                                         msg = "[AutoTimer] Try to add similar Timer because of conflicts with %s." % (conflictString)
503                                         doLog(msg)
504                                         newEntry.log(504, msg)
505
506                                 # Try to add timer
507                                 conflicts = recordHandler.record(newEntry)
508
509                                 if conflicts:
510                                         # Maybe use newEntry.log
511                                         conflictString += ' / '.join(["%s (%s)" % (x.name, strftime("%Y%m%d %H%M", localtime(x.begin))) for x in conflicts])
512                                         doLog("conflict with %s detected" % (conflictString))
513
514                                         if config.plugins.autotimer.addsimilar_on_conflict.value:
515                                                 # We start our search right after our actual index
516                                                 # Attention we have to use a copy of the list, because we have to append the previous older matches
517                                                 lepgm = len(epgmatches)
518                                                 for i in xrange(lepgm):
519                                                         servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS = epgmatches[ (i+idx+1)%lepgm ]
520                                                         if self.checkDuplicates(timer, name, nameS, shortdesc, shortdescS, extdesc, extdescS, force=True ):
521                                                                 # Check if the similar is already known
522                                                                 if eitS not in similardict:
523                                                                         doLog("Found similar Timer: " + name)
524
525                                                                         # Store the actual and similar eit and conflictString, so it can be handled later
526                                                                         newEntry.conflictString = conflictString
527                                                                         similardict[eit] = newEntry
528                                                                         similardict[eitS] = newEntry
529                                                                         similarTimer = True
530                                                                         if beginS <= evtBegin:
531                                                                                 # Event is before our actual epgmatch so we have to append it to the epgmatches list
532                                                                                 epgmatches.append((servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS))
533                                                                         # If we need a second similar it will be found the next time
534                                                                 else:
535                                                                         similarTimer = False
536                                                                         newEntry = similardict[eitS]
537                                                                 break
538
539                                 if conflicts is None:
540                                         timer.decrementCounter()
541                                         new += 1
542                                         newEntry.extdesc = extdesc
543                                         timerdict[serviceref].append(newEntry)
544
545                                         # Similar timers are in new timers list and additionally in similar timers list
546                                         if similarTimer:
547                                                 similars.append((name, begin, end, serviceref, timer.name))
548                                                 similardict.clear()
549
550                                 # Don't care about similar timers
551                                 elif not similarTimer:
552                                         conflicting.append((name, begin, end, serviceref, timer.name))
553
554                                         if config.plugins.autotimer.disabled_on_conflict.value:
555                                                 msg = "[AutoTimer] Timer disabled because of conflicts with %s." % (conflictString)
556                                                 doLog(msg)
557                                                 newEntry.log(503, msg)
558                                                 newEntry.disabled = True
559                                                 # We might want to do the sanity check locally so we don't run it twice - but I consider this workaround a hack anyway
560                                                 conflicts = recordHandler.record(newEntry)
561                 
562                 return (new, modified)
563
564         def parseEPG(self, simulateOnly=False, uniqueId=None, callback=None):
565
566                 from plugin import AUTOTIMER_VERSION
567                 doLog("AutoTimer Version: " + AUTOTIMER_VERSION)
568
569                 if NavigationInstance.instance is None:
570                         doLog("Navigation is not available, can't parse EPG")
571                         return (0, 0, 0, [], [], [])
572
573                 new = 0
574                 modified = 0
575                 timers = []
576                 conflicting = []
577                 similars = []
578                 skipped = []
579
580                 if currentThread().getName() == 'MainThread':
581                         doBlockingCallFromMainThread = lambda f, *a, **kw: f(*a, **kw)
582                 else:
583                         doBlockingCallFromMainThread = blockingCallFromMainThread
584
585                 # NOTE: the config option specifies "the next X days" which means today (== 1) + X
586                 delta = timedelta(days = config.plugins.autotimer.maxdaysinfuture.value + 1)
587                 evtLimit = mktime((date.today() + delta).timetuple())
588                 checkEvtLimit = delta.days > 1
589                 del delta
590
591                 # Read AutoTimer configuration
592                 self.readXml()
593
594                 # Get E2 instances
595                 epgcache = eEPGCache.getInstance()
596                 serviceHandler = eServiceCenter.getInstance()
597                 recordHandler = NavigationInstance.instance.RecordTimer
598
599                 # Save Timer in a dict to speed things up a little
600                 # We include processed timers as we might search for duplicate descriptions
601                 # NOTE: It is also possible to use RecordTimer isInTimer(), but we won't get the timer itself on a match
602                 timerdict = defaultdict(list)
603                 doBlockingCallFromMainThread(self.populateTimerdict, epgcache, recordHandler, timerdict)
604
605                 # Create dict of all movies in all folders used by an autotimer to compare with recordings
606                 # The moviedict will be filled only if one AutoTimer is configured to avoid duplicate description for any recordings
607                 moviedict = defaultdict(list)
608
609                 # Iterate Timer
610                 for timer in self.getEnabledTimerList():
611                         if uniqueId == None or timer.id == uniqueId:
612                                 tup = doBlockingCallFromMainThread(self.parseTimer, timer, epgcache, serviceHandler, recordHandler, checkEvtLimit, evtLimit, timers, conflicting, similars, skipped, timerdict, moviedict, simulateOnly=simulateOnly)
613                                 if callback:
614                                         callback(timers, conflicting, similars, skipped)
615                                         del timers[:]
616                                         del conflicting[:]
617                                         del similars[:]
618                                         del skipped[:]
619                                 else:
620                                         new += tup[0]
621                                         modified += tup[1]
622                 
623                 if not simulateOnly:
624                         if sp_showResult is not None:
625                                 sp_showResult()
626                 
627                 return (len(timers), new, modified, timers, conflicting, similars)
628
629 # Supporting functions
630
631         def populateTimerdict(self, epgcache, recordHandler, timerdict):
632                 remove = []
633                 for timer in chain(recordHandler.timer_list, recordHandler.processed_timers):
634                         if timer and timer.service_ref:
635                                 if timer.eit is not None:
636                                         event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit)
637                                         if event:
638                                                 timer.extdesc = event.getExtendedDescription()
639                                         else:
640                                                 remove.append(timer)
641                                 else:
642                                         remove.append(timer)
643                                         continue
644
645                                 if not hasattr(timer, 'extdesc'):
646                                         timer.extdesc = ''
647
648                                 timerdict[str(timer.service_ref)].append(timer)
649
650                 if config.plugins.autotimer.check_eit_and_remove.value:
651                         for timer in remove:
652                                 if hasattr(timer, "isAutoTimer") or (config.plugins.autotimer.add_autotimer_to_tags.value and TAG in timer.tags):
653                                         try:
654                                                 # Because of the duplicate check, we only want to remove future timer
655                                                 if timer in recordHandler.timer_list:
656                                                         if not timer.isRunning():
657                                                                 global NavigationInstance
658                                                                 doLog("Remove timer because of eit check " + timer.name)
659                                                                 NavigationInstance.instance.RecordTimer.removeEntry(timer)
660                                         except:
661                                                 pass
662                 del remove
663
664         def modifyTimer(self, timer, name, shortdesc, begin, end, serviceref, eit=None):
665                 # Only update the name and description if we got a "new" one
666                 if len(timer.name) < len(name):
667                         timer.name = name
668                 if len(timer.description) < len(shortdesc):
669                         timer.description = shortdesc
670                 timer.begin = int(begin)
671                 timer.end = int(end)
672                 timer.service_ref = ServiceReference(serviceref)
673                 if eit:
674                         timer.eit = eit
675
676         def addDirectoryToMovieDict(self, moviedict, dest, serviceHandler):
677                 movielist = serviceHandler.list(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + dest))
678                 if movielist is None:
679                         doLog("listing of movies in " + dest + " failed")
680                 else:
681                         append = moviedict[dest].append
682                         while 1:
683                                 movieref = movielist.getNext()
684                                 if not movieref.valid():
685                                         break
686                                 if movieref.flags & eServiceReference.mustDescent:
687                                         continue
688                                 info = serviceHandler.info(movieref)
689                                 if info is None:
690                                         continue
691                                 event = info.getEvent(movieref)
692                                 if event is None:
693                                         continue
694                                 append({
695                                         "name": info.getName(movieref),
696                                         "shortdesc": info.getInfoString(movieref, iServiceInformation.sDescription),
697                                         "extdesc": event.getExtendedDescription() or '' # XXX: does event.getExtendedDescription() actually return None on no description or an empty string?
698                                 })
699
700         def checkDuplicates(self, timer, name1, name2, shortdesc1, shortdesc2, extdesc1, extdesc2, force=False):
701                 if name1 and name2:
702                         sequenceMatcher = SequenceMatcher(" ".__eq__, name1, name2)
703                 else:
704                         return False
705
706                 ratio = sequenceMatcher.ratio()
707                 doDebug("names ratio %f - %s - %d - %s - %d" % (ratio, name1, len(name1), name2, len(name2)))
708                 if name1 in name2 or (0.8 < ratio): # this is probably a match
709                         foundShort = True
710                         if (force or timer.searchForDuplicateDescription > 0) and shortdesc1 and shortdesc2:
711                                 sequenceMatcher.set_seqs(shortdesc1, shortdesc2)
712                                 ratio = sequenceMatcher.ratio()
713                                 doDebug("shortdesc ratio %f - %s - %d - %s - %d" % (ratio, shortdesc1, len(shortdesc1), shortdesc2, len(shortdesc2)))
714                                 foundShort = shortdesc1 in shortdesc2 or (0.8 < ratio)
715                                 if foundShort:
716                                         doLog("shortdesc ratio %f - %s - %d - %s - %d" % (ratio, shortdesc1, len(shortdesc1), shortdesc2, len(shortdesc2)))
717
718                         foundExt = True
719                         # NOTE: only check extended if short description already is a match because otherwise
720                         # it won't evaluate to True anyway
721                         if foundShort and (force or timer.searchForDuplicateDescription > 1) and extdesc1 and extdesc2:
722                                 sequenceMatcher.set_seqs(extdesc1, extdesc2)
723                                 ratio = sequenceMatcher.ratio()
724                                 doDebug("extdesc ratio %f - %s - %d - %s - %d" % (ratio, extdesc1, len(extdesc1), extdesc2, len(extdesc2)))
725                                 foundExt = (0.8 < ratio)
726                                 if foundExt:
727                                         doLog("extdesc ratio %f - %s - %d - %s - %d" % (ratio, extdesc1, len(extdesc1), extdesc2, len(extdesc2)))
728                         return foundShort and foundExt