refactoring in publisher and Podcast model code
[mygpo.git] / mygpo / publisher / utils.py
blobfbac2be6958d79d1bcfd080e7b15b813d03eb078
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 datetime import timedelta, date
19 from mygpo.utils import daterange
20 from mygpo.api.models import Episode, EpisodeAction
21 from mygpo.data.models import HistoricPodcastData
22 from mygpo.web.utils import flatten_intervals
23 from mygpo.publisher.models import PodcastPublisher
24 from mygpo.api.constants import DEVICE_TYPES
25 from django.db.models import Avg, Count
26 from django.contrib.auth.models import User
29 def listener_data(podcasts, start_date=date(2010, 1, 1), leap=timedelta(days=1)):
30 episode_actions = EpisodeAction.objects.filter(
31 episode__podcast__in=podcasts,
32 timestamp__gte=start_date,
33 action='play').order_by('timestamp').values('timestamp')
35 if len(episode_actions) == 0:
36 return []
38 start = episode_actions[0]['timestamp']
40 # pre-calculate episode list, make it index-able by release-date
41 episodes = Episode.objects.filter(podcast__in=podcasts)
42 episodes = filter(lambda e: e.timestamp, episodes)
43 episodes = dict([(e.timestamp.date(), e) for e in episodes])
45 days = []
46 for d in daterange(start, leap=leap):
47 next = d + leap
49 get_listeners = lambda p: p.listener_count_timespan(d, next)
50 listeners = map(get_listeners, podcasts)
51 listener_sum = sum(listeners)
53 episode = episodes[d.date()] if d.date() in episodes else None
55 days.append(dict(date=d, listeners=listener_sum, episode=episode))
57 return days
60 def episode_listener_data(episode, start_date=date(2010, 1, 1), leap=timedelta(days=1)):
61 episodes = EpisodeAction.objects.filter(episode=episode,
62 timestamp__gte=start_date).order_by('timestamp').values('timestamp')
63 if len(episodes) == 0:
64 return []
66 start = episodes[0]['timestamp']
68 intervals = []
69 for d in daterange(start, leap=leap):
70 next = d + leap
72 listeners = episode.listener_count_timespan(d, next)
73 released_episode = episode if episode.timestamp and episode.timestamp >= d and episode.timestamp <= next else None
74 intervals.append(dict(date=d, listeners=listeners, episode=released_episode))
76 return intervals
79 def subscriber_data(podcasts):
81 records = HistoricPodcastData.objects.filter(podcast__in=podcasts).order_by('date')
83 include_record = lambda r: r.date.day == 1
84 records = filter(include_record, records)
86 create_entry = lambda r: dict(x=r.date.strftime('%y-%m'), y=r.subscriber_count)
87 data = map(create_entry, records)
89 data.sort(key=lambda x: x['x'])
91 return data
94 def check_publisher_permission(user, podcast):
95 if user.is_staff:
96 return True
98 if PodcastPublisher.objects.filter(user=user, podcast=podcast).count() > 0:
99 return True
101 return False
104 def device_stats(podcasts):
105 l = EpisodeAction.objects.filter(episode__podcast__in=podcasts).values('device__type').annotate(count=Count('id'))
106 l = filter(lambda x: int(x['count']) > 0, l)
107 l = map(lambda x: (x['device__type'], x['count']), l)
108 return dict(l)
111 def episode_heatmap(episode, max_part_num=30, min_part_length=10):
113 Generates "Heatmap Data" for the given episode
115 The episode is split up in parts having max 'max_part_num' segments which
116 are all of the same length, minimum 'min_part_length' seconds.
118 For each segment, the number of users that have played it (at least
119 partially) is calculated and returned
122 episode_actions = EpisodeAction.objects.filter(episode=episode, action='play')
124 duration = episode.duration or episode_actions.aggregate(duration=Avg('total'))['duration']
126 if not duration:
127 return [0], 0
129 part_length = max(min_part_length, int(duration / max_part_num))
131 part_num = int(duration / part_length)
133 heatmap = [0]*part_num
135 user_ids = [x['user'] for x in episode_actions.values('user').distinct()]
136 for user_id in user_ids:
137 actions = episode_actions.filter(user__id=user_id, playmark__isnull=False, started__isnull=False)
138 if actions.exists():
139 played_parts = flatten_intervals(actions)
140 user_heatmap = played_parts_to_heatmap(played_parts, part_length, part_num)
141 heatmap = [sum(pair) for pair in zip(heatmap, user_heatmap)]
143 return heatmap, part_length
146 def played_parts_to_heatmap(played_parts, part_length, part_count):
148 takes the (flattened) parts of an episode that a user has played, and
149 generates a heatmap data for this user.
151 The result is a list with part_count elements, each having a value
152 of either 0 (user has not played that part) or 1 (user has at least
153 partially played that part)
155 parts = [0]*part_count
157 if not played_parts:
158 return parts
160 part_iter = iter(played_parts)
161 current_part = part_iter.next()
163 for i in range(0, part_count):
164 part = i * part_length
165 while current_part['end'] < part:
166 try:
167 current_part = part_iter.next()
168 except StopIteration:
169 return parts
171 if current_part['start'] <= (part + part_length) and current_part['end'] >= part:
172 parts[i] += 1
174 return parts
177 def colour_repr(val, max_val, colours):
179 returns a color representing the given value within a color gradient.
181 The color gradient is given by a list of (r, g, b) tupels. The value
182 is first located within two colors (of the list) and then approximated
183 between these two colors, based on its position within this segment.
185 if len(colours) == 1:
186 return colours[0]
188 # calculate position in the gradient; defines the segment
189 pos = float(val) / max_val
190 colour_nr1 = min(len(colours)-1, int(pos * (len(colours)-1)))
191 colour_nr2 = min(len(colours)-1, colour_nr1+1)
192 colour1 = colours[ colour_nr1 ]
193 colour2 = colours[ colour_nr2 ]
195 r1, g1, b1 = colour1
196 r2, g2, b2 = colour2
198 # determine bounds of segment
199 lower_bound = float(max_val) / (len(colours)-1) * colour_nr1
200 upper_bound = min(max_val, lower_bound + float(max_val) / (len(colours)-1))
202 # position within the segment
203 percent = (val - lower_bound) / upper_bound
205 r_step = r2 - r1
206 g_step = g2 - g1
207 b_step = b2 - b1
209 return (r1 + r_step * percent, g1 + g_step * percent, b1 + b_step * percent)