Similar timer will be added if it is before the regular autotimer
[enigma2-plugins.git] / autotimer / src / AutoTimer.py
1 # Plugins Config
2 from xml.etree.cElementTree import parse as cet_parse
3 from os import path as os_path
4 from AutoTimerConfiguration import parseConfig, buildConfig
5
6 # Navigation (RecordTimer)
7 import NavigationInstance
8
9 # Timer
10 from ServiceReference import ServiceReference
11 from RecordTimer import RecordTimerEntry
12 from Components.TimerSanityCheck import TimerSanityCheck
13
14 # Timespan
15 from time import localtime, strftime, time, mktime
16 from datetime import timedelta, date
17
18 # EPGCache & Event
19 from enigma import eEPGCache, eServiceReference, eServiceCenter, iServiceInformation
20
21 # Enigma2 Config
22 from Components.config import config
23
24 # AutoTimer Component
25 from AutoTimerComponent import preferredAutoTimerComponent
26
27 from itertools import chain
28 from collections import defaultdict
29
30 XML_CONFIG = "/etc/enigma2/autotimer.xml"
31
32 def getTimeDiff(timer, begin, end):
33         if begin <= timer.begin <= end:
34                 return end - timer.begin
35         elif timer.begin <= begin <= timer.end:
36                 return timer.end - begin
37         return 0
38
39 typeMap = {
40         "exact": eEPGCache.EXAKT_TITLE_SEARCH,
41         "partial": eEPGCache.PARTIAL_TITLE_SEARCH
42 }
43
44 caseMap = {
45         "sensitive": eEPGCache.CASE_CHECK,
46         "insensitive": eEPGCache.NO_CASE_CHECK
47 }
48
49 class AutoTimer:
50         """Read and save xml configuration, query EPGCache"""
51
52         def __init__(self):
53                 # Initialize
54                 self.timers = []
55                 self.configMtime = -1
56                 self.uniqueTimerId = 0
57                 self.defaultTimer = preferredAutoTimerComponent(
58                         0,              # Id
59                         "",             # Name
60                         "",             # Match
61                         True    # Enabled
62                 )
63
64 # Configuration
65
66         def readXml(self):
67                 # Abort if no config found
68                 if not os_path.exists(XML_CONFIG):
69                         print "[AutoTimer] No configuration file present"
70                         return
71
72                 # Parse if mtime differs from whats saved
73                 mtime = os_path.getmtime(XML_CONFIG)
74                 if mtime == self.configMtime:
75                         print "[AutoTimer] No changes in configuration, won't parse"
76                         return
77
78                 # Save current mtime
79                 self.configMtime = mtime
80
81                 # Parse Config
82                 configuration = cet_parse(XML_CONFIG).getroot()
83
84                 # Empty out timers and reset Ids
85                 del self.timers[:]
86                 self.defaultTimer.clear(-1, True)
87
88                 parseConfig(
89                         configuration,
90                         self.timers,
91                         configuration.get("version"),
92                         0,
93                         self.defaultTimer
94                 )
95                 self.uniqueTimerId = len(self.timers)
96
97         def getXml(self):
98                 return buildConfig(self.defaultTimer, self.timers, webif = True)
99
100         def writeXml(self):
101                 file = open(XML_CONFIG, 'w')
102                 file.writelines(buildConfig(self.defaultTimer, self.timers))
103                 file.close()
104
105 # Manage List
106
107         def add(self, timer):
108                 self.timers.append(timer)
109
110         def getEnabledTimerList(self):
111                 return (x for x in self.timers if x.enabled)
112
113         def getTimerList(self):
114                 return self.timers
115
116         def getTupleTimerList(self):
117                 lst = self.timers
118                 return [(x,) for x in lst]
119
120         def getSortedTupleTimerList(self):
121                 lst = self.timers[:]
122                 lst.sort()
123                 return [(x,) for x in lst]
124
125         def getUniqueId(self):
126                 self.uniqueTimerId += 1
127                 return self.uniqueTimerId
128
129         def remove(self, uniqueId):
130                 idx = 0
131                 for timer in self.timers:
132                         if timer.id == uniqueId:
133                                 self.timers.pop(idx)
134                                 return
135                         idx += 1
136
137         def set(self, timer):
138                 idx = 0
139                 for stimer in self.timers:
140                         if stimer == timer:
141                                 self.timers[idx] = timer
142                                 return
143                         idx += 1
144                 self.timers.append(timer)
145
146 # Main function
147
148         def parseEPG(self, simulateOnly = False):
149                 if NavigationInstance.instance is None:
150                         print "[AutoTimer] Navigation is not available, can't parse EPG"
151                         return (0, 0, 0, [], [], [])
152
153                 total = 0
154                 new = 0
155                 modified = 0
156                 timers = []
157                 conflicting = []
158                 similar = {}                    # Contains the the marked similar servicerefs and the conflicting string
159                 similars = []                   # Contains the added similar timers 
160
161                 # NOTE: the config option specifies "the next X days" which means today (== 1) + X
162                 delta = timedelta(days = config.plugins.autotimer.maxdaysinfuture.value + 1)
163                 evtLimit = mktime((date.today() + delta).timetuple())
164                 checkEvtLimit = delta.days > 1
165                 del delta
166
167                 # Read AutoTimer configuration
168                 self.readXml()
169
170                 # Get E2 instances
171                 epgcache = eEPGCache.getInstance()
172                 serviceHandler = eServiceCenter.getInstance()
173                 recordHandler = NavigationInstance.instance.RecordTimer
174
175                 # Create dict of all movies in all folders used by an autotimer to compare with recordings
176                 # The moviedict will be filled only if one AutoTimer is configured to avoid duplicate description for any recordings
177                 moviedict = defaultdict(list)
178
179                 # Save Recordings in a dict to speed things up a little
180                 # We include processed timers as we might search for duplicate descriptions
181                 # The recordict is always filled
182                 recorddict = defaultdict(list)
183                 for timer in ( recordHandler.timer_list + recordHandler.processed_timers ):
184                         if timer and timer.service_ref:
185                                 event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit)
186                                 extdesc = event and event.getExtendedDescription()
187                                 timer.extdesc = extdesc
188                                 recorddict[str(timer.service_ref)].append(timer)
189
190                 # Iterate Timer
191                 for timer in self.getEnabledTimerList():
192                         # Precompute timer destination dir
193                         dest = timer.destination or config.usage.default_path.value
194
195                         # Workaround to allow search for umlauts if we know the encoding
196                         match = timer.match
197                         if timer.encoding != 'UTF-8':
198                                 try:
199                                         match = match.decode('UTF-8').encode(timer.encoding)
200                                 except UnicodeDecodeError:
201                                         pass
202
203                         # Search EPG, default to empty list
204                         epgmatches = epgcache.search(('RITBDSE', 500, typeMap[timer.searchType], match, caseMap[timer.searchCase])) or []
205                         # Sort list of tuples by begin time 'B'
206                         epgmatches.sort(key = lambda x: x[3]) 
207
208                         # Reset the the marked similar servicerefs
209                         similar.clear()
210
211                         # Loop over all EPG matches
212                         for idx, ( serviceref, eit, name, begin, duration, shortdesc, extdesc ) in enumerate( epgmatches ):
213                                 #print "TEST AT epgmatches idx " + str(idx)
214                                 # Reset similarMatch and conflictString
215                                 similarMatch = False
216                                 conflictString = ""
217                                 
218                                 # Check if serviceref is in similar matches list
219                                 if eit in similar:
220                                         similarMatch = True
221                                         conflictString = similar[eit]
222
223                                 eserviceref = eServiceReference(serviceref)
224                                 evt = epgcache.lookupEventId(eserviceref, eit)
225                                 if not evt:
226                                         print "[AutoTimer] Could not create Event!"
227                                         continue
228
229                                 # Try to determine real service (we always choose the last one)
230                                 n = evt.getNumOfLinkageServices()
231                                 if n > 0:
232                                         i = evt.getLinkageService(eserviceref, n-1)
233                                         serviceref = i.toString()
234
235                                 evtBegin = begin
236                                 evtEnd = end = begin + duration
237                                 
238                                 # If event starts in less than 60 seconds skip it
239                                 if begin < time() + 60:
240                                         print "[AutoTimer] Skipping an event because it starts in less than 60 seconds"
241                                         continue
242
243                                 # If maximum days in future is set then check time
244                                 if checkEvtLimit:
245                                         if begin > evtLimit:
246                                                 continue
247
248                                 # Convert begin time
249                                 timestamp = localtime(begin)
250                                 # Update timer
251                                 timer.update(begin, timestamp)
252
253                                 # Check Duration, Timespan, Timeframe and Excludes
254                                 # Remove similarMatch if You want perform the check also on similar timers
255                                 if timer.checkServices(serviceref) \
256                                         or timer.checkDuration(duration) \
257                                         or ( not similarMatch and timer.checkTimespan(timestamp) ) \
258                                         or ( not similarMatch and timer.checkTimeframe(begin) ) \
259                                         or timer.checkFilter(name, shortdesc, extdesc, ( not similarMatch and str(timestamp.tm_wday) ) ):
260                                         continue
261
262                                 if timer.hasOffset():
263                                         # Apply custom Offset
264                                         begin, end = timer.applyOffset(begin, end)
265                                 else:
266                                         # Apply E2 Offset
267                                         begin -= config.recording.margin_before.value * 60
268                                         end += config.recording.margin_after.value * 60
269
270                                 # Eventually change service to alternative
271                                 if timer.overrideAlternatives:
272                                         serviceref = timer.getAlternative(serviceref)
273
274                                 total += 1
275
276                                 # Append to timerlist and abort if simulating
277                                 #if not similarMatch: # Do we want the similar timers in the timers list?
278                                 timers.append((name, begin, end, serviceref, timer.name))
279                                 if simulateOnly:
280                                         continue
281
282                                 # Check for existing recordings in directory
283                                 if timer.avoidDuplicateDescription == 3:
284                                         # Reset movie Exists
285                                         movieExists = False
286
287                                         # Eventually create cache
288                                         if dest and dest not in moviedict:
289                                                 movielist = serviceHandler.list(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + dest))
290                                                 if movielist is None:
291                                                         print "[AutoTimer] listing of movies in " + dest + " failed"
292                                                 else:
293                                                         append = moviedict[dest].append
294                                                         while 1:
295                                                                 movieref = movielist.getNext()
296                                                                 if not movieref.valid():
297                                                                         break
298                                                                 if movieref.flags & eServiceReference.mustDescent:
299                                                                         continue
300                                                                 info = serviceHandler.info(movieref)
301                                                                 if info is None:
302                                                                         continue
303                                                                 event = info.getEvent(movieref)
304                                                                 if event is None:
305                                                                         continue
306                                                                 append({
307                                                                         "name": info.getName(movieref),
308                                                                         "shortdesc": info.getInfoString(movieref, iServiceInformation.sDescription),
309                                                                         "extdesc": event.getExtendedDescription()
310                                                                 })
311                                                         del append
312
313                                         for movieinfo in moviedict.get(dest, []):
314                                                 if movieinfo.get("name") == name \
315                                                         and movieinfo.get("shortdesc") == shortdesc \
316                                                         and movieinfo.get("extdesc") == extdesc:
317                                                         print "[AutoTimer] We found a matching recorded movie, skipping event:", name
318                                                         movieExists = True
319                                                         break
320
321                                         if movieExists:
322                                                 continue
323
324                                 # Initialize
325                                 newEntry = None
326                                 oldExists = False
327                                 similarExists = False           # Indicates if we found a matching similar timer
328
329                                 # Check for double Timers
330                                 # We first check eit and if user wants us to guess event based on time
331                                 # we try this as backup. The allowed diff should be configurable though.
332                                 for rtimer in recorddict.get(serviceref, []):
333                                         if rtimer.eit == eit or config.plugins.autotimer.try_guessing.value and getTimeDiff(rtimer, evtBegin, evtEnd) > ((duration/10)*8):
334                                                 oldExists = True
335
336                                                 # Abort if we don't want to modify timers or timer is repeated
337                                                 if config.plugins.autotimer.refresh.value == "none" or rtimer.repeated:
338                                                         print "[AutoTimer] Won't modify existing timer because either no modification allowed or repeated timer"
339                                                         break
340
341                                                 if hasattr(rtimer, "isAutoTimer"):
342                                                         rtimer.log(501, "[AutoTimer] AutoTimer %s modified this automatically generated timer." % (timer.name,))
343                                                 else:
344                                                         if config.plugins.autotimer.refresh.value != "all":
345                                                                 print "[AutoTimer] Won't modify existing timer because it's no timer set by us"
346                                                                 break
347
348                                                         rtimer.log(501, "[AutoTimer] Warning, AutoTimer %s messed with a timer which might not belong to it." % (timer.name,))
349
350                                                 newEntry = rtimer
351                                                 modified += 1
352
353                                                 # Modify values saved in timer
354                                                 newEntry.name = name
355                                                 newEntry.description = shortdesc
356                                                 newEntry.begin = int(begin)
357                                                 newEntry.end = int(end)
358                                                 newEntry.service_ref = ServiceReference(serviceref)
359
360                                                 # Don't store the extended description within the timer
361                                                 if hasattr(newEntry, "extdesc"):
362                                                         del newEntry.extdesc
363
364                                                 break
365                                         elif timer.avoidDuplicateDescription >= 1 \
366                                                 and not rtimer.disabled \
367                                                 and rtimer.name == name \
368                                                 and rtimer.description == shortdesc \
369                                                 and hasattr(rtimer, "extdesc") and rtimer.extdesc == extdesc:
370                                                 oldExists = True
371                                                 print "[AutoTimer] We found a timer (similar service) with same description, skipping event"
372                                                 break
373
374                                 # We found no timer we want to edit
375                                 if newEntry is None:
376                                         # But there is a match
377                                         if oldExists:
378                                                 continue
379
380                                         # We want to search for possible doubles
381                                         if timer.avoidDuplicateDescription >= 2:
382                                                 for rtimer in chain.from_iterable( recorddict.values() ):
383                                                         if not rtimer.disabled \
384                                                                 and rtimer.name == name \
385                                                                 and rtimer.description == shortdesc \
386                                                                 and hasattr(rtimer, "extdesc") and rtimer.extdesc == extdesc:
387                                                                 oldExists = True
388                                                                 print "[AutoTimer] We found a timer (any service) with same description, skipping event"
389                                                                 break
390                                                 if oldExists:
391                                                         continue
392
393                                         if timer.checkCounter(timestamp):
394                                                 print "[AutoTimer] Not adding new timer because counter is depleted."
395                                                 continue
396
397                                         newEntry = RecordTimerEntry(ServiceReference(serviceref), begin, end, name, shortdesc, eit)
398                                         newEntry.log(500, "[AutoTimer] Adding new timer based on AutoTimer %s." % (timer.name,))
399
400                                         # Mark this entry as AutoTimer (only AutoTimers will have this Attribute set)
401                                         newEntry.isAutoTimer = True
402
403                                 # Apply afterEvent
404                                 if timer.hasAfterEvent():
405                                         afterEvent = timer.getAfterEventTimespan(localtime(end))
406                                         if afterEvent is None:
407                                                 afterEvent = timer.getAfterEvent()
408                                         if afterEvent is not None:
409                                                 newEntry.afterEvent = afterEvent
410
411                                 newEntry.dirname = timer.destination
412                                 newEntry.justplay = timer.justplay
413                                 newEntry.tags = timer.tags
414                                 newEntry.vpsplugin_enabled = timer.vps_enabled
415                                 newEntry.vpsplugin_overwrite = timer.vps_overwrite
416
417                                 if oldExists:
418                                         # XXX: this won't perform a sanity check, but do we actually want to do so?
419                                         recordHandler.timeChanged(newEntry)
420                                 else:
421                                         if similarMatch:
422                                                 newEntry.log(504, "[AutoTimer] Similar Timer is added because of conflicts with %s." % (conflictString))
423
424                                         conflicts = recordHandler.record(newEntry)
425
426                                         if conflicts:
427                                                 conflictString += ' / '.join(["%s (%s)" % (x.name, strftime("%Y%m%d %H%M", localtime(x.begin))) for x in conflicts])
428                                                 print "[AutoTimer] conflict with %s detected" % (conflictString)
429
430                                         if conflicts and config.plugins.autotimer.addsimilar_on_conflict.value:
431                                                 # We start our search right after our actual index
432                                                 for servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS in ( epgmatches[idx+1:] + epgmatches[:idx] ):
433                                                         # Match only if the descriptions are equal
434                                                         if extdesc == extdescS and shortdesc == shortdescS:
435                                                                 # Check if we already know it
436                                                                 if eitS not in similar:
437                                                                         print "[AutoTimer] Found similar Timer: " + name
438                                                                         
439                                                                         # Indicate that there exists a similar timer
440                                                                         similarExists = True
441
442                                                                         # Store the similar eit and conflictString, so it can be handled later
443                                                                         similar[eitS] = conflictString
444
445                                                                         print "TEST AT beginS < evtBegin " + str(beginS) + " " +str(evtBegin)
446                                                                         if beginS < evtBegin:
447                                                                                 #print "TEST AT epgmatches len " + str(len(epgmatches))
448                                                                                 # Event is before our actual epgmatch so we have to append it to the epgmatches list
449                                                                                 epgmatches.append((servicerefS, eitS, nameS, beginS, durationS, shortdescS, extdescS))
450                                                                                 #print "TEST AT epgmatches len " + str(len(epgmatches))
451                                                                                 #print "TEST AT epgmatches " + str(epgmatches)
452
453                                                                         # If this one also will conflict and there are more matches, they will be marked the next time 
454                                                                         break
455
456                                         if conflicts and config.plugins.autotimer.disabled_on_conflict.value and not similarExists:                     # Don't add disabled timer if a similar timer exists
457                                         #if conflicts and config.plugins.autotimer.disabled_on_conflict.value:                                                                                                  # Add disabled timer even if a similar timer exists
458                                                 newEntry.log(503, "[AutoTimer] Timer disabled because of conflicts with %s." % (conflictString,))
459                                                 newEntry.disabled = True
460                                                 # We might want to do the sanity check locally so we don't run it twice - but I consider this workaround a hack anyway
461                                                 conflicts = recordHandler.record(newEntry)
462                                                 conflicting.append((name, begin, end, serviceref, timer.name))
463
464                                         if conflicts is None:                                                                                   # Similar timers will be added to the new timer list
465                                         #if conflicts is None and not similarMatch: # Similar timers won't be added to the new timer list
466                                                 timer.decrementCounter()
467                                                 new += 1
468                                                 timer.extdesc = extdesc
469                                                 recorddict[serviceref].append(newEntry)
470
471                                         elif conflicts:                                                                                                                 # Conflicting timers will be added also if a similar timer exists
472                                         #elif conflicts and not similarExists:                  # Conflicting timers won't be added if a similar timer exists
473                                                 conflicting.append((name, begin, end, serviceref, timer.name))
474
475                                         if similarMatch and conflicts is None:                  # Similar timers can be in new timers list and additionally in similar timers list
476                                         #elif similarMatch and conflicts is None:               # Similar timers will be only in new timers list or similar timers list
477                                                 similars.append((name, begin, end, serviceref, timer.name))
478
479                 return (total, new, modified, timers, conflicting, similars)