autotimer: more control over duplicate search
[enigma2-plugins.git] / autotimer / src / AutoTimerComponent.py
1 # Format counter
2 from time import strftime
3
4 # regular expression
5 from re import compile as re_compile
6
7 # Alternatives and service restriction
8 from enigma import eServiceReference, eServiceCenter
9
10 # To get preferred component
11 from Components.config import config
12
13 # Default encoding
14 from Components.Language import language
15
16 class AutoTimerComponent(object):
17         """AutoTimer Component which also handles validity checks"""
18
19         """
20          Initiate
21         """
22         def __init__(self, id, name, match, enabled, *args, **kwargs):
23                 self.id = id
24                 self._afterevent = []
25                 self.setValues(name, match, enabled, *args, **kwargs)
26
27         """
28          Unsets all Attributes
29         """
30         def clear(self, id = -1, enabled = False):
31                 self.id = id
32                 self.setValues('', '', enabled)
33
34         """
35          Create a deep copy of this instance
36         """
37         def clone(self):
38                 return self.__deepcopy__({})
39
40         """
41          Hook needed for WebIf
42         """
43         def getEntry(self):
44                 return self
45
46         """
47          Keeps init small and helps setting many values at once
48         """
49         def setValues(self, name, match, enabled, timespan=None, services=None, \
50                         offset=None, afterevent=[], exclude=None, maxduration=None, \
51                         destination=None, include=None, matchCount=0, matchLeft=0, \
52                         matchLimit='', matchFormatString='', lastBegin=0, justplay=False, \
53                         avoidDuplicateDescription=0, searchForDuplicateDescription=3, bouquets=None, \
54                         tags=None, encoding=None, searchType="partial", searchCase="insensitive", \
55                         overrideAlternatives=False, timeframe=None, vps_enabled=False, \
56                         vps_overwrite=False):
57                 self.name = name
58                 self.match = match
59                 self.enabled = enabled
60                 self.timespan = timespan
61                 self.services = services
62                 self.offset = offset
63                 self.afterevent = afterevent
64                 self.exclude = exclude
65                 self.maxduration = maxduration
66                 self.destination = destination
67                 self.include = include
68                 self.matchCount = matchCount
69                 self.matchLeft = matchLeft
70                 self.matchLimit = matchLimit
71                 self.matchFormatString = matchFormatString
72                 self.lastBegin = lastBegin
73                 self.justplay = justplay
74                 self.avoidDuplicateDescription = avoidDuplicateDescription
75                 self.searchForDuplicateDescription = searchForDuplicateDescription
76                 self.bouquets = bouquets
77                 self.tags = tags or []
78                 self.encoding = encoding or getDefaultEncoding()
79                 self.searchType = searchType
80                 self.searchCase = searchCase
81                 self.overrideAlternatives = overrideAlternatives
82                 self.timeframe = timeframe
83                 self.vps_enabled = vps_enabled
84                 self.vps_overwrite = vps_overwrite
85
86 ### Attributes / Properties
87
88         def setAfterEvent(self, afterevent):
89                 if afterevent is not self._afterevent:
90                         del self._afterevent[:]
91                 else:
92                         self._afterevent = []
93
94                 for action, timespan in afterevent:
95                         if timespan is None or timespan[0] is None:
96                                 self._afterevent.append((action, (None,)))
97                         else:
98                                 self._afterevent.append((action, self.calculateDayspan(*timespan)))
99
100         afterevent = property(lambda self: self._afterevent, setAfterEvent)
101
102         def setBouquets(self, bouquets):
103                 if bouquets:
104                         self._bouquets = bouquets
105                 else:
106                         self._bouquets = []
107
108         bouquets = property(lambda self: self._bouquets , setBouquets)
109
110         def setEncoding(self, encoding):
111                 if encoding == '(null)':
112                         self._encoding = getDefaultEncoding()
113                 elif encoding:
114                         self._encoding = encoding
115                 elif not self._encoding:
116                         self._encoding = getDefaultEncoding()
117
118         encoding = property(lambda self: self._encoding, setEncoding)
119
120         def setExclude(self, exclude):
121                 if exclude:
122                         self._exclude = (
123                                 [re_compile(x) for x in exclude[0]],
124                                 [re_compile(x) for x in exclude[1]],
125                                 [re_compile(x) for x in exclude[2]],
126                                 exclude[3]
127                         )
128                 else:
129                         self._exclude = ([], [], [], [])
130
131         exclude = property(lambda self: self._exclude, setExclude)
132
133         def setInclude(self, include):
134                 if include:
135                         self._include = (
136                                 [re_compile(x) for x in include[0]],
137                                 [re_compile(x) for x in include[1]],
138                                 [re_compile(x) for x in include[2]],
139                                 include[3]
140                         )
141                 else:
142                         self._include = ([], [], [], [])
143
144         include = property(lambda self: self._include, setInclude)
145
146         def setSearchCase(self, case):
147                 assert case in ("sensitive", "insensitive"), "search case must be sensitive or insensitive"
148                 self._searchCase = case
149
150         searchCase = property(lambda self: self._searchCase, setSearchCase)
151
152         def setSearchType(self, type):
153                 assert type in ("exact", "partial", "description"), "search type must be exact, partial or description"
154                 self._searchType = type
155
156         searchType = property(lambda self: self._searchType, setSearchType)
157
158         def setServices(self, services):
159                 if services:
160                         self._services = services
161                 else:
162                         self._services = []
163
164         services = property(lambda self: self._services, setServices)
165
166         def setTimespan(self, timespan):
167                 if timespan is None or timespan and timespan[0] is None:
168                         self._timespan = (None,)
169                 else:
170                         self._timespan = self.calculateDayspan(*timespan)
171
172         timespan = property(lambda self: self._timespan, setTimespan)
173
174 ### See if Attributes are set
175
176         def hasAfterEvent(self):
177                 return len(self.afterevent)
178
179         def hasAfterEventTimespan(self):
180                 for afterevent in self.afterevent:
181                         if afterevent[1][0] is not None:
182                                 return True
183                 return False
184
185         def hasCounter(self):
186                 return self.matchCount != 0
187
188         def hasCounterFormatString(self):
189                 return self.matchFormatString != ''
190
191         def hasDestination(self):
192                 return self.destination is not None
193
194         def hasDuration(self):
195                 return self.maxduration is not None
196
197         def hasTags(self):
198                 return len(self.tags)
199
200         def hasTimespan(self):
201                 return self.timespan[0] is not None
202
203         def hasOffset(self):
204                 return self.offset is not None
205
206         def hasTimeframe(self):
207                 return self.timeframe is not None
208
209 ### Helper
210
211         """
212          Returns a tulple of (input begin, input end, begin earlier than end)
213         """
214         def calculateDayspan(self, begin, end, ignore = None):
215                 if end[0] < begin[0] or (end[0] == begin[0] and end[1] <= begin[1]):
216                         return (begin, end, True)
217                 else:
218                         return (begin, end, False)
219
220         """
221          Returns if a given timestruct is in a timespan
222         """
223         def checkAnyTimespan(self, time, begin = None, end = None, haveDayspan = False):
224                 if begin is None:
225                         return False
226
227                 # Check if we span a day
228                 if haveDayspan:
229                         # Check if begin of event is later than our timespan starts
230                         if time.tm_hour > begin[0] or (time.tm_hour == begin[0] and time.tm_min >= begin[1]):
231                                 # If so, event is in our timespan
232                                 return False
233                         # Check if begin of event is earlier than our timespan end
234                         if time.tm_hour < end[0] or (time.tm_hour == end[0] and time.tm_min <= end[1]):
235                                 # If so, event is in our timespan
236                                 return False
237                         return True
238                 else:
239                         # Check if event begins earlier than our timespan starts
240                         if time.tm_hour < begin[0] or (time.tm_hour == begin[0] and time.tm_min < begin[1]):
241                                 # Its out of our timespan then
242                                 return True
243                         # Check if event begins later than our timespan ends
244                         if time.tm_hour > end[0] or (time.tm_hour == end[0] and time.tm_min > end[1]):
245                                 # Its out of our timespan then
246                                 return True
247                         return False
248
249         """
250          Called when a timer based on this component was added
251         """
252         def update(self, begin, timestamp):
253                 # Only update limit when we have new begin
254                 if begin > self.lastBegin:
255                         self.lastBegin = begin
256
257                         # Update Counter:
258                         # %m is Month, %U is week (sunday), %W is week (monday)
259                         newLimit = strftime(self.matchFormatString, timestamp)
260
261                         if newLimit != self.matchLimit:
262                                 self.matchLeft = self.matchCount
263                                 self.matchLimit = newLimit
264
265 ### Makes saving Config easier
266
267         getAvoidDuplicateDescription = lambda self: self.avoidDuplicateDescription
268
269         getSearchForDuplicateDescription = lambda self: self.searchForDuplicateDescription
270
271         getBouquets = lambda self: self._bouquets
272
273         getCompleteAfterEvent = lambda self: self._afterevent
274
275         getCounter = lambda self: self.matchCount
276         getCounterFormatString = lambda self: self.matchFormatString
277         getCounterLeft = lambda self: self.matchLeft
278         getCounterLimit = lambda self: self.matchLimit
279
280         # XXX: as this function was not added by me (ritzMo) i'll leave it like this but i'm not really sure if this is right ;-)
281         getDestination = lambda self: self.destination is not None
282
283         getDuration = lambda self: self.maxduration/60
284
285         getEnabled = lambda self: self.enabled and "yes" or "no"
286
287         getExclude = lambda self: self._exclude
288         getExcludedDays = lambda self: self.exclude[3]
289         getExcludedDescription = lambda self: [x.pattern for x in self.exclude[2]]
290         getExcludedShort = lambda self: [x.pattern for x in self.exclude[1]]
291         getExcludedTitle = lambda self: [x.pattern for x in self.exclude[0]]
292
293         getId = lambda self: self.id
294
295         getInclude = lambda self: self._include
296         getIncludedTitle = lambda self: [x.pattern for x in self.include[0]]
297         getIncludedShort = lambda self: [x.pattern for x in self.include[1]]
298         getIncludedDescription = lambda self: [x.pattern for x in self.include[2]]
299         getIncludedDays = lambda self: self.include[3]
300
301         getJustplay = lambda self: self.justplay and "1" or "0"
302
303         getLastBegin = lambda self: self.lastBegin
304
305         getMatch = lambda self: self.match
306         getName = lambda self: self.name
307
308         getOffsetBegin = lambda self: self.offset[0]/60
309         getOffsetEnd = lambda self: self.offset[1]/60
310
311         getOverrideAlternatives = lambda self: self.overrideAlternatives and "1" or "0"
312
313         getServices = lambda self: self._services
314
315         getTags = lambda self: self.tags
316
317         getTimespan = lambda self: self._timespan
318         getTimespanBegin = lambda self: '%02d:%02d' % (self.timespan[0][0], self.timespan[0][1])
319         getTimespanEnd = lambda self: '%02d:%02d' % (self.timespan[1][0], self.timespan[1][1])
320
321         getTimeframe = lambda self: self.timeframe
322         getTimeframeBegin = lambda self: int(self.timeframe[0])
323         getTimeframeEnd = lambda self: int(self.timeframe[1])
324
325         isOffsetEqual = lambda self: self.offset[0] == self.offset[1]
326
327 ### Actual functionality
328
329         def applyOffset(self, begin, end):
330                 if self.offset is None:
331                         return (begin, end)
332                 return (begin - self.offset[0], end + self.offset[1])
333
334         def checkCounter(self, timestamp):
335                 # 0-Count is considered "unset"
336                 if self.matchCount == 0:
337                         return False
338
339                 # Check if event is in current timespan (we can only manage one!)
340                 limit = strftime(self.matchFormatString, timestamp)
341                 if limit != self.matchLimit:
342                         return True
343
344                 if self.matchLeft > 0:
345                         return False
346                 return True
347
348         def checkDuration(self, length):
349                 if self.maxduration is None:
350                         return False
351                 return length > self.maxduration
352
353         def checkExcluded(self, title, short, extended, dayofweek):
354                 if dayofweek and self.exclude[3]:
355                         list = self.exclude[3]
356                         if dayofweek in list:
357                                 return True
358                         if "weekend" in list and dayofweek in ("5", "6"):
359                                 return True
360                         if "weekday" in list and dayofweek in ("0", "1", "2", "3", "4"):
361                                 return True
362
363                 for exclude in self.exclude[0]:
364                         if exclude.search(title):
365                                 return True
366                 for exclude in self.exclude[1]:
367                         if exclude.search(short):
368                                 return True
369                 for exclude in self.exclude[2]:
370                         if exclude.search(extended):
371                                 return True
372                 return False
373
374         def checkFilter(self, title, short, extended, dayofweek):
375                 if self.checkExcluded(title, short, extended, dayofweek):
376                         return True
377
378                 return self.checkIncluded(title, short, extended, dayofweek)
379
380         def checkIncluded(self, title, short, extended, dayofweek):
381                 if dayofweek and self.include[3]:
382                         list = self.include[3][:]
383                         if "weekend" in list:
384                                 list.extend(("5", "6"))
385                         if "weekday" in list:
386                                 list.extend(("0", "1", "2", "3", "4"))
387                         if dayofweek not in list:
388                                 return True
389
390                 for include in self.include[0]:
391                         if not include.search(title):
392                                 return True
393                 for include in self.include[1]:
394                         if not include.search(short):
395                                 return True
396                 for include in self.include[2]:
397                         if not include.search(extended):
398                                 return True
399
400                 return False
401
402         def checkServices(self, check_service):
403                 services = self.services
404                 bouquets = self.bouquets
405                 if services or bouquets:
406                         addbouquets = []
407
408                         for service in services:
409                                 if service == check_service:
410                                         return False
411
412                                 myref = eServiceReference(str(service))
413                                 if myref.flags & eServiceReference.isGroup:
414                                         addbouquets.append(service)
415
416                         serviceHandler = eServiceCenter.getInstance()
417                         for bouquet in bouquets + addbouquets:
418                                 myref = eServiceReference(str(bouquet))
419                                 mylist = serviceHandler.list(myref)
420                                 if mylist is not None:
421                                         while 1:
422                                                 s = mylist.getNext()
423                                                 # TODO: I wonder if its sane to assume we get services here (and not just new lists)
424                                                 # We can ignore markers & directorys here because they won't match any event's service :-)
425                                                 if s.valid():
426                                                         # strip all after last :
427                                                         value = s.toString()
428                                                         pos = value.rfind(':')
429                                                         if pos != -1:
430                                                                 if value[pos-1] == ':':
431                                                                         pos -= 1
432                                                                 value = value[:pos+1]
433
434                                                         if value == check_service:
435                                                                 return False
436                                                 else:
437                                                         break
438                         return True
439                 return False
440
441         """
442         Return alternative service including a given ref.
443         Note that this only works for alternatives that the autotimer is restricted to.
444         """
445         def getAlternative(self, override_service):
446                 services = self.services
447                 if services:
448                         serviceHandler = eServiceCenter.getInstance()
449
450                         for service in services:
451                                 myref = eServiceReference(str(service))
452                                 if myref.flags & eServiceReference.isGroup:
453                                         mylist = serviceHandler.list(myref)
454                                         if mylist is not None:
455                                                 while 1:
456                                                         s = mylist.getNext()
457                                                         if s.valid():
458                                                                 # strip all after last :
459                                                                 value = s.toString()
460                                                                 pos = value.rfind(':')
461                                                                 if pos != -1:
462                                                                         if value[pos-1] == ':':
463                                                                                 pos -= 1
464                                                                         value = value[:pos+1]
465
466                                                                 if value == override_service:
467                                                                         return service
468                                                         else:
469                                                                 break
470                 return override_service
471
472         def checkTimespan(self, begin):
473                 return self.checkAnyTimespan(begin, *self.timespan)
474
475         def decrementCounter(self):
476                 if self.matchCount and self.matchLeft > 0:
477                         self.matchLeft -= 1
478
479         def getAfterEvent(self):
480                 for afterevent in self.afterevent:
481                         if afterevent[1][0] is None:
482                                 return afterevent[0]
483                 return None
484
485         def getAfterEventTimespan(self, end):
486                 for afterevent in self.afterevent:
487                         if not self.checkAnyTimespan(end, *afterevent[1]):
488                                 return afterevent[0]
489                 return None
490
491         def checkTimeframe(self, begin):
492                 if self.timeframe is not None:
493                         start, end = self.timeframe
494                         if begin > start and begin < end:
495                                 return False
496                         return True
497                 return False
498
499 ### Misc
500
501         def __copy__(self):
502                 return self.__class__(
503                         self.id,
504                         self.name,
505                         self.match,
506                         self.enabled,
507                         timespan = self.timespan,
508                         services = self.services,
509                         offset = self.offset,
510                         afterevent = self.afterevent,
511                         exclude = (self.getExcludedTitle(), self.getExcludedShort(), self.getExcludedDescription(), self.getExcludedDays()),
512                         maxduration = self.maxduration,
513                         destination = self.destination,
514                         include = (self.getIncludedTitle(), self.getIncludedShort(), self.getIncludedDescription(), self.getIncludedDays()),
515                         matchCount = self.matchCount,
516                         matchLeft = self.matchLeft,
517                         matchLimit = self.matchLimit,
518                         matchFormatString = self.matchFormatString,
519                         lastBegin = self.lastBegin,
520                         justplay = self.justplay,
521                         avoidDuplicateDescription = self.avoidDuplicateDescription,
522                         searchForDuplicateDescription = self.searchForDuplicateDescription,
523                         bouquets = self.bouquets,
524                         tags = self.tags,
525                         encoding = self.encoding,
526                         searchType = self.searchType,
527                         searchCase = self.searchCase,
528                         overrideAlternatives = self.overrideAlternatives,
529                         timeframe = self.timeframe,
530                         vps_enabled = self.vps_enabled,
531                         vps_overwrite = self.vps_overwrite,
532                 )
533
534         def __deepcopy__(self, memo):
535                 return self.__class__(
536                         self.id,
537                         self.name,
538                         self.match,
539                         self.enabled,
540                         timespan = self.timespan,
541                         services = self.services[:],
542                         offset = self.offset and self.offset[:],
543                         afterevent = self.afterevent[:],
544                         exclude = (self.getExcludedTitle(), self.getExcludedShort(), self.getExcludedDescription(), self.exclude[3][:]),
545                         maxduration = self.maxduration,
546                         destination = self.destination,
547                         include = (self.getIncludedTitle(), self.getIncludedShort(), self.getIncludedDescription(), self.include[3][:]),
548                         matchCount = self.matchCount,
549                         matchLeft = self.matchLeft,
550                         matchLimit = self.matchLimit,
551                         matchFormatString = self.matchFormatString,
552                         lastBegin = self.lastBegin,
553                         justplay = self.justplay,
554                         avoidDuplicateDescription = self.avoidDuplicateDescription,
555                         searchForDuplicateDescription = self.searchForDuplicateDescription,
556                         bouquets = self.bouquets[:],
557                         tags = self.tags[:],
558                         encoding = self.encoding,
559                         searchType = self.searchType,
560                         searchCase = self.searchCase,
561                         overrideAlternatives = self.overrideAlternatives,
562                         timeframe = self.timeframe,
563                         vps_enabled = self.vps_enabled,
564                         vps_overwrite = self.vps_overwrite,
565                 )
566
567         def __eq__(self, other):
568                 if isinstance(other, AutoTimerComponent):
569                         return self.id == other.id
570                 return False
571
572         def __lt__(self, other):
573                 if isinstance(other, AutoTimerComponent):
574                         return self.name.lower() < other.name.lower()
575                 return False
576
577         def __ne__(self, other):
578                 return not self.__eq__(other)
579
580         def __repr__(self):
581                 return ''.join((
582                         '<AutomaticTimer ',
583                         self.name,
584                         ' (',
585                         ', '.join((
586                                         str(self.match),
587                                         str(self.encoding),
588                                         str(self.searchCase),
589                                         str(self.searchType),
590                                         str(self.timespan),
591                                         str(self.services),
592                                         str(self.offset),
593                                         str(self.afterevent),
594                                         str(([x.pattern for x in self.exclude[0]],
595                                                 [x.pattern for x in self.exclude[1]],
596                                                 [x.pattern for x in self.exclude[2]],
597                                                 self.exclude[3]
598                                         )),
599                                         str(([x.pattern for x in self.include[0]],
600                                                 [x.pattern for x in self.include[1]],
601                                                 [x.pattern for x in self.include[2]],
602                                                 self.include[3]
603                                         )),
604                                         str(self.maxduration),
605                                         str(self.enabled),
606                                         str(self.destination),
607                                         str(self.matchCount),
608                                         str(self.matchLeft),
609                                         str(self.matchLimit),
610                                         str(self.matchFormatString),
611                                         str(self.lastBegin),
612                                         str(self.justplay),
613                                         str(self.avoidDuplicateDescription),
614                                         str(self.searchForDuplicateDescription),
615                                         str(self.bouquets),
616                                         str(self.tags),
617                                         str(self.overrideAlternatives),
618                                         str(self.timeframe),
619                                         str(self.vps_enabled),
620                                         str(self.vps_overwrite),
621                          )),
622                          ")>"
623                 ))
624
625 class AutoTimerFastscanComponent(AutoTimerComponent):
626         def __init__(self, *args, **kwargs):
627                 AutoTimerComponent.__init__(self, *args, **kwargs)
628                 self._fastServices = None
629
630         def setBouquets(self, bouquets):
631                 AutoTimerComponent.setBouquets(self, bouquets)
632                 self._fastServices = None
633
634         def setServices(self, services):
635                 AutoTimerComponent.setServices(self, services)
636                 self._fastServices = None
637
638         def getFastServices(self):
639                 if self._fastServices is None:
640                         fastServices = []
641                         append = fastServices.append
642                         addbouquets = []
643                         for service in self.services:
644                                 myref = eServiceReference(str(service))
645                                 if myref.flags & eServiceReference.isGroup:
646                                         addbouquets.append(service)
647                                 else:
648                                         comp = service.split(':')
649                                         append(':'.join(comp[3:]))
650
651                         serviceHandler = eServiceCenter.getInstance()
652                         for bouquet in self.bouquets + addbouquets:
653                                 myref = eServiceReference(str(bouquet))
654                                 mylist = serviceHandler.list(myref)
655                                 if mylist is not None:
656                                         while 1:
657                                                 s = mylist.getNext()
658                                                 # TODO: I wonder if its sane to assume we get services here (and not just new lists)
659                                                 # We can ignore markers & directorys here because they won't match any event's service :-)
660                                                 if s.valid():
661                                                         # strip all after last :
662                                                         value = s.toString()
663                                                         pos = value.rfind(':')
664                                                         if pos != -1:
665                                                                 if value[pos-1] == ':':
666                                                                         pos -= 1
667                                                                 value = value[:pos+1]
668
669                                                         comp = value.split(':')
670                                                         append(':'.join(value[3:]))
671                                                 else:
672                                                         break
673                         self._fastServices = fastServices
674                 return self._fastServices
675
676         def checkServices(self, check_service):
677                 services = self.getFastServices()
678                 if services:
679                         check = ':'.join(check_service.split(':')[3:])
680                         for service in services:
681                                 if service == check:
682                                         return False # included
683                         return True # not included
684                 return False # no restriction
685
686         def getAlternative(self, override_service):
687                 services = self.services
688                 if services:
689                         override = ':'.join(override_service.split(':')[3:])
690                         serviceHandler = eServiceCenter.getInstance()
691
692                         for service in services:
693                                 myref = eServiceReference(str(service))
694                                 if myref.flags & eServiceReference.isGroup:
695                                         mylist = serviceHandler.list(myref)
696                                         if mylist is not None:
697                                                 while 1:
698                                                         s = mylist.getNext()
699                                                         if s.valid():
700                                                                 # strip all after last :
701                                                                 value = s.toString()
702                                                                 pos = value.rfind(':')
703                                                                 if pos != -1:
704                                                                         if value[pos-1] == ':':
705                                                                                 pos -= 1
706                                                                         value = value[:pos+1]
707
708                                                                 if ':'.join(value.split(':')[3:]) == override:
709                                                                         return service
710                                                         else:
711                                                                 break
712                 return override_service
713
714 def getDefaultEncoding():
715         if 'de' in language.getLanguage():
716                 return 'ISO8859-15'
717         return 'UTF-8'
718
719 # very basic factory ;-)
720 preferredAutoTimerComponent = lambda *args, **kwargs: AutoTimerFastscanComponent(*args, **kwargs) if config.plugins.autotimer.fastscan.value else AutoTimerComponent(*args, **kwargs)