fix episode_count_for_podcast() for podcasts w/o episodes
[mygpo.git] / mygpo / db / couchdb / episode.py
blob7e7fe76b6a781764bf94c8485d927f273641f936
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_current(podcast, limit=None):
310 if not podcast:
311 raise QueryParameterMissing('podcast')
313 res = Episode.view('episodes/by_podcast_current',
314 startkey = podcast.get_id(),
315 endkey = podcast.get_id(),
316 include_docs = True,
317 limit = limit,
320 episodes = list(res)
322 for episode in episodes:
323 if episode.needs_update:
324 incomplete_obj.send_robust(sender=episode)
326 return episodes
330 def episodes_for_podcast_uncached(podcast, since=None, until={}, **kwargs):
332 if not podcast:
333 raise QueryParameterMissing('podcast')
336 if kwargs.get('descending', False):
337 since, until = until, since
339 if isinstance(since, datetime):
340 since = since.isoformat()
342 if isinstance(until, datetime):
343 until = until.isoformat()
345 res = Episode.view('episodes/by_podcast',
346 startkey = [podcast.get_id(), since],
347 endkey = [podcast.get_id(), until],
348 include_docs = True,
349 reduce = False,
350 **kwargs
353 episodes = list(res)
355 for episode in episodes:
356 if episode.needs_update:
357 incomplete_obj.send_robust(sender=episode)
359 return episodes
362 episodes_for_podcast = cache_result(timeout=60*60)(episodes_for_podcast_uncached)
365 @cache_result(timeout=60*60)
366 def episode_count_for_podcast(podcast, since=None, until={}, **kwargs):
368 if not podcast:
369 raise QueryParameterMissing('podcast')
372 if kwargs.get('descending', False):
373 since, until = until, since
375 if isinstance(since, datetime):
376 since = since.isoformat()
378 if isinstance(until, datetime):
379 until = until.isoformat()
381 res = Episode.view('episodes/by_podcast',
382 startkey = [podcast.get_id(), since],
383 endkey = [podcast.get_id(), until],
384 reduce = True,
385 group_level = 1,
386 **kwargs
389 return res.one()['value'] if res else 0
392 def favorite_episode_ids_for_user(user):
394 if not user:
395 raise QueryParameterMissing('user')
397 udb = get_userdata_database()
398 favorites = udb.view('favorites/episodes_by_user',
399 key = user._id,
402 return set(x['value']['_id'] for x in favorites)
405 def favorite_episodes_for_user(user):
406 episode_ids = list(favorite_episode_ids_for_user(user))
407 return episodes_by_id(episode_ids)
410 def chapters_for_episode(episode_id):
412 if not episode_id:
413 raise QueryParameterMissing('episode_id')
415 udb = get_userdata_database()
416 r = udb.view('chapters/by_episode',
417 startkey = [episode_id, None],
418 endkey = [episode_id, {}],
421 return map(_wrap_chapter, r)
424 def filetype_stats():
425 """ Returns a filetype counter over all episodes """
427 db = get_main_database()
428 r = db.view('episode_stats/filetypes',
429 stale = 'update_after',
430 reduce = True,
431 group_level = 1,
434 return Counter({x['key']: x['value'] for x in r})
437 def _wrap_chapter(res):
438 from mygpo.users.models import Chapter
439 user = res['key'][1]
440 chapter = Chapter.wrap(res['value'])
441 udb = get_userdata_database()
442 chapter.set_db(udb)
443 return (user, chapter)
446 @repeat_on_conflict(['episode'])
447 def set_episode_slug(episode, slug):
448 """ sets slug as new main slug of the episode, moves other to merged """
449 episode.set_slug(slug)
450 episode.save()
453 @repeat_on_conflict(['episode'])
454 def remove_episode_slug(episode, slug):
455 """ removes slug from main and merged slugs """
456 episode.remove_slug(slug)
457 episode.save()
460 @repeat_on_conflict(['episode_state'])
461 def set_episode_favorite(episode_state, is_fav):
462 udb = get_userdata_database()
463 episode_state.set_favorite(is_fav)
464 udb.save_doc(episode_state)
467 @repeat_on_conflict(['episode'])
468 def set_episode_listeners(episode, listeners):
470 if episode.listeners == listeners:
471 return False
473 episode.listeners = listeners
475 db = get_main_database()
476 db.save_doc(episode)
477 return True