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
):
19 raise QueryParameterMissing('user')
22 raise QueryParameterMissing('episode')
24 if hasattr(episode
, '_id'):
25 episode_id
= episode
._id
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)
36 udb
= get_userdata_database()
37 state
= get_single_result(udb
, 'episode_states/by_user_episode',
38 key
= [user
.profile
.uuid
.hex, episode_id
],
41 schema
= EpisodeUserState
,
49 if isinstance(episode
.podcast
, unicode):
50 podcast
= Podcast
.objects
.get_by_any_id(episode
.podcast
)
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
66 def all_episode_states(episode
):
69 raise QueryParameterMissing('episode')
71 if isinstance(episode
.podcast
, unicode):
72 podcast_id
= episode
.podcast
74 podcast_id
= episode
.podcast
.get_id()
76 if hasattr(episode
, '_id'):
77 episode_id
= episode
._id
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
, {}],
86 schema
= EpisodeUserState
,
97 @cache_result(timeout
=60*60)
98 def podcast_listener_count(episode
):
99 """ returns the number of users that have listened to this podcast """
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(), {}],
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 """
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
],
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 """
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(), {}, {}],
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 """
167 raise QueryParameterMissing('podcast')
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 """
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
],
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 """
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
],
223 stale
= 'update_after',
226 return map(_wrap_listener_count
, r
)
230 def episode_state_for_ref_urls(user
, podcast_url
, episode_url
):
233 raise QueryParameterMissing('user')
236 raise QueryParameterMissing('podcast_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
)
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
],
255 schema
= EpisodeUserState
,
259 state
.ref_url
= episode_url
260 state
.podcast_ref_url
= podcast_url
261 cache
.set(cache_key
, state
, 60*60)
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.
280 raise QueryParameterMissing('user_id')
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
]
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()
313 actions
= map(lambda r
: r
['value'], results
)
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',
327 stale
= 'update_after',
332 def get_nth_episode_state(n
):
333 udb
= get_userdata_database()
334 state
= get_single_result(udb
, 'episode_states/by_user_episode',
338 schema
= EpisodeUserState
,
344 def get_duplicate_episode_states(user
, episode
):
347 raise QueryParameterMissing('user')
350 raise QueryParameterMissing('episode')
352 udb
= get_userdata_database()
353 r
= udb
.view('episode_states/by_user_episode',
354 key
= [user
, episode
],
356 schema
= EpisodeUserState
,
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 {}],
390 group_level
= group_level
,
391 stale
= 'update_after',
393 # TODO: Heatmap not available during transition to Django ORM
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
)
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()
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()
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
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
)
466 def chapters_for_episode(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
483 chapter
= Chapter
.wrap(res
['value'])
484 udb
= get_userdata_database()
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
)