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