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
20 logger
= logging
.getLogger(__name__
)
23 @cache_result(timeout
=60*60)
24 def episode_by_id(episode_id
, current_id
=False):
27 raise QueryParameterMissing('episode_id')
29 r
= Episode
.view('episodes/by_id',
38 if current_id
and obj
._id
!= episode_id
:
39 raise MergedIdException(obj
, obj
._id
)
42 incomplete_obj
.send_robust(sender
=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')
56 r
= Episode
.view('episodes/by_id',
63 for episode
in episodes
:
64 if episode
.needs_update
:
65 incomplete_obj
.send_robust(sender
=episode
)
70 @cache_result(timeout
=60*60)
71 def episode_for_oldid(oldid
):
74 raise QueryParameterMissing('oldid')
77 r
= Episode
.view('episodes/by_oldid',
88 if episode
.needs_update
:
89 incomplete_obj
.send_robust(sender
=episode
)
94 @cache_result(timeout
=60*60)
95 def episode_for_slug(podcast_id
, episode_slug
):
98 raise QueryParameterMissing('podcast_id')
101 raise QueryParameterMissing('episode_slug')
103 _view
= 'episodes/by_slug'
105 r
= Episode
.view(_view
,
106 key
= [podcast_id
, episode_slug
],
116 except MultipleResultsFound
as ex
:
117 logger
.exception('Multiple results found in %s with params %s',
121 if episode
.needs_update
:
122 incomplete_obj
.send_robust(sender
=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 """
134 raise QueryParameterMissing('podcast_id')
137 raise QueryParameterMissing('episode_slug')
139 r
= Episode
.view('episodes/by_slug',
140 key
= [podcast_id
, episode_slug
],
149 for episode
in episodes
:
150 if episode
.needs_update
:
151 incomplete_obj
.send_robust(sender
=episode
)
157 def episode_for_podcast_url(podcast_url
, episode_url
, create
=False):
160 raise QueryParameterMissing('podcast_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
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):
177 raise QueryParameterMissing('podcast_id')
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)
192 r
= Episode
.view('episodes/by_podcast_url',
193 key
= [podcast_id
, episode_url
],
201 if episode
.needs_update
:
202 incomplete_obj
.send_robust(sender
=episode
)
204 cache
.set(key
, episode
)
209 episode
.created_timestamp
= get_timestamp(datetime
.utcnow())
210 episode
.podcast
= podcast_id
211 episode
.urls
= [episode_url
]
213 incomplete_obj
.send_robust(sender
=episode
)
219 def episode_for_slug_id(p_slug_id
, e_slug_id
):
220 """ Returns the Episode for Podcast Slug/Id and Episode Slug/Id """
223 raise QueryParameterMissing('p_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
):
237 podcast
= podcast_for_slug_id(p_slug_id
)
242 p_id
= podcast
.get_id()
244 return episode_for_slug(p_id
, e_slug_id
)
247 @cache_result(timeout
=60*60)
249 r
= Episode
.view('episodes/by_podcast',
251 stale
= 'update_after',
253 return r
.one()['value'] if r
else 0
256 def episodes_to_dict(ids
, use_cache
=False):
259 raise QueryParameterMissing('ids')
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
:
285 for i
in obj
.get_ids():
289 cache
.set_many(dict( (obj
._id
, obj
) for obj
in db_objs
))
294 def episode_slugs_per_podcast(podcast_id
, base_slug
):
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'],
305 return [r
['key'][1] for r
in res
]
308 def episodes_for_podcast_uncached(podcast
, since
=None, until
={}, **kwargs
):
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
],
333 for episode
in episodes
:
334 if episode
.needs_update
:
335 incomplete_obj
.send_robust(sender
=episode
)
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
):
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
],
367 return res
.one()['value']
370 def favorite_episodes_for_user(user
):
373 raise QueryParameterMissing('user')
375 udb
= get_userdata_database()
376 favorites
= udb
.view('favorites/episodes_by_user',
382 episodes
= list(favorites
)
384 for episode
in episodes
:
385 if episode
.needs_update
:
386 incomplete_obj
.send_robust(sender
=episode
)
391 def chapters_for_episode(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',
415 return Counter({x
['key']: x
['value'] for x
in r
})
418 def _wrap_chapter(res
):
419 from mygpo
.users
.models
import Chapter
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
)
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
)
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
)