[Favorites] get favorite episodes from Django ORM
[mygpo.git] / mygpo / db / couchdb / episode_state.py
blobdeee02b16a614346b7b288256a7b23d62b8fd6cc
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.podcasts.models import Podcast, Episode
8 from mygpo.users.models import EpisodeUserState
9 from mygpo.db import QueryParameterMissing
10 from mygpo.db.couchdb import get_userdata_database, get_single_result
11 from mygpo.cache import cache_result
12 from mygpo.decorators import repeat_on_conflict
16 def episode_state_for_user_episode(user, episode):
18 if not user:
19 raise QueryParameterMissing('user')
21 if not episode:
22 raise QueryParameterMissing('episode')
24 if hasattr(episode, '_id'):
25 episode_id = episode._id
26 else:
27 episode_id = episode.get_id()
28 key = 'episode-state-userid-%s-episodeid-%s' % (sha1(user.profile.uuid.hex).hexdigest(),
29 sha1(episode_id).hexdigest())
31 # Disabled as cache invalidation does not work properly
32 # state = cache.get(key)
33 # if state:
34 # return state
36 udb = get_userdata_database()
37 state = get_single_result(udb, 'episode_states/by_user_episode',
38 key = [user.profile.uuid.hex, episode_id],
39 include_docs = True,
40 limit = 1,
41 schema = EpisodeUserState,
44 if state:
45 cache.set(key, state)
46 return state
48 else:
49 if isinstance(episode.podcast, unicode):
50 podcast = Podcast.objects.get_by_any_id(episode.podcast)
51 else:
52 podcast = episode.podcast
54 state = EpisodeUserState()
55 state.episode = episode_id
56 state.podcast = podcast.get_id()
57 state.user = user.profile.uuid.hex
58 state.ref_url = episode.url
59 state.podcast_ref_url = podcast.url
60 # don't cache here, because the state is saved by the calling function
62 return state
66 def all_episode_states(episode):
68 if not episode:
69 raise QueryParameterMissing('episode')
71 if isinstance(episode.podcast, unicode):
72 podcast_id = episode.podcast
73 else:
74 podcast_id = episode.podcast.get_id()
76 if hasattr(episode, '_id'):
77 episode_id = episode._id
78 else:
79 episode_id = episode.get_id()
81 udb = get_userdata_database()
82 r = udb.view('episode_states/by_podcast_episode',
83 startkey = [podcast_id, episode_id, None],
84 endkey = [podcast_id, episode_id, {}],
85 include_docs = True,
86 schema = EpisodeUserState,
89 states = list(r)
91 for state in states:
92 state.set_db(udb)
94 return states
97 @cache_result(timeout=60*60)
98 def podcast_listener_count(episode):
99 """ returns the number of users that have listened to this podcast """
101 if not episode:
102 raise QueryParameterMissing('episode')
104 udb = get_userdata_database()
105 r = get_single_result(udb, 'listeners/by_podcast',
106 startkey = [episode.get_id(), None],
107 endkey = [episode.get_id(), {}],
108 group = True,
109 group_level = 1,
110 reduce = True,
111 stale = 'update_after',
113 return r['value'] if r else 0
116 @cache_result(timeout=60*60)
117 def podcast_listener_count_timespan(podcast, start=None, end={}):
118 """ returns (date, listener-count) tuples for all days w/ listeners """
120 if not podcast:
121 raise QueryParameterMissing('podcast')
123 if isinstance(start, datetime):
124 start = start.isoformat()
126 if isinstance(end, datetime):
127 end = end.isoformat()
129 udb = get_userdata_database()
130 r = udb.view('listeners/by_podcast',
131 startkey = [podcast.get_id(), start],
132 endkey = [podcast.get_id(), end],
133 group = True,
134 group_level = 2,
135 reduce = True,
136 stale = 'update_after',
139 return map(_wrap_listener_count, r)
142 @cache_result(timeout=60*60)
143 def episode_listener_counts(episode):
144 """ (Episode-Id, listener-count) tuples for episodes w/ listeners """
146 if not episode:
147 raise QueryParameterMissing('episode')
149 udb = get_userdata_database()
150 r = udb.view('listeners/by_podcast_episode',
151 startkey = [episode.get_id(), None, None],
152 endkey = [episode.get_id(), {}, {}],
153 group = True,
154 group_level = 2,
155 reduce = True,
156 stale = 'update_after',
159 return map(_wrap_listeners, r)
163 def get_podcasts_episode_states(podcast, user_id):
164 """ Returns the latest episode actions for the podcast's episodes """
166 if not podcast:
167 raise QueryParameterMissing('podcast')
169 if not user_id:
170 raise QueryParameterMissing('user_id')
172 udb = get_userdata_database()
173 res = udb.view('episode_states/by_user_podcast',
174 startkey = [user_id, podcast.get_id(), None],
175 endkey = [user_id, podcast.get_id(), {}],
178 return map(lambda r: r['value'], res)
182 @cache_result(timeout=60*60)
183 def episode_listener_count(episode, start=None, end={}):
184 """ returns the number of users that have listened to this episode """
186 if not episode:
187 raise QueryParameterMissing('episode')
189 udb = get_userdata_database()
190 r = get_single_result(udb, 'listeners/by_episode',
191 startkey = [episode.id, start],
192 endkey = [episode.id, end],
193 group = True,
194 group_level = 2,
195 reduce = True,
196 stale = 'update_after',
198 return r['value'] if r else 0
202 @cache_result(timeout=60*60)
203 def episode_listener_count_timespan(episode, start=None, end={}):
204 """ returns (date, listener-count) tuples for all days w/ listeners """
206 if not episode:
207 raise QueryParameterMissing('episode')
210 if isinstance(start, datetime):
211 start = start.isoformat()
213 if isinstance(end, datetime):
214 end = end.isoformat()
216 udb = get_userdata_database()
217 r = udb.view('listeners/by_episode',
218 startkey = [episode._id, start],
219 endkey = [episode._id, end],
220 group = True,
221 group_level = 3,
222 reduce = True,
223 stale = 'update_after',
226 return map(_wrap_listener_count, r)
230 def episode_state_for_ref_urls(user, podcast_url, episode_url):
232 if not user:
233 raise QueryParameterMissing('user')
235 if not podcast_url:
236 raise QueryParameterMissing('podcast_url')
238 if not episode_url:
239 raise QueryParameterMissing('episode_url')
242 cache_key = 'episode-state-%s-%s-%s' % (user.profile.uuid.hex,
243 sha1(podcast_url).hexdigest(),
244 sha1(episode_url).hexdigest())
246 state = cache.get(cache_key)
247 if state:
248 return state
250 udb = get_userdata_database()
251 state = get_single_result(udb, 'episode_states/by_ref_urls',
252 key = [user.profile.uuid.hex, podcast_url, episode_url],
253 limit = 1,
254 include_docs=True,
255 schema = EpisodeUserState,
258 if state:
259 state.ref_url = episode_url
260 state.podcast_ref_url = podcast_url
261 cache.set(cache_key, state, 60*60)
262 return state
264 else:
265 podcast = Podcast.objects.get_or_create_for_url(podcast_url)
266 episode = Episode.objects.get_or_create_for_url(podcast, episode_url)
267 return episode_state_for_user_episode(user, episode)
271 def get_episode_actions(user_id, since=None, until={}, podcast_id=None,
272 device_id=None, limit=1000):
273 """ Returns Episode Actions for the given criteria
275 There is an upper limit on how many actions will be returned; until is the
276 timestamp of the last episode action.
279 if not user_id:
280 raise QueryParameterMissing('user_id')
282 if since >= until:
283 return [], until
285 if not podcast_id and not device_id:
286 view = 'episode_actions/by_user'
287 startkey = [user_id, since]
288 endkey = [user_id, until]
290 elif podcast_id and not device_id:
291 view = 'episode_actions/by_podcast'
292 startkey = [user_id, podcast_id, since]
293 endkey = [user_id, podcast_id, until]
295 elif device_id and not podcast_id:
296 view = 'episode_actions/by_device'
297 startkey = [user_id, device_id, since]
298 endkey = [user_id, device_id, until]
300 else:
301 view = 'episode_actions/by_podcast_device'
302 startkey = [user_id, podcast_id, device_id, since]
303 endkey = [user_id, podcast_id, device_id, until]
305 udb = get_userdata_database()
306 res = udb.view(view,
307 startkey = startkey,
308 endkey = endkey,
309 limit = limit,
312 results = list(res)
313 actions = map(lambda r: r['value'], results)
314 if actions:
315 # the upload_timestamp is always the last part of the key
316 until = results[-1]['key'][-1]
318 return actions, until
322 @cache_result(timeout=60*60)
323 def episode_states_count():
324 udb = get_userdata_database()
325 r = udb.view('episode_states/by_user_episode',
326 limit = 0,
327 stale = 'update_after',
329 return r.total_rows
332 def get_nth_episode_state(n):
333 udb = get_userdata_database()
334 state = get_single_result(udb, 'episode_states/by_user_episode',
335 skip = n,
336 include_docs = True,
337 limit = 1,
338 schema = EpisodeUserState,
341 return state
344 def get_duplicate_episode_states(user, episode):
346 if not user:
347 raise QueryParameterMissing('user')
349 if not episode:
350 raise QueryParameterMissing('episode')
352 udb = get_userdata_database()
353 r = udb.view('episode_states/by_user_episode',
354 key = [user, episode],
355 include_docs = True,
356 schema = EpisodeUserState,
359 states = list(r)
361 for state in states:
362 state.set_db(udb)
364 return states
367 def _wrap_listener_count(res):
368 date = parser.parse(res['key'][1]).date()
369 listeners = res['value']
370 return (date, listeners)
373 def _wrap_listeners(res):
374 episode = res['key'][1]
375 listeners = res['value']
376 return (episode, listeners)
379 @cache_result(timeout=60*60)
380 def get_heatmap(podcast_id, episode_id, user_id):
381 udb = get_userdata_database()
383 group_level = len(filter(None, [podcast_id, episode_id, user_id]))
385 r = udb.view('heatmap/by_episode',
386 startkey = [podcast_id, episode_id, user_id],
387 endkey = [podcast_id, episode_id or {}, user_id or {}],
388 reduce = True,
389 group = True,
390 group_level = group_level,
391 stale = 'update_after',
393 # TODO: Heatmap not available during transition to Django ORM
394 r = False
396 if not r:
397 return [], []
399 else:
400 res = r.first()['value']
401 return res['heatmap'], res['borders']
404 @repeat_on_conflict(['state'])
405 def add_episode_actions(state, actions):
406 udb = get_userdata_database()
407 state.add_actions(actions)
408 udb.save_doc(state)
411 @repeat_on_conflict(['state'])
412 def update_episode_state_object(state, podcast_id, episode_id=None):
413 state.podcast = podcast_id
415 if episode_id is not None:
416 state.episode = episode_id
418 udb = get_userdata_database()
419 udb.save_doc(state)
422 @repeat_on_conflict(['state'])
423 def merge_episode_states(state, state2):
424 state.add_actions(state2.actions)
426 # overwrite settings in state2 with state's settings
427 settings = state2.settings
428 settings.update(state.settings)
429 state.settings = settings
431 merged_ids = set(state.merged_ids + [state2._id] + state2.merged_ids)
432 state.merged_ids = filter(None, merged_ids)
434 state.chapters = list(set(state.chapters + state2.chapters))
436 udb = get_userdata_database()
437 udb.save_doc(state)
440 @repeat_on_conflict(['state'])
441 def delete_episode_state(state):
442 udb = get_userdata_database()
443 udb.delete_doc(state)
446 @repeat_on_conflict(['episode_state'])
447 def update_episode_chapters(episode_state, add=[], rem=[]):
448 """ Updates the Chapter list
450 * add contains the chapters to be added
452 * rem contains tuples of (start, end) times. Chapters that match
453 both endpoints will be removed
456 for chapter in add:
457 episode_state.chapters = episode_state.chapters + [chapter]
459 for start, end in rem:
460 keep = lambda c: c.start != start or c.end != end
461 episode_state.chapters = filter(keep, episode_state.chapters)
463 episode_state.save()
466 def chapters_for_episode(episode_id):
468 if not episode_id:
469 raise QueryParameterMissing('episode_id')
471 udb = get_userdata_database()
472 r = udb.view('chapters/by_episode',
473 startkey = [episode_id, None],
474 endkey = [episode_id, {}],
477 return map(_wrap_chapter, r)
480 def _wrap_chapter(res):
481 from mygpo.users.models import Chapter
482 user = res['key'][1]
483 chapter = Chapter.wrap(res['value'])
484 udb = get_userdata_database()
485 chapter.set_db(udb)
486 return (user, chapter)
489 @repeat_on_conflict(['episode_state'])
490 def set_episode_favorite(episode_state, is_fav):
491 udb = get_userdata_database()
492 episode_state.set_favorite(is_fav)
493 udb.save_doc(episode_state)