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