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:
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
])
46 for d
in daterange(start
, leap
=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
))
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:
66 start
= episodes
[0]['timestamp']
69 for d
in daterange(start
, leap
=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
))
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'])
94 def check_publisher_permission(user
, podcast
):
98 if PodcastPublisher
.objects
.filter(user
=user
, podcast
=podcast
).count() > 0:
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
)
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']
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)
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
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
:
167 current_part
= part_iter
.next()
168 except StopIteration:
171 if current_part
['start'] <= (part
+ part_length
) and current_part
['end'] >= part
:
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:
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
]
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
209 return (r1
+ r_step
* percent
, g1
+ g_step
* percent
, b1
+ b_step
* percent
)