Query "userdata" views in correct database
[mygpo.git] / mygpo / db / couchdb / episode.py
bloba33e16886e86fd04a8098d6bd0d68a438fbd497e
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 episode = cache.get(key)
188 if episode:
189 return episode
191 r = Episode.view('episodes/by_podcast_url',
192 key = [podcast_id, episode_url],
193 include_docs = True,
194 reduce = False,
197 if r:
198 episode = r.first()
200 if episode.needs_update:
201 incomplete_obj.send_robust(sender=episode)
202 else:
203 cache.set(key, episode)
204 return episode
206 if create:
207 episode = Episode()
208 episode.created_timestamp = get_timestamp(datetime.utcnow())
209 episode.podcast = podcast_id
210 episode.urls = [episode_url]
211 episode.save()
212 incomplete_obj.send_robust(sender=episode)
213 return episode
215 return None
218 def episode_for_slug_id(p_slug_id, e_slug_id):
219 """ Returns the Episode for Podcast Slug/Id and Episode Slug/Id """
221 if not p_slug_id:
222 raise QueryParameterMissing('p_slug_id')
224 if not e_slug_id:
225 raise QueryParameterMissing('e_slug_id')
228 # The Episode-Id is unique, so take that
229 if is_couchdb_id(e_slug_id):
230 return episode_by_id(e_slug_id)
232 # If we search using a slug, we need the Podcast's Id
233 if is_couchdb_id(p_slug_id):
234 p_id = p_slug_id
235 else:
236 podcast = podcast_for_slug_id(p_slug_id)
238 if podcast is None:
239 return None
241 p_id = podcast.get_id()
243 return episode_for_slug(p_id, e_slug_id)
246 @cache_result(timeout=60*60)
247 def episode_count():
248 r = Episode.view('episodes/by_podcast',
249 reduce = True,
250 stale = 'update_after',
252 return r.one()['value'] if r else 0
255 def episodes_to_dict(ids, use_cache=False):
257 if ids is None:
258 raise QueryParameterMissing('ids')
260 if not ids:
261 return {}
264 ids = list(set(ids))
265 objs = dict()
267 cache_objs = []
268 if use_cache:
269 res = cache.get_many(ids)
270 cache_objs.extend(res.values())
271 ids = [x for x in ids if x not in res.keys()]
273 db_objs = list(episodes_by_id(ids))
275 for obj in (cache_objs + db_objs):
277 # get_multi returns dict {'key': _id, 'error': 'not found'}
278 # for non-existing objects
279 if isinstance(obj, dict) and 'error' in obj:
280 _id = obj['key']
281 objs[_id] = None
282 continue
284 for i in obj.get_ids():
285 objs[i] = obj
287 if use_cache:
288 cache.set_many(dict( (obj._id, obj) for obj in db_objs))
290 return objs
293 def episode_slugs_per_podcast(podcast_id, base_slug):
295 if not podcast_id:
296 raise QueryParameterMissing('podcast_id')
299 res = Episode.view('episodes/by_slug',
300 startkey = [podcast_id, base_slug],
301 endkey = [podcast_id, base_slug + 'ZZZZZ'],
302 wrap_doc = False,
304 return [r['key'][1] for r in res]
307 def episodes_for_podcast_uncached(podcast, since=None, until={}, **kwargs):
309 if not podcast:
310 raise QueryParameterMissing('podcast')
313 if kwargs.get('descending', False):
314 since, until = until, since
316 if isinstance(since, datetime):
317 since = since.isoformat()
319 if isinstance(until, datetime):
320 until = until.isoformat()
322 res = Episode.view('episodes/by_podcast',
323 startkey = [podcast.get_id(), since],
324 endkey = [podcast.get_id(), until],
325 include_docs = True,
326 reduce = False,
327 **kwargs
330 episodes = list(res)
332 for episode in episodes:
333 if episode.needs_update:
334 incomplete_obj.send_robust(sender=episode)
336 return episodes
339 episodes_for_podcast = cache_result(timeout=60*60)(episodes_for_podcast_uncached)
342 @cache_result(timeout=60*60)
343 def episode_count_for_podcast(podcast, since=None, until={}, **kwargs):
345 if not podcast:
346 raise QueryParameterMissing('podcast')
349 if kwargs.get('descending', False):
350 since, until = until, since
352 if isinstance(since, datetime):
353 since = since.isoformat()
355 if isinstance(until, datetime):
356 until = until.isoformat()
358 res = Episode.view('episodes/by_podcast',
359 startkey = [podcast.get_id(), since],
360 endkey = [podcast.get_id(), until],
361 reduce = True,
362 group_level = 1,
363 **kwargs
366 return res.one()['value']
369 def favorite_episodes_for_user(user):
371 if not user:
372 raise QueryParameterMissing('user')
374 udb = get_userdata_database()
375 favorites = udb.view('favorites/episodes_by_user',
376 key = user._id,
377 include_docs = True,
378 wrapper = Episode,
381 episodes = list(favorites)
383 for episode in episodes:
384 if episode.needs_update:
385 incomplete_obj.send_robust(sender=episode)
387 return episodes
390 def chapters_for_episode(episode_id):
392 if not episode_id:
393 raise QueryParameterMissing('episode_id')
395 udb = get_userdata_database()
396 r = udb.view('chapters/by_episode',
397 startkey = [episode_id, None],
398 endkey = [episode_id, {}],
401 return map(_wrap_chapter, r)
404 def filetype_stats():
405 """ Returns a filetype counter over all episodes """
407 db = get_main_database()
408 r = db.view('episode_stats/filetypes',
409 stale = 'update_after',
410 reduce = True,
411 group_level = 1,
414 return Counter({x['key']: x['value'] for x in r})
417 def _wrap_chapter(res):
418 from mygpo.users.models import Chapter
419 user = res['key'][1]
420 chapter = Chapter.wrap(res['value'])
421 return (user, chapter)
424 @repeat_on_conflict(['episode'])
425 def set_episode_slug(episode, slug):
426 """ sets slug as new main slug of the episode, moves other to merged """
427 episode.set_slug(slug)
428 episode.save()
431 @repeat_on_conflict(['episode'])
432 def remove_episode_slug(episode, slug):
433 """ removes slug from main and merged slugs """
434 episode.remove_slug(slug)
435 episode.save()