1 from hashlib
import sha1
2 from random
import random
3 from datetime
import datetime
5 from restkit
import RequestFailed
7 from django
.core
.cache
import cache
9 from mygpo
.core
.models
import Podcast
, PodcastGroup
, PodcastSubscriberData
10 from mygpo
.core
.signals
import incomplete_obj
11 from mygpo
.decorators
import repeat_on_conflict
12 from mygpo
.cache
import cache_result
13 from mygpo
.utils
import get_timestamp
14 from mygpo
.db
.couchdb
import get_main_database
, get_userdata_database
15 from mygpo
.db
import QueryParameterMissing
16 from mygpo
.db
.couchdb
.utils
import multi_request_view
, is_couchdb_id
19 def podcast_slugs(base_slug
):
20 res
= Podcast
.view('podcasts/by_slug',
21 startkey
= [base_slug
, None],
22 endkey
= [base_slug
+ 'ZZZZZ', None],
25 return [r
['key'][0] for r
in res
]
28 @cache_result(timeout
=60*60)
30 return Podcast
.view('podcasts/by_id',
32 stale
= 'update_after',
36 @cache_result(timeout
=60*60)
37 def podcasts_for_tag(tag
):
38 """ Returns the podcasts with the current tag.
40 Some podcasts might be returned twice """
43 raise QueryParameterMissing('tag')
45 res
= multi_request_view(Podcast
, 'podcasts/by_tag',
47 startkey
= [tag
, None],
55 yield (r
['key'][1], r
['value'])
57 udb
= get_userdata_database()
58 res
= multi_request_view(udb
, 'usertags/podcasts',
60 startkey
= [tag
, None],
68 yield (r
['key'][1], r
['value'])
71 @cache_result(timeout
=60*60)
72 def get_podcast_languages():
73 """ Returns all 2-letter language codes that are used by podcasts.
75 It filters obviously invalid strings, but does not check if any
76 of these codes is contained in ISO 639. """
78 from mygpo
.web
.utils
import sanitize_language_codes
80 res
= Podcast
.view('podcasts/by_language',
85 langs
= [r
['key'][0] for r
in res
]
86 sane_lang
= sanitize_language_codes(langs
)
91 def podcast_by_id_uncached(podcast_id
, current_id
=False):
94 raise QueryParameterMissing('podcast_id')
96 r
= Podcast
.view('podcasts/by_id',
98 classes
= [Podcast
, PodcastGroup
],
105 podcast_group
= r
.first()
107 podcast
= podcast_group
.get_podcast_by_id(podcast_id
, current_id
)
109 if podcast
.needs_update
:
110 incomplete_obj
.send_robust(sender
=podcast
)
115 podcast_by_id
= cache_result(timeout
=60*60)(podcast_by_id_uncached
)
118 @cache_result(timeout
=60*60)
119 def podcastgroup_by_id(group_id
):
122 raise QueryParameterMissing('group_id')
124 pg
= PodcastGroup
.get(group_id
)
127 incomplete_obj
.send_robust(sender
=pg
)
133 @cache_result(timeout
=60*60)
134 def podcast_for_slug(slug
):
137 raise QueryParameterMissing('slug')
139 r
= Podcast
.view('podcasts/by_slug',
140 startkey
= [slug
, None],
151 if doc
['doc_type'] == 'Podcast':
152 obj
= Podcast
.wrap(doc
)
155 pg
= PodcastGroup
.wrap(doc
)
158 # TODO: we don't return PodcastGroups atm
161 obj
= pg
.get_podcast_by_id(pid
)
164 incomplete_obj
.send_robust(sender
=obj
)
169 @cache_result(timeout
=60*60)
170 def podcast_for_slug_id(slug_id
):
171 """ Returns the Podcast for either an CouchDB-ID for a Slug """
173 if is_couchdb_id(slug_id
):
174 return podcast_by_id(slug_id
)
176 return podcast_for_slug(slug_id
)
179 @cache_result(timeout
=60*60)
180 def podcastgroup_for_slug_id(slug_id
):
181 """ Returns the Podcast for either an CouchDB-ID for a Slug """
184 raise QueryParameterMissing('slug_id')
186 if is_couchdb_id(slug_id
):
187 return podcastgroup_by_id(slug_id
)
191 return PodcastGroup
.for_slug(slug_id
)
195 def podcasts_by_id(ids
):
198 raise QueryParameterMissing('ids')
203 r
= Podcast
.view('podcasts/by_id',
209 podcasts
= map(_wrap_podcast_group
, r
)
211 for podcast
in podcasts
:
212 if podcast
.needs_update
:
213 incomplete_obj
.send_robust(sender
=podcast
)
218 def podcasts_groups_by_id(ids
):
219 """ gets podcast groups and top-level podcasts for the given ids """
222 raise QueryParameterMissing('ids')
227 db
= get_main_database()
228 res
= db
.view('podcasts/podcasts_groups',
231 classes
= [Podcast
, PodcastGroup
],
240 if doc
['doc_type'] == 'Podcast':
241 obj
= Podcast
.wrap(doc
)
243 elif doc
['doc_type'] == 'PodcastGroup':
244 obj
= PodcastGroup
.wrap(doc
)
247 logger
.error('podcasts_groups_by_id retrieved unknown doc_type '
248 '"%s" for params %s', doc
['doc_type'], res
.params
)
252 incomplete_obj
.send_robust(sender
=obj
)
258 @cache_result(timeout
=60*60)
259 def podcast_for_oldid(oldid
):
262 raise QueryParameterMissing('oldid')
264 r
= Podcast
.view('podcasts/by_oldid',
266 classes
= [Podcast
, PodcastGroup
],
273 podcast_group
= r
.first()
274 podcast
= podcast_group
.get_podcast_by_oldid(oldid
)
276 if podcast
.needs_update
:
277 incomplete_obj
.send_robust(sender
=podcast
)
282 @cache_result(timeout
=60*60)
283 def podcastgroup_for_oldid(oldid
):
286 raise QueryParameterMissing('oldid')
288 r
= PodcastGroup
.view('podcasts/groups_by_oldid',
299 incomplete_obj
.send_robust(sender
=pg
)
304 def podcast_for_url(url
, create
=False):
307 raise QueryParameterMissing('url')
309 key
= 'podcast-by-url-%s' % sha1(url
.encode('utf-8')).hexdigest()
311 podcast
= cache
.get(key
)
315 r
= Podcast
.view('podcasts/by_url',
317 classes
=[Podcast
, PodcastGroup
],
322 podcast_group
= r
.first()
323 podcast
= podcast_group
.get_podcast_by_url(url
)
325 if podcast
.needs_update
:
326 incomplete_obj
.send_robust(sender
=podcast
)
328 cache
.set(key
, podcast
)
334 podcast
.created_timestamp
= get_timestamp(datetime
.utcnow())
337 incomplete_obj
.send_robust(sender
=podcast
)
345 def random_podcasts(language
='', chunk_size
=5):
346 """ Returns an iterator of random podcasts
348 optionaly a language code can be specified. If given the podcasts will
349 be restricted to this language. chunk_size determines how many podcasts
350 will be fetched at once """
354 res
= Podcast
.view('podcasts/random',
355 startkey
= [language
, rnd
],
367 # The view podcasts/random does not include incomplete podcasts,
368 # so we don't need to send any 'incomplete_obj' signals here
371 if obj
['doc_type'] == 'Podcast':
372 yield Podcast
.wrap(obj
)
374 elif obj
['doc_type'] == 'PodcastGroup':
375 yield PodcastGroup
.wrap(obj
)
379 def podcasts_by_last_update():
380 res
= Podcast
.view('podcasts/by_last_update',
382 stale
= 'update_after',
386 # TODO: this method is only used for retrieving podcasts to update;
387 # should we really send 'incomplete_obj' signals here?
389 return map(_wrap_podcast_group_key1
, res
)
395 from mygpo
.db
.couchdb
.utils
import multi_request_view
396 res
= multi_request_view(Podcast
,'podcasts/by_id',
399 stale
= 'update_after',
402 # TODO: this method is only used for maintenance purposes; should we
403 # really send 'incomplete_obj' signals here?
407 if obj
['doc_type'] == 'Podcast':
408 yield Podcast
.wrap(obj
)
411 pg
= PodcastGroup
.wrap(obj
)
412 podcast
= pg
.get_podcast_by_id(pid
)
416 def podcasts_to_dict(ids
, use_cache
=False):
419 raise QueryParameterMissing('ids')
430 res
= cache
.get_many(ids
)
431 cache_objs
.extend(res
.values())
432 ids
= [x
for x
in ids
if x
not in res
.keys()]
434 db_objs
= podcasts_by_id(ids
)
436 for obj
in (cache_objs
+ db_objs
):
438 # get_multi returns dict {'key': _id, 'error': 'not found'}
439 # for non-existing objects
440 if isinstance(obj
, dict) and 'error' in obj
:
445 for i
in obj
.get_ids():
449 cache
.set_many(dict( (obj
.get_id(), obj
) for obj
in db_objs
))
455 def podcasts_need_update():
456 db
= get_main_database()
457 res
= db
.view('episodes/need_update',
463 # TODO: this method is only used for retrieving podcasts to update;
464 # should we really send 'incomplete_obj' signals here?
467 podcast_id
= r
['key']
468 podcast
= podcast_by_id(podcast_id
)
473 @cache_result(timeout
=60*60)
474 def get_flattr_podcasts(offset
=0, limit
=20):
475 """ returns all podcasts that contain Flattr payment URLs """
477 r
= Podcast
.view('podcasts/flattr',
480 classes
= [Podcast
, PodcastGroup
],
487 for podcast
in podcasts
:
488 if podcast
.needs_update
:
489 incomplete_obj
.send_robust(sender
=podcast
)
494 @cache_result(timeout
=60*60)
495 def get_flattr_podcast_count():
496 """ returns the number of podcasts that contain Flattr payment URLs """
497 r
= list(Podcast
.view('podcasts/flattr'))
501 @cache_result(timeout
=60*60)
502 def get_license_podcasts(offset
=0, limit
=20):
503 """ returns a page of podcasts w/ license information """
505 r
= Podcast
.view('podcasts/license',
508 classes
= [Podcast
, PodcastGroup
],
515 for podcast
in podcasts
:
516 if podcast
.needs_update
:
517 incomplete_obj
.send_robust(sender
=podcast
)
522 @cache_result(timeout
=60*60)
523 def get_license_podcast_count():
524 """ returns the number of podcasts that contain license information """
525 r
= list(Podcast
.view('podcasts/license'))
526 return r
[0]['value'] if r
else 0
529 def subscriberdata_for_podcast(podcast_id
):
532 raise QueryParameterMissing('podcast_id')
534 r
= PodcastSubscriberData
.view('podcasts/subscriber_data',
542 data
= PodcastSubscriberData()
543 data
.podcast
= podcast_id
548 def _wrap_podcast_group(res
):
549 if res
['doc']['doc_type'] == 'Podcast':
550 return Podcast
.wrap(res
['doc'])
552 pg
= PodcastGroup
.wrap(res
['doc'])
554 return pg
.get_podcast_by_id(id)
557 def _wrap_podcast_group_key1(res
):
559 if obj
['doc_type'] == 'Podcast':
560 return Podcast
.wrap(obj
)
564 pg
= PodcastGroup
.wrap(obj
)
565 podcast
= pg
.get_podcast_by_id(pid
)
570 def search_wrapper(result
):
572 if doc
['doc_type'] == 'Podcast':
573 p
= Podcast
.wrap(doc
)
574 elif doc
['doc_type'] == 'PodcastGroup':
575 p
= PodcastGroup
.wrap(doc
)
580 @cache_result(timeout
=60*60)
581 def search(q
, offset
=0, num_results
=20):
586 db
= get_main_database()
588 #FIXME current couchdbkit can't parse responses for multi-query searches
589 q
= q
.replace(',', '')
592 res
= db
.search('podcasts/search',
593 wrapper
= search_wrapper
,
596 stale
= 'update_after',
599 sort
='\\subscribers<int>')
603 for podcast
in podcasts
:
604 if podcast
.needs_update
:
605 incomplete_obj
.send_robust(sender
=podcast
)
607 return podcasts
, res
.total_rows
609 except RequestFailed
:
613 def reload_podcast(podcast
):
614 return podcast_by_id_uncached(podcast
.get_id())
617 @repeat_on_conflict(['podcast'], reload_f
=reload_podcast
)
618 def update_additional_data(podcast
, twitter
):
619 podcast
.twitter
= twitter
622 # clear the whole cache until we have a better invalidation mechanism
626 @repeat_on_conflict(['podcast'], reload_f
=reload_podcast
)
627 def update_related_podcasts(podcast
, related
):
628 if podcast
.related_podcasts
== related
:
631 podcast
.related_podcasts
= related
635 @repeat_on_conflict(['podcast'], reload_f
=reload_podcast
)
636 def delete_podcast(podcast
):