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