[Episodes] fix heatmap calculation
[mygpo.git] / mygpo / web / heatmap.py
blob2ea1e0fd695c9996dc1283dded5e5494fcda385c
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 from functools import wraps
20 from mygpo.history.models import EpisodeHistoryEntry
23 class EpisodeHeatmap(object):
24 """ Information about how often certain parts of Episodes are played """
26 def __init__(self, podcast, episode=None, user=None, duration=None):
27 """ Initialize a new Episode heatmap """
28 self.duration = duration
29 self.heatmap = None
30 self.borders = None
32 history = EpisodeHistoryEntry.objects.filter(episode__podcast=podcast)
34 if episode:
35 history = history.filter(episode=episode)
37 if user:
38 history = history.filter(user=user)
40 self.history = history
42 @staticmethod
43 def _raw_heatmap(events):
44 """ Returns the detailled (exact) heatmap
46 >>> _raw_heatmap([(70, 200), (0, 100), (0, 50)])
47 ([2, 1, 2, 1], [0, 50, 70, 100, 200])
48 """
50 # get a list of all borders that occur in events
51 borders = set()
52 for start, end in events:
53 borders.add(start)
54 borders.add(end)
55 borders = sorted(borders)
57 # this contains the value for the spaces within the borders
58 # therefore we need one field less then we have borders
59 counts = [0] * (len(borders)-1)
61 for start, end in events:
62 # for each event we calculate its range
63 start_idx = borders.index(start)
64 end_idx = borders.index(end)
66 # and increase the play-count within the range by 1
67 for inc in range(start_idx, end_idx):
68 counts[inc] = counts[inc] + 1
70 return counts, borders
72 # we return the heatmap as (start, stop, playcount) tuples
73 # for i in range(len(counts)):
74 # yield (borders[i], borders[i+1], counts[i])
76 def _query(self):
77 self.heatmap, self.borders = self._raw_heatmap(
78 self.history.values_list('started', 'stopped'))
80 def query_if_required():
81 """ If required, queries the database before calling the function """
82 def decorator(f):
83 @wraps(f)
84 def tmp(self, *args, **kwargs):
85 if None in (self.heatmap, self.borders):
86 self._query()
88 return f(self, *args, **kwargs)
89 return tmp
90 return decorator
92 @property
93 @query_if_required()
94 def max_plays(self):
95 """ Returns the highest number of plays of all sections """
97 return max(self.heatmap, default=0)
99 @property
100 @query_if_required()
101 def sections(self):
102 """ Returns an iterator that emits (from, to, play-counts) tuples
104 Each tuple represents one part in the heatmap with a distinct
105 play-count. from and to indicate the range of section in seconds."""
107 # this could be written as "yield from"
108 for x in self._sections(self.heatmap, self.borders):
109 yield x
111 @staticmethod
112 def _sections(heatmap, borders):
113 """ Merges heatmap-counts and borders into a list of 3-tuples
115 Each tuple contains (start-border, end-border, play-count)
117 >>> list(_sections([2, 1, 2, 1], [0, 50, 70, 100, 200]))
118 [(0, 50, 2), (50, 70, 1), (70, 100, 2), (100, 200, 1)]
120 for i in range(len(heatmap)):
121 yield (borders[i], borders[i+1], heatmap[i])
123 @query_if_required()
124 def __nonzero__(self):
125 return any(self.heatmap)