bundle queries for episode states by URLs (fixes bug 1588)
[mygpo.git] / mygpo / db / couchdb / episode_state.py
blob37aa610a2c5aeaac713df32ef7ef31e295995292
1 from hashlib import sha1
2 from datetime import datetime
3 from dateutil import parser
5 from django.core.cache import cache
7 from mygpo.users.models import EpisodeUserState
8 from mygpo.db import QueryParameterMissing
9 from mygpo.db.couchdb.podcast import podcast_by_id, podcast_for_url
10 from mygpo.db.couchdb.episode import episode_for_podcast_id_url
11 from mygpo.couch import get_main_database
12 from mygpo.cache import cache_result
13 from mygpo.decorators import repeat_on_conflict
17 def episode_state_for_user_episode(user, episode):
19 if not user:
20 raise QueryParameterMissing('user')
22 if not episode:
23 raise QueryParameterMissing('episode')
26 key = 'episode-state-userid-%s-episodeid-%s' % (sha1(user._id).hexdigest(),
27 sha1(episode._id).hexdigest())
29 state = cache.get(key)
30 if state:
31 return state
33 r = EpisodeUserState.view('episode_states/by_user_episode',
34 key = [user._id, episode._id],
35 include_docs = True,
36 limit = 1,
39 if r:
40 state = r.one()
41 cache.set(key, state)
42 return state
44 else:
45 podcast = podcast_by_id(episode.podcast)
47 state = EpisodeUserState()
48 state.episode = episode._id
49 state.podcast = episode.podcast
50 state.user = user._id
51 state.ref_url = episode.url
52 state.podcast_ref_url = podcast.url
53 # don't cache here, because the state is saved by the calling function
55 return state
59 def all_episode_states(episode):
61 if not episode:
62 raise QueryParameterMissing('episode')
64 r = EpisodeUserState.view('episode_states/by_podcast_episode',
65 startkey = [episode.podcast, episode._id, None],
66 endkey = [episode.podcast, episode._id, {}],
67 include_docs = True,
69 return list(r)
73 def all_podcast_episode_states(podcast):
75 if not podcast:
76 raise QueryParameterMissing('podcast')
78 r = EpisodeUserState.view('episode_states/by_podcast_episode',
79 startkey = [podcast.get_id(), None, None],
80 endkey = [podcast.get_id(), {}, {}],
81 include_docs = True
83 return list(r)
87 @cache_result(timeout=60*60)
88 def podcast_listener_count(episode):
89 """ returns the number of users that have listened to this podcast """
91 if not episode:
92 raise QueryParameterMissing('episode')
94 r = EpisodeUserState.view('listeners/by_podcast',
95 startkey = [episode.get_id(), None],
96 endkey = [episode.get_id(), {}],
97 group = True,
98 group_level = 1,
99 reduce = True,
101 return r.first()['value'] if r else 0
104 @cache_result(timeout=60*60)
105 def podcast_listener_count_timespan(podcast, start=None, end={}):
106 """ returns (date, listener-count) tuples for all days w/ listeners """
108 if not podcast:
109 raise QueryParameterMissing('podcast')
111 if isinstance(start, datetime):
112 start = start.isoformat()
114 if isinstance(end, datetime):
115 end = end.isoformat()
117 r = EpisodeUserState.view('listeners/by_podcast',
118 startkey = [podcast.get_id(), start],
119 endkey = [podcast.get_id(), end],
120 group = True,
121 group_level = 2,
122 reduce = True,
125 return map(_wrap_listener_count, r)
128 @cache_result(timeout=60*60)
129 def episode_listener_counts(episode):
130 """ (Episode-Id, listener-count) tuples for episodes w/ listeners """
132 if not episode:
133 raise QueryParameterMissing('episode')
136 r = EpisodeUserState.view('listeners/by_podcast_episode',
137 startkey = [episode.get_id(), None, None],
138 endkey = [episode.get_id(), {}, {}],
139 group = True,
140 group_level = 2,
141 reduce = True,
144 return map(_wrap_listeners, r)
148 def get_podcasts_episode_states(podcast, user_id):
149 """ Returns the latest episode actions for the podcast's episodes """
151 if not podcast:
152 raise QueryParameterMissing('podcast')
154 if not user_id:
155 raise QueryParameterMissing('user_id')
158 db = get_main_database()
159 res = db.view('episode_states/by_user_podcast',
160 startkey = [user_id, podcast.get_id(), None],
161 endkey = [user_id, podcast.get_id(), {}],
164 return map(lambda r: r['value'], res)
168 @cache_result(timeout=60*60)
169 def episode_listener_count(episode, start=None, end={}):
170 """ returns the number of users that have listened to this episode """
172 if not episode:
173 raise QueryParameterMissing('episode')
176 r = EpisodeUserState.view('listeners/by_episode',
177 startkey = [episode._id, start],
178 endkey = [episode._id, end],
179 group = True,
180 group_level = 2,
181 reduce = True,
183 return r.first()['value'] if r else 0
187 @cache_result(timeout=60*60)
188 def episode_listener_count_timespan(episode, start=None, end={}):
189 """ returns (date, listener-count) tuples for all days w/ listeners """
191 if not episode:
192 raise QueryParameterMissing('episode')
195 if isinstance(start, datetime):
196 start = start.isoformat()
198 if isinstance(end, datetime):
199 end = end.isoformat()
201 r = EpisodeUserState.view('listeners/by_episode',
202 startkey = [episode._id, start],
203 endkey = [episode._id, end],
204 group = True,
205 group_level = 3,
206 reduce = True,
209 return map(_wrap_listener_count, r)
213 def episode_states_for_ref_urls(user, ref_urls):
214 """ Returns episode-states for the episodes specified by URLs
216 user: the user for which states should be returned
217 ref_urls: a list of (podcast_url, episode_url) tuples, specifying episodes
220 if not user:
221 raise QueryParameterMissing('user')
223 if not ref_urls:
224 raise QueryParameterMissing('ref_urls')
226 # expand ref_urls to include the user-id
227 keys = [ [user._id, p_url, e_url] for (p_url, e_url) in ref_urls ]
229 res = EpisodeUserState.view('episode_states/by_ref_urls',
230 keys = keys,
231 limit = len(keys),
232 include_docs = True,
235 states = list(res)
237 for p_url, e_url in ref_urls:
239 state = states[0] if states else None
241 # response include an episode state
242 if state and (state.ref_url==p_url and state.episode_ref_url==e_url):
243 state = states.pop(0)
244 state.ref_url = episode_url
245 state.podcast_ref_url = podcast_url
246 yield state
248 # there is no episode state - create one
249 else:
250 podcast = podcast_for_url(p_url, create=True)
251 episode = episode_for_podcast_id_url(podcast.get_id(), e_url,
252 create=True)
253 yield episode_state_for_user_episode(user, episode)
257 def get_episode_actions(user_id, since=None, until={}, podcast_id=None,
258 device_id=None):
259 """ Returns Episode Actions for the given criteria"""
261 if not user_id:
262 raise QueryParameterMissing('user_id')
264 if since >= until:
265 return []
267 if not podcast_id and not device_id:
268 view = 'episode_actions/by_user'
269 startkey = [user_id, since]
270 endkey = [user_id, until]
272 elif podcast_id and not device_id:
273 view = 'episode_actions/by_podcast'
274 startkey = [user_id, podcast_id, since]
275 endkey = [user_id, podcast_id, until]
277 elif device_id and not podcast_id:
278 view = 'episode_actions/by_device'
279 startkey = [user_id, device_id, since]
280 endkey = [user_id, device_id, until]
282 else:
283 view = 'episode_actions/by_podcast_device'
284 startkey = [user_id, podcast_id, device_id, since]
285 endkey = [user_id, podcast_id, device_id, until]
287 db = get_main_database()
288 res = db.view(view,
289 startkey = startkey,
290 endkey = endkey
293 return map(lambda r: r['value'], res)
297 @cache_result(timeout=60*60)
298 def episode_states_count():
299 r = EpisodeUserState.view('episode_states/by_user_episode',
300 limit = 0,
301 stale = 'update_after',
303 return r.total_rows
306 def get_nth_episode_state(n):
307 first = EpisodeUserState.view('episode_states/by_user_episode',
308 skip = n,
309 include_docs = True,
310 limit = 1,
312 return first.one() if first else None
315 def get_duplicate_episode_states(user, episode):
317 if not user:
318 raise QueryParameterMissing('user')
320 if not episode:
321 raise QueryParameterMissing('episode')
323 states = EpisodeUserState.view('episode_states/by_user_episode',
324 key = [user, episode],
325 include_docs = True,
327 return list(states)
330 def _wrap_listener_count(res):
331 date = parser.parse(res['key'][1]).date()
332 listeners = res['value']
333 return (date, listeners)
336 def _wrap_listeners(res):
337 episode = res['key'][1]
338 listeners = res['value']
339 return (episode, listeners)
342 @cache_result(timeout=60*60)
343 def get_heatmap(podcast_id, episode_id, user_id):
344 db = get_main_database()
346 group_level = len(filter(None, [podcast_id, episode_id, user_id]))
348 r = db.view('heatmap/by_episode',
349 startkey = [podcast_id, episode_id, user_id],
350 endkey = [podcast_id, episode_id or {}, user_id or {}],
351 reduce = True,
352 group = True,
353 group_level = group_level,
354 stale = 'update_after',
357 if not r:
358 return [], []
360 else:
361 res = r.first()['value']
362 return res['heatmap'], res['borders']
365 @repeat_on_conflict(['state'])
366 def add_episode_actions(state, actions):
367 state.add_actions(actions)
368 state.save()