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
26 from django
.contrib
.auth
.models
import User
29 def listener_data(podcasts
):
34 episode_actions
= EpisodeAction
.objects
.filter(episode__podcast__in
=podcasts
, timestamp__gte
=d
, action
='play').order_by('timestamp').values('timestamp')
35 if len(episode_actions
) == 0:
38 start
= episode_actions
[0]['timestamp']
40 # pre-calculate episode list, make it index-able by release-date
42 for episode
in Episode
.objects
.filter(podcast__in
=podcasts
):
44 episodes
[episode
.timestamp
.date()] = episode
47 for d
in daterange(start
):
48 next
= d
+ timedelta(days
=1)
51 # this is faster than .filter(episode__podcast__in=podcasts)
53 listeners
= EpisodeAction
.objects
.filter(episode__podcast
=p
, timestamp__gte
=d
, timestamp__lt
=next
, action
='play').values('user_id').distinct().count()
54 listener_sum
+= listeners
56 if d
.date() in episodes
:
57 episode
= episodes
[d
.date()]
63 'listeners': listener_sum
,
69 def episode_listener_data(episode
):
71 leap
= timedelta(days
=1)
73 episodes
= EpisodeAction
.objects
.filter(episode
=episode
, timestamp__gte
=d
).order_by('timestamp').values('timestamp')
74 if len(episodes
) == 0:
77 start
= episodes
[0]['timestamp']
80 for d
in daterange(start
, leap
=leap
):
82 listeners
= EpisodeAction
.objects
.filter(episode
=episode
, timestamp__gte
=d
, timestamp__lt
=next
).values('user_id').distinct().count()
83 e
= episode
if episode
.timestamp
and episode
.timestamp
>= d
and episode
.timestamp
<= next
else None
86 'listeners': listeners
,
92 def subscriber_data(podcasts
):
95 #this is fater than a subquery
98 records
.extend(HistoricPodcastData
.objects
.filter(podcast
=p
).order_by('date'))
102 s
= r
.date
.strftime('%y-%m')
104 data
[s
] = val
+ r
.subscriber_count
107 for k
, v
in data
.iteritems():
108 list.append({'x': k
, 'y': v
})
110 list.sort(key
=lambda x
: x
['x'])
115 def check_publisher_permission(user
, podcast
):
119 if PodcastPublisher
.objects
.filter(user
=user
, podcast
=podcast
).count() > 0:
124 def episode_list(podcast
):
125 episodes
= Episode
.objects
.filter(podcast
=podcast
).order_by('-timestamp')
127 listeners
= EpisodeAction
.objects
.filter(episode
=e
, action
='play').values('user').distinct()
128 e
.listeners
= listeners
.count()
133 def device_stats(podcasts
):
135 for type in DEVICE_TYPES
:
138 # this is faster than a subquery
140 c
+= EpisodeAction
.objects
.filter(episode__podcast
=p
, device__type
=type[0]).values('user_id').distinct().count()
147 def episode_heatmap(episode
, max_part_num
=50, min_part_length
=10):
149 Generates "Heatmap Data" for the given episode
151 The episode is split up in parts having max 'max_part_num' segments which
152 are all of the same length, minimum 'min_part_length' seconds.
154 For each segment, the number of users that have played it (at least
155 partially) is calculated and returned
158 episode_actions
= EpisodeAction
.objects
.filter(episode
=episode
, action
='play')
161 duration
= episode
.duration
163 duration
= episode_actions
.aggregate(duration
=Avg('total'))['duration']
168 part_length
= max(min_part_length
, int(duration
/ max_part_num
))
170 part_num
= int(duration
/ part_length
)
172 heatmap
= [0]*part_num
174 user_ids
= [x
['user'] for x
in episode_actions
.values('user').distinct()]
175 for user_id
in user_ids
:
176 user
= User
.objects
.get(id=user_id
)
177 actions
= episode_actions
.filter(user
=user
, playmark__isnull
=False, started__isnull
=False)
179 played_parts
= flatten_intervals(actions
)
180 user_heatmap
= played_parts_to_heatmap(played_parts
, part_length
, part_num
)
181 heatmap
= [sum(pair
) for pair
in zip(heatmap
, user_heatmap
)]
183 return heatmap
, part_length
186 def played_parts_to_heatmap(played_parts
, part_length
, part_count
):
188 takes the (flattened) parts of an episode that a user has played, and
189 generates a heatmap data for this user.
191 The result is a list with part_count elements, each having a value
192 of either 0 (user has not played that part) or 1 (user has at least
193 partially played that part)
195 parts
= [0]*part_count
200 part_iter
= iter(played_parts
)
201 current_part
= part_iter
.next()
203 for i
in range(0, part_count
):
204 part
= i
* part_length
205 while current_part
['end'] < part
:
207 current_part
= part_iter
.next()
208 except StopIteration:
211 if current_part
['start'] <= (part
+ part_length
) and current_part
['end'] >= part
:
216 def colour_repr(val
, max_val
, colours
):
218 returns a color representing the given value within a color gradient.
220 The color gradient is given by a list of (r, g, b) tupels. The value
221 is first located within two colors (of the list) and then approximated
222 between these two colors, based on its position within this segment.
224 if len(colours
) == 1:
227 # calculate position in the gradient; defines the segment
228 pos
= float(val
) / max_val
229 colour_nr1
= min(len(colours
)-1, int(pos
* (len(colours
)-1)))
230 colour_nr2
= min(len(colours
)-1, colour_nr1
+1)
231 colour1
= colours
[ colour_nr1
]
232 colour2
= colours
[ colour_nr2
]
237 # determine bounds of segment
238 lower_bound
= float(max_val
) / (len(colours
)-1) * colour_nr1
239 upper_bound
= min(max_val
, lower_bound
+ float(max_val
) / (len(colours
)-1))
241 # position within the segment
242 percent
= (val
- lower_bound
) / upper_bound
248 return (r1
+ r_step
* percent
, g1
+ g_step
* percent
, b1
+ b_step
* percent
)