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 episode
= cache
.get(key
)
191 r
= Episode
.view('episodes/by_podcast_url',
192 key
= [podcast_id
, episode_url
],
200 if episode
.needs_update
:
201 incomplete_obj
.send_robust(sender
=episode
)
203 cache
.set(key
, episode
)
208 episode
.created_timestamp
= get_timestamp(datetime
.utcnow())
209 episode
.podcast
= podcast_id
210 episode
.urls
= [episode_url
]
212 incomplete_obj
.send_robust(sender
=episode
)
218 def episode_for_slug_id(p_slug_id
, e_slug_id
):
219 """ Returns the Episode for Podcast Slug/Id and Episode Slug/Id """
222 raise QueryParameterMissing('p_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
):
236 podcast
= podcast_for_slug_id(p_slug_id
)
241 p_id
= podcast
.get_id()
243 return episode_for_slug(p_id
, e_slug_id
)
246 @cache_result(timeout
=60*60)
248 r
= Episode
.view('episodes/by_podcast',
250 stale
= 'update_after',
252 return r
.one()['value'] if r
else 0
255 def episodes_to_dict(ids
, use_cache
=False):
258 raise QueryParameterMissing('ids')
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
:
284 for i
in obj
.get_ids():
288 cache
.set_many(dict( (obj
._id
, obj
) for obj
in db_objs
))
293 def episode_slugs_per_podcast(podcast_id
, base_slug
):
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'],
304 return [r
['key'][1] for r
in res
]
307 def episodes_for_podcast_uncached(podcast
, since
=None, until
={}, **kwargs
):
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
],
332 for episode
in episodes
:
333 if episode
.needs_update
:
334 incomplete_obj
.send_robust(sender
=episode
)
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
):
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
],
366 return res
.one()['value']
369 def favorite_episodes_for_user(user
):
372 raise QueryParameterMissing('user')
374 udb
= get_userdata_database()
375 favorites
= udb
.view('favorites/episodes_by_user',
381 episodes
= list(favorites
)
383 for episode
in episodes
:
384 if episode
.needs_update
:
385 incomplete_obj
.send_robust(sender
=episode
)
390 def chapters_for_episode(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',
414 return Counter({x
['key']: x
['value'] for x
in r
})
417 def _wrap_chapter(res
):
418 from mygpo
.users
.models
import Chapter
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
)
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
)