2 from dateutil
import parser
3 from couchdbkit
.ext
.django
.schema
import *
4 from mygpo
.decorators
import repeat_on_conflict
5 from mygpo
import utils
8 class Episode(Document
):
10 Represents an Episode. Can only be part of a Podcast
13 title
= StringProperty()
14 description
= StringProperty()
15 link
= StringProperty()
16 released
= DateTimeProperty()
17 author
= StringProperty()
18 duration
= IntegerProperty()
19 filesize
= IntegerProperty()
20 language
= StringProperty()
21 last_update
= DateTimeProperty()
22 outdated
= BooleanProperty()
23 mimetypes
= StringListProperty()
24 merged_ids
= StringListProperty()
25 oldid
= IntegerProperty()
26 urls
= StringListProperty()
27 podcast
= StringProperty(required
=True)
28 listeners
= IntegerProperty()
29 content_types
= StringListProperty()
34 r
= cls
.view('core/episodes_by_id',
38 return r
.first() if r
else None
42 def get_multi(cls
, ids
):
43 return cls
.view('core/episodes_by_id',
50 def for_oldid(self
, oldid
):
51 r
= Episode
.view('core/episodes_by_oldid', key
=oldid
, limit
=1, include_docs
=True)
52 return r
.one() if r
else None
55 def get_old_obj(self
):
57 from mygpo
.api
.models
import Episode
58 return Episode
.objects
.get(id=self
.oldid
)
62 def get_user_state(self
, user
):
63 from mygpo
.users
.models
import EpisodeUserState
64 return EpisodeUserState
.for_user_episode(user
, self
)
72 return 'Episode %s' % self
._id
75 def listener_count(self
, start
=None, end
={}):
76 """ returns the number of users that have listened to this episode """
78 from mygpo
.users
.models
import EpisodeUserState
79 r
= EpisodeUserState
.view('users/listeners_by_episode',
80 startkey
= [self
._id
, start
],
81 endkey
= [self
._id
, end
],
86 return r
.first()['value'] if r
else 0
89 def listener_count_timespan(self
, start
=None, end
={}):
90 """ returns (date, listener-count) tuples for all days w/ listeners """
92 from mygpo
.users
.models
import EpisodeUserState
93 r
= EpisodeUserState
.view('users/listeners_by_episode',
94 startkey
= [self
._id
, start
],
95 endkey
= [self
._id
, end
],
102 date
= parser
.parse(res
['key'][1]).date()
103 listeners
= res
['value']
104 yield (date
, listeners
)
109 r
= cls
.view('core/episodes_by_podcast', limit
=0)
115 return utils
.multi_request_view(cls
, 'core/episodes_by_podcast',
118 def __eq__(self
, other
):
121 return self
.id == other
.id
124 class SubscriberData(DocumentSchema
):
125 timestamp
= DateTimeProperty()
126 subscriber_count
= IntegerProperty()
128 def __eq__(self
, other
):
129 if not isinstance(other
, SubscriberData
):
132 return (self
.timestamp
== other
.timestamp
) and \
133 (self
.subscriber_count
== other
.subscriber_count
)
136 class PodcastSubscriberData(Document
):
137 podcast
= StringProperty()
138 subscribers
= SchemaListProperty(SubscriberData
)
141 def for_podcast(cls
, id):
142 r
= cls
.view('core/subscribers_by_podcast', key
=id, include_docs
=True)
146 data
= PodcastSubscriberData()
151 return 'PodcastSubscriberData for Podcast %s (%s)' % (self
.podcast
, self
._id
)
154 class Podcast(Document
):
155 id = StringProperty()
156 title
= StringProperty()
157 urls
= StringListProperty()
158 description
= StringProperty()
159 link
= StringProperty()
160 last_update
= DateTimeProperty()
161 logo_url
= StringProperty()
162 author
= StringProperty()
163 merged_ids
= StringListProperty()
164 oldid
= IntegerProperty()
165 group
= StringProperty()
166 group_member_name
= StringProperty()
167 related_podcasts
= StringListProperty()
168 subscribers
= SchemaListProperty(SubscriberData
)
169 language
= StringProperty()
170 content_types
= StringListProperty()
171 tags
= DictProperty()
176 r
= cls
.view('core/podcasts_by_id',
178 classes
=[Podcast
, PodcastGroup
],
185 podcast_group
= r
.first()
186 return podcast_group
.get_podcast_by_id(id)
190 def get_multi(cls
, ids
):
191 db
= Podcast
.get_db()
192 r
= db
.view('core/podcasts_by_id',
198 if res
['doc']['doc_type'] == 'Podcast':
199 yield Podcast
.wrap(res
['doc'])
201 pg
= PodcastGroup
.wrap(res
['doc'])
203 yield pg
.get_podcast_by_id(id)
207 def for_oldid(cls
, oldid
):
208 r
= cls
.view('core/podcasts_by_oldid',
210 classes
=[Podcast
, PodcastGroup
],
217 podcast_group
= r
.first()
218 return podcast_group
.get_podcast_by_oldid(oldid
)
222 def for_url(cls
, url
):
223 r
= cls
.view('core/podcasts_by_url',
225 classes
=[Podcast
, PodcastGroup
],
232 podcast_group
= r
.first()
233 return podcast_group
.get_podcast_by_url(url
)
237 def get_podcast_by_id(self
, _
):
239 get_podcast_by_oldid
= get_podcast_by_id
240 get_podcast_by_url
= get_podcast_by_id
244 return self
.id or self
._id
247 def display_title(self
):
248 return self
.title
or self
.url
250 def get_episodes(self
):
251 return list(Episode
.view('core/episodes_by_podcast', key
=self
.get_id(), include_docs
=True))
259 def get_podcast(self
):
263 def get_logo_url(self
, size
):
265 sha
= hashlib
.sha1(self
.logo_url
).hexdigest()
266 return '/logo/%d/%s.jpg' % (size
, sha
)
267 return '/media/podcast-%d.png' % (hash(self
.title
) % 5, )
270 def subscriber_count(self
):
271 if not self
.subscribers
:
273 return self
.subscribers
[-1].subscriber_count
276 def prev_subscriber_count(self
):
277 if len(self
.subscribers
) < 2:
279 return self
.subscribers
[-2].subscriber_count
282 def get_user_state(self
, user
):
283 from mygpo
.users
.models
import PodcastUserState
284 return PodcastUserState
.for_user_podcast(user
, self
)
287 def get_all_states(self
):
288 from mygpo
.users
.models
import PodcastUserState
289 return PodcastUserState
.view('users/podcast_states_by_podcast',
290 startkey
= [self
.get_id(), None],
291 endkey
= [self
.get_id(), '\ufff0'],
295 @repeat_on_conflict()
296 def subscribe(self
, device
):
297 from mygpo
import migrate
298 state
= self
.get_user_state(device
.user
)
299 device
= migrate
.get_or_migrate_device(device
)
300 state
.subscribe(device
)
304 @repeat_on_conflict()
305 def unsubscribe(self
, device
):
306 from mygpo
import migrate
307 state
= self
.get_user_state(device
.user
)
308 device
= migrate
.get_or_migrate_device(device
)
309 state
.unsubscribe(device
)
313 def subscribe_targets(self
, user
):
315 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
316 devices/syncgroups on which the podcast is already subscribed
320 from mygpo
.api
.models
import Device
321 from mygpo
import migrate
323 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
325 dev
= migrate
.get_or_migrate_device(d
)
326 subscriptions
= dev
.get_subscribed_podcasts()
327 if self
in subscriptions
: continue
330 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
339 Returns all tags that are stored for the podcast, in decreasing order of importance
342 res
= Podcast
.view('directory/tags_by_podcast', startkey
=[self
.get_id(), None],
343 endkey
=[self
.get_id(), 'ZZZZZZ'], reduce=True, group
=True, group_level
=2)
344 tags
= sorted(res
.all(), key
=lambda x
: x
['value'], reverse
=True)
345 return [x
['key'][1] for x
in tags
]
348 def listener_count(self
):
349 """ returns the number of users that have listened to this podcast """
351 from mygpo
.users
.models
import EpisodeUserState
352 r
= EpisodeUserState
.view('users/listeners_by_podcast',
353 startkey
= [self
.get_id(), None],
354 endkey
= [self
.get_id(), {}],
359 return r
.first()['value']
362 def listener_count_timespan(self
, start
=None, end
={}):
363 """ returns (date, listener-count) tuples for all days w/ listeners """
365 from mygpo
.users
.models
import EpisodeUserState
366 r
= EpisodeUserState
.view('users/listeners_by_podcast',
367 startkey
= [self
.get_id(), start
],
368 endkey
= [self
.get_id(), end
],
375 date
= parser
.parse(res
['key'][1]).date()
376 listeners
= res
['value']
377 yield (date
, listeners
)
380 def episode_listener_counts(self
):
381 """ (Episode-Id, listener-count) tuples for episodes w/ listeners """
383 from mygpo
.users
.models
import EpisodeUserState
384 r
= EpisodeUserState
.view('users/listeners_by_podcast_episode',
385 startkey
= [self
.get_id(), None, None],
386 endkey
= [self
.get_id(), {}, {}],
393 episode
= res
['key'][1]
394 listeners
= res
['value']
395 yield (episode
, listeners
)
398 def get_episode_states(self
, user_oldid
):
399 """ Returns the latest episode actions for the podcast's episodes """
401 from mygpo
.users
.models
import EpisodeUserState
402 db
= EpisodeUserState
.get_db()
404 res
= db
.view('users/episode_states',
405 startkey
= [user_oldid
, self
.get_id(), None],
406 endkey
= [user_oldid
, self
.get_id(), {}]
414 def get_old_obj(self
):
416 from mygpo
.api
.models
import Podcast
417 return Podcast
.objects
.get(id=self
.oldid
)
422 return hash(self
.get_id())
427 return super(Podcast
, self
).__repr
__()
429 return '%s %s (%s)' % (self
.__class
__.__name
__, self
.get_id(), self
.oldid
)
431 return '%s %s' % (self
.__class
__.__name
__, self
.get_id())
435 group
= getattr(self
, 'group', None)
436 if group
: #we are part of a PodcastGroup
437 group
= PodcastGroup
.get(group
)
438 podcasts
= list(group
.podcasts
)
440 if not self
in podcasts
:
441 # the podcast has not been added to the group correctly
442 group
.add_podcast(self
)
445 i
= podcasts
.index(self
)
447 group
.podcasts
= podcasts
450 i
= podcasts
.index(self
)
452 group
.podcasts
= podcasts
456 super(Podcast
, self
).save()
460 group
= getattr(self
, 'group', None)
462 group
= PodcastGroup
.get(group
)
463 podcasts
= list(group
.podcasts
)
466 i
= podcasts
.index(self
)
468 group
.podcasts
= podcasts
472 super(Podcast
, self
).delete()
475 def all_podcasts_groups(cls
):
476 return cls
.view('core/podcasts_groups', include_docs
=True,
477 classes
=[Podcast
, PodcastGroup
]).iterator()
480 def __eq__(self
, other
):
481 if not self
.get_id():
487 return self
.get_id() == other
.get_id()
491 def all_podcasts(cls
):
492 from mygpo
.utils
import multi_request_view
494 for r
in multi_request_view(cls
, 'core/podcasts_by_oldid', wrap
=False, include_docs
=True):
496 if obj
['doc_type'] == 'Podcast':
497 yield Podcast
.wrap(obj
)
500 pg
= PodcastGroup
.wrap(obj
)
501 podcast
= pg
.get_podcast_by_oldid(oldid
)
506 class PodcastGroup(Document
):
507 title
= StringProperty()
508 podcasts
= SchemaListProperty(Podcast
)
514 def for_oldid(cls
, oldid
):
515 r
= cls
.view('core/podcastgroups_by_oldid', \
516 key
=oldid
, limit
=1, include_docs
=True)
517 return r
.first() if r
else None
519 def get_podcast_by_id(self
, id):
520 for podcast
in self
.podcasts
:
521 if podcast
.get_id() == id:
523 if id in podcast
.merged_ids
:
527 def get_podcast_by_oldid(self
, oldid
):
528 for podcast
in self
.podcasts
:
529 if podcast
.oldid
== oldid
:
533 def get_podcast_by_url(self
, url
):
534 for podcast
in self
.podcasts
:
535 if url
in list(podcast
.urls
):
539 def subscriber_count(self
):
540 return sum([p
.subscriber_count() for p
in self
.podcasts
])
543 def prev_subscriber_count(self
):
544 return sum([p
.prev_subscriber_count() for p
in self
.podcasts
])
547 def display_title(self
):
551 def get_podcast(self
):
552 # return podcast with most subscribers (bug 1390)
553 return sorted(self
.podcasts
, key
=Podcast
.subscriber_count
,
559 return utils
.first(p
.logo_url
for p
in self
.podcasts
)
562 def get_logo_url(self
, size
):
564 sha
= hashlib
.sha1(self
.logo_url
).hexdigest()
565 return '/logo/%d/%s.jpg' % (size
, sha
)
566 return '/media/podcast-%d.png' % (hash(self
.title
) % 5, )
569 def add_podcast(self
, podcast
):
571 podcast
.id = podcast
._id
574 raise ValueError('group has to have an _id first')
577 podcast
.group
= self
._id
578 self
.podcasts
.append(podcast
)
580 return self
.podcasts
[-1]
582 def get_old_obj(self
):
583 from mygpo
.api
.models
import PodcastGroup
584 return PodcastGroup
.objects
.get(id=self
.oldid
) if self
.oldid
else None
589 return super(PodcastGroup
, self
).__repr
__()
591 return '%s %s (%s)' % (self
.__class
__.__name
__, self
._id
[:10], self
.oldid
)
593 return '%s %s' % (self
.__class
__.__name
__, self
._id
[:10])
596 class SanitizingRuleStub(object):
599 class SanitizingRule(Document
):
600 slug
= StringProperty()
601 applies_to
= StringListProperty()
602 search
= StringProperty()
603 replace
= StringProperty()
604 priority
= IntegerProperty()
605 description
= StringProperty()
609 def for_obj_type(cls
, obj_type
):
610 r
= cls
.view('core/sanitizing_rules_by_target', include_docs
=True,
611 startkey
=[obj_type
, None], endkey
=[obj_type
, {}])
614 obj
= SanitizingRuleStub()
616 obj
.applies_to
= list(rule
.applies_to
)
617 obj
.search
= rule
.search
618 obj
.replace
= rule
.replace
619 obj
.priority
= rule
.priority
620 obj
.description
= rule
.description
625 def for_slug(cls
, slug
):
626 r
= cls
.view('core/sanitizing_rules_by_slug', include_docs
=True,
628 return r
.one() if r
else None
632 return 'SanitizingRule %s' % self
._id