disable query caching, invalidation not working properly
[mygpo.git] / mygpo / db / couchdb / episode.py
blobf42bae067aa6708b1b4ed4bb429a02d8055365e0
1 from hashlib import sha1
2 from datetime import datetime
3 from collections import Counter
5 from couchdbkit import MultipleResultsFound
7 from django.core.cache import cache
9 from mygpo.core.models import Podcast, Episode, MergedIdException
10 from mygpo.core.signals import incomplete_obj
11 from mygpo.cache import cache_result
12 from mygpo.decorators import repeat_on_conflict
13 from mygpo.utils import get_timestamp
14 from mygpo.db import QueryParameterMissing
15 from mygpo.db.couchdb.utils import is_couchdb_id
16 from mygpo.db.couchdb import get_main_database, get_userdata_database
17 from mygpo.db.couchdb.podcast import podcast_for_url, podcast_for_slug_id
19 import logging
20 logger = logging.getLogger(__name__)
23 @cache_result(timeout=60*60)
24 def episode_by_id(episode_id, current_id=False):
26 if not episode_id:
27 raise QueryParameterMissing('episode_id')
29 r = Episode.view('episodes/by_id',
30 key = episode_id,
31 include_docs = True,
34 if not r:
35 return None
37 obj = r.one()
38 if current_id and obj._id != episode_id:
39 raise MergedIdException(obj, obj._id)
41 if obj.needs_update:
42 incomplete_obj.send_robust(sender=obj)
44 return obj
47 @cache_result(timeout=60*60)
48 def episodes_by_id(episode_ids):
50 if episode_ids is None:
51 raise QueryParameterMissing('episode_ids')
53 if not episode_ids:
54 return []
56 r = Episode.view('episodes/by_id',
57 include_docs = True,
58 keys = episode_ids,
61 episodes = list(r)
63 for episode in episodes:
64 if episode.needs_update:
65 incomplete_obj.send_robust(sender=episode)
67 return episodes
70 @cache_result(timeout=60*60)
71 def episode_for_oldid(oldid):
73 if not oldid:
74 raise QueryParameterMissing('oldid')
76 oldid = int(oldid)
77 r = Episode.view('episodes/by_oldid',
78 key = oldid,
79 limit = 1,
80 include_docs = True,
83 if not r:
84 return None
86 episode = r.one()
88 if episode.needs_update:
89 incomplete_obj.send_robust(sender=episode)
91 return episode
94 @cache_result(timeout=60*60)
95 def episode_for_slug(podcast_id, episode_slug):
97 if not podcast_id:
98 raise QueryParameterMissing('podcast_id')
100 if not episode_slug:
101 raise QueryParameterMissing('episode_slug')
103 _view = 'episodes/by_slug'
105 r = Episode.view(_view,
106 key = [podcast_id, episode_slug],
107 include_docs = True,
110 if not r:
111 return None
113 try:
114 episode = r.one()
116 except MultipleResultsFound as ex:
117 logger.exception('Multiple results found in %s with params %s',
118 _view, r.params)
119 episode = r.first()
121 if episode.needs_update:
122 incomplete_obj.send_robust(sender=episode)
124 return episode
127 def episodes_for_slug(podcast_id, episode_slug):
128 """ returns all episodes for the given slug
130 this should normally only return one episode, but there might be multiple
131 due to resolved replication conflicts, etc """
133 if not podcast_id:
134 raise QueryParameterMissing('podcast_id')
136 if not episode_slug:
137 raise QueryParameterMissing('episode_slug')
139 r = Episode.view('episodes/by_slug',
140 key = [podcast_id, episode_slug],
141 include_docs = True,
144 if not r:
145 return []
147 episodes = r.all()
149 for episode in episodes:
150 if episode.needs_update:
151 incomplete_obj.send_robust(sender=episode)
153 return episodes
157 def episode_for_podcast_url(podcast_url, episode_url, create=False):
159 if not podcast_url:
160 raise QueryParameterMissing('podcast_url')
162 if not episode_url:
163 raise QueryParameterMissing('episode_url')
166 podcast = podcast_for_url(podcast_url, create=create)
168 if not podcast: # podcast does not exist and should not be created
169 return None
171 return episode_for_podcast_id_url(podcast.get_id(), episode_url, create)
174 def episode_for_podcast_id_url(podcast_id, episode_url, create=False):
176 if not podcast_id:
177 raise QueryParameterMissing('podcast_id')
179 if not episode_url:
180 raise QueryParameterMissing('episode_url')
183 key = u'episode-podcastid-%s-url-%s' % (
184 sha1(podcast_id.encode('utf-8')).hexdigest(),
185 sha1(episode_url.encode('utf-8')).hexdigest())
187 # Disabled as cache invalidation is not working properly
188 # episode = cache.get(key)
189 # if episode:
190 # return episode
192 r = Episode.view('episodes/by_podcast_url',
193 key = [podcast_id, episode_url],
194 include_docs = True,
195 reduce = False,
198 if r:
199 episode = r.first()
201 if episode.needs_update:
202 incomplete_obj.send_robust(sender=episode)
203 else:
204 cache.set(key, episode)
205 return episode
207 if create:
208 episode = Episode()
209 episode.created_timestamp = get_timestamp(datetime.utcnow())
210 episode.podcast = podcast_id
211 episode.urls = [episode_url]
212 episode.save()
213 incomplete_obj.send_robust(sender=episode)
214 return episode
216 return None
219 def episode_for_slug_id(p_slug_id, e_slug_id):
220 """ Returns the Episode for Podcast Slug/Id and Episode Slug/Id """
222 if not p_slug_id:
223 raise QueryParameterMissing('p_slug_id')
225 if not e_slug_id:
226 raise QueryParameterMissing('e_slug_id')
229 # The Episode-Id is unique, so take that
230 if is_couchdb_id(e_slug_id):
231 return episode_by_id(e_slug_id)
233 # If we search using a slug, we need the Podcast's Id
234 if is_couchdb_id(p_slug_id):
235 p_id = p_slug_id
236 else:
237 podcast = podcast_for_slug_id(p_slug_id)
239 if podcast is None:
240 return None
242 p_id = podcast.get_id()
244 return episode_for_slug(p_id, e_slug_id)
247 @cache_result(timeout=60*60)
248 def episode_count():
249 r = Episode.view('episodes/by_podcast',
250 reduce = True,
251 stale = 'update_after',
253 return r.one()['value'] if r else 0
256 def episodes_to_dict(ids, use_cache=False):
258 if ids is None:
259 raise QueryParameterMissing('ids')
261 if not ids:
262 return {}
265 ids = list(set(ids))
266 objs = dict()
268 cache_objs = []
269 if use_cache:
270 res = cache.get_many(ids)
271 cache_objs.extend(res.values())
272 ids = [x for x in ids if x not in res.keys()]
274 db_objs = list(episodes_by_id(ids))
276 for obj in (cache_objs + db_objs):
278 # get_multi returns dict {'key': _id, 'error': 'not found'}
279 # for non-existing objects
280 if isinstance(obj, dict) and 'error' in obj:
281 _id = obj['key']
282 objs[_id] = None
283 continue
285 for i in obj.get_ids():
286 objs[i] = obj
288 if use_cache:
289 cache.set_many(dict( (obj._id, obj) for obj in db_objs))
291 return objs
294 def episode_slugs_per_podcast(podcast_id, base_slug):
296 if not podcast_id:
297 raise QueryParameterMissing('podcast_id')
300 res = Episode.view('episodes/by_slug',
301 startkey = [podcast_id, base_slug],
302 endkey = [podcast_id, base_slug + 'ZZZZZ'],
303 wrap_doc = False,
305 return [r['key'][1] for r in res]
308 def episodes_for_podcast_uncached(podcast, since=None, until={}, **kwargs):
310 if not podcast:
311 raise QueryParameterMissing('podcast')
314 if kwargs.get('descending', False):
315 since, until = until, since
317 if isinstance(since, datetime):
318 since = since.isoformat()
320 if isinstance(until, datetime):
321 until = until.isoformat()
323 res = Episode.view('episodes/by_podcast',
324 startkey = [podcast.get_id(), since],
325 endkey = [podcast.get_id(), until],
326 include_docs = True,
327 reduce = False,
328 **kwargs
331 episodes = list(res)
333 for episode in episodes:
334 if episode.needs_update:
335 incomplete_obj.send_robust(sender=episode)
337 return episodes
340 episodes_for_podcast = cache_result(timeout=60*60)(episodes_for_podcast_uncached)
343 @cache_result(timeout=60*60)
344 def episode_count_for_podcast(podcast, since=None, until={}, **kwargs):
346 if not podcast:
347 raise QueryParameterMissing('podcast')
350 if kwargs.get('descending', False):
351 since, until = until, since
353 if isinstance(since, datetime):
354 since = since.isoformat()
356 if isinstance(until, datetime):
357 until = until.isoformat()
359 res = Episode.view('episodes/by_podcast',
360 startkey = [podcast.get_id(), since],
361 endkey = [podcast.get_id(), until],
362 reduce = True,
363 group_level = 1,
364 **kwargs
367 return res.one()['value']
370 def favorite_episodes_for_user(user):
372 if not user:
373 raise QueryParameterMissing('user')
375 udb = get_userdata_database()
376 favorites = udb.view('favorites/episodes_by_user',
377 key = user._id,
378 include_docs = True,
379 schema = Episode,
382 episodes = list(favorites)
384 for episode in episodes:
385 if episode.needs_update:
386 incomplete_obj.send_robust(sender=episode)
388 return episodes
391 def chapters_for_episode(episode_id):
393 if not episode_id:
394 raise QueryParameterMissing('episode_id')
396 udb = get_userdata_database()
397 r = udb.view('chapters/by_episode',
398 startkey = [episode_id, None],
399 endkey = [episode_id, {}],
402 return map(_wrap_chapter, r)
405 def filetype_stats():
406 """ Returns a filetype counter over all episodes """
408 db = get_main_database()
409 r = db.view('episode_stats/filetypes',
410 stale = 'update_after',
411 reduce = True,
412 group_level = 1,
415 return Counter({x['key']: x['value'] for x in r})
418 def _wrap_chapter(res):
419 from mygpo.users.models import Chapter
420 user = res['key'][1]
421 chapter = Chapter.wrap(res['value'])
422 return (user, chapter)
425 @repeat_on_conflict(['episode'])
426 def set_episode_slug(episode, slug):
427 """ sets slug as new main slug of the episode, moves other to merged """
428 episode.set_slug(slug)
429 episode.save()
432 @repeat_on_conflict(['episode'])
433 def remove_episode_slug(episode, slug):
434 """ removes slug from main and merged slugs """
435 episode.remove_slug(slug)
436 episode.save()
439 @repeat_on_conflict(['episode_state'])
440 def set_episode_favorite(episode_state, is_fav):
441 udb = get_userdata_database()
442 episode_state.set_favorite(is_fav)
443 udb.save_doc(episode_state)